mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-17 08:15:16 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2c0109bdd |
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
||||||
|
"name": "1c-skills-py",
|
||||||
|
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
||||||
|
"author": {
|
||||||
|
"name": "Nikolay Shirokov"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
||||||
|
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"1c",
|
||||||
|
"1c-dev",
|
||||||
|
"cf",
|
||||||
|
"cfe",
|
||||||
|
"epf",
|
||||||
|
"erf",
|
||||||
|
"metadata",
|
||||||
|
"configuration",
|
||||||
|
"extension",
|
||||||
|
"form",
|
||||||
|
"report",
|
||||||
|
"skd",
|
||||||
|
"data-processor",
|
||||||
|
"mxl",
|
||||||
|
"web-client",
|
||||||
|
"testing",
|
||||||
|
"test-automation"
|
||||||
|
],
|
||||||
|
"skills": "./.claude/skills/"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: cf-edit
|
name: cf-edit
|
||||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
||||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
@@ -32,11 +32,13 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
|
|||||||
| Операция | Формат Value | Описание |
|
| Операция | Формат Value | Описание |
|
||||||
|----------|-------------|----------|
|
|----------|-------------|----------|
|
||||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||||
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
|
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||||
|
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
|
||||||
|
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
|
||||||
|
|
||||||
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
||||||
|
|
||||||
@@ -44,15 +46,15 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Изменить версию и поставщика
|
# Изменить версию и поставщика
|
||||||
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||||
|
|
||||||
# Добавить объекты
|
# Добавить объекты
|
||||||
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||||
|
|
||||||
# Удалить объект
|
# Удалить объект
|
||||||
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||||
|
|
||||||
# Роли по умолчанию
|
# Роли по умолчанию
|
||||||
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
|
||||||
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
### Enum
|
### Enum
|
||||||
| Свойство | Допустимые значения |
|
| Свойство | Допустимые значения |
|
||||||
|----------|---------------------|
|
|----------|---------------------|
|
||||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
|
||||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||||
| `ScriptVariant` | `Russian`, `English` |
|
| `ScriptVariant` | `Russian`, `English` |
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||||
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
|
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
|
||||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||||
|
|
||||||
@@ -35,10 +35,7 @@
|
|||||||
|
|
||||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||||
|
|
||||||
При добавлении объект вставляется в каноническую позицию:
|
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
|
||||||
1. Находит последний элемент того же типа → вставляет после
|
|
||||||
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
|
|
||||||
3. Внутри одного типа — алфавитный порядок
|
|
||||||
|
|
||||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||||
|
|
||||||
@@ -48,6 +45,99 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
|
|||||||
|
|
||||||
`set-defaultRoles` полностью заменяет список ролей.
|
`set-defaultRoles` полностью заменяет список ролей.
|
||||||
|
|
||||||
|
## set-panels
|
||||||
|
|
||||||
|
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
|
||||||
|
|
||||||
|
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
|
||||||
|
|
||||||
|
**Запись** — одна из:
|
||||||
|
- Строка-алиас (одна панель в этом слоте)
|
||||||
|
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
|
||||||
|
|
||||||
|
**Алиасы панелей:**
|
||||||
|
|
||||||
|
| Алиас | Панель |
|
||||||
|
|-------|--------|
|
||||||
|
| `sections` | Панель разделов |
|
||||||
|
| `open` | Панель открытых |
|
||||||
|
| `favorites` | Панель избранного |
|
||||||
|
| `history` | Панель истории |
|
||||||
|
| `functions` | Панель функций текущего раздела |
|
||||||
|
|
||||||
|
**Семантика:**
|
||||||
|
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
|
||||||
|
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
|
||||||
|
|
||||||
|
**Пример** (DefinitionFile):
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"operation": "set-panels",
|
||||||
|
"value": {
|
||||||
|
"top": ["open"],
|
||||||
|
"left": ["sections"],
|
||||||
|
"right": [{ "group": ["favorites", "history"] }],
|
||||||
|
"bottom": ["functions"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
|
||||||
|
|
||||||
|
## set-home-page
|
||||||
|
|
||||||
|
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
|
||||||
|
|
||||||
|
`value` — объект:
|
||||||
|
|
||||||
|
| Ключ | Канонич. (XML) | Описание |
|
||||||
|
|------|----------------|----------|
|
||||||
|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
|
||||||
|
| `left` | `LeftColumn` | массив записей форм |
|
||||||
|
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
|
||||||
|
|
||||||
|
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
|
||||||
|
|
||||||
|
**Запись формы** — одна из:
|
||||||
|
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
|
||||||
|
- Объект `{form, height?, visibility?, roles?}`
|
||||||
|
|
||||||
|
| Поле | Канонич. | Дефолт | Описание |
|
||||||
|
|------|----------|--------|----------|
|
||||||
|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
|
||||||
|
| `height` | `Height` | `10` | Высота |
|
||||||
|
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
|
||||||
|
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
|
||||||
|
|
||||||
|
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"operation": "set-home-page",
|
||||||
|
"value": {
|
||||||
|
"template": "TwoColumnsVariableWidth",
|
||||||
|
"left": [
|
||||||
|
"CommonForm.НачалоРаботы",
|
||||||
|
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
|
||||||
|
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
|
||||||
|
{
|
||||||
|
"form": "CommonForm.РабочийСтолОператора",
|
||||||
|
"visibility": false,
|
||||||
|
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
## DefinitionFile (JSON)
|
## DefinitionFile (JSON)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -58,6 +148,3 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Авто-валидация
|
|
||||||
|
|
||||||
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
|
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[string]$Value,
|
[string]$Value,
|
||||||
[switch]$NoValidate
|
[switch]$NoValidate
|
||||||
@@ -27,6 +27,127 @@ if (Test-Path $ConfigPath -PathType Container) {
|
|||||||
}
|
}
|
||||||
if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 }
|
if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 }
|
||||||
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
||||||
|
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||||
|
|
||||||
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
|
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||||
|
# throws — guard errors degrade to allow.
|
||||||
|
function Get-RootUuid([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $null }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Get-EditMode([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||||
|
if (-not $pj) { return 'deny' }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||||
|
return 'deny'
|
||||||
|
} catch { return 'deny' }
|
||||||
|
}
|
||||||
|
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||||
|
try {
|
||||||
|
$rp = $targetPath
|
||||||
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
$cfgDir = $null; $binPath = $null
|
||||||
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
|
if (-not $cfgDir) {
|
||||||
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||||
|
}
|
||||||
|
if ($elemUuid -and $cfgDir) { break }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
# New object (no element file): fall back to config root uuid.
|
||||||
|
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||||
|
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return }
|
||||||
|
$start = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||||
|
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $hm.Success) { return }
|
||||||
|
$G = [int]$hm.Groups[1].Value
|
||||||
|
$K = [int]$hm.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return }
|
||||||
|
$best = $null
|
||||||
|
if ($elemUuid) {
|
||||||
|
$u = [regex]::Escape($elemUuid.ToLower())
|
||||||
|
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||||
|
$f1 = [int]$m.Groups[1].Value
|
||||||
|
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$blocked = $false; $code = ""; $reason = ""
|
||||||
|
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||||
|
elseif ($require -eq 'removed') {
|
||||||
|
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||||
|
}
|
||||||
|
if (-not $blocked) { return }
|
||||||
|
$mode = Get-EditMode $cfgDir
|
||||||
|
if ($mode -eq 'off') { return }
|
||||||
|
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||||
|
# latter throws and would be swallowed by this function's own catch.
|
||||||
|
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||||
|
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if ($code -eq "capability-off") {
|
||||||
|
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||||
|
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
} elseif ($code -eq "not-removed") {
|
||||||
|
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||||
|
} else {
|
||||||
|
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||||
|
}
|
||||||
|
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||||
|
exit 1
|
||||||
|
} catch { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-EditAllowed $resolvedPath 'editable'
|
||||||
|
|
||||||
# --- Load XML with PreserveWhitespace ---
|
# --- Load XML with PreserveWhitespace ---
|
||||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
@@ -75,7 +196,7 @@ Info "Configuration: $($script:objName)"
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -87,6 +208,22 @@ $script:typeOrder = @(
|
|||||||
"BusinessProcess","Task","IntegrationService"
|
"BusinessProcess","Task","IntegrationService"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Type → on-disk directory name (plural) ---
|
||||||
|
$script:typeToDir = @{
|
||||||
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||||
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||||
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||||
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||||
|
"Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
|
||||||
|
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums"
|
||||||
|
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||||
|
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||||
|
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||||
|
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
|
||||||
|
}
|
||||||
|
|
||||||
# --- XML manipulation helpers (from subsystem-edit pattern) ---
|
# --- XML manipulation helpers (from subsystem-edit pattern) ---
|
||||||
function Get-ChildIndent($container) {
|
function Get-ChildIndent($container) {
|
||||||
foreach ($child in $container.ChildNodes) {
|
foreach ($child in $container.ChildNodes) {
|
||||||
@@ -247,6 +384,29 @@ function Do-AddChildObject([string]$batchVal) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check that the referenced object actually exists on disk.
|
||||||
|
# cf-edit add-childObject is a low-level operation for rare scenarios
|
||||||
|
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
||||||
|
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
||||||
|
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
||||||
|
# unnecessary and error-prone.
|
||||||
|
$typeDir = $script:typeToDir[$typeName]
|
||||||
|
$objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml"
|
||||||
|
if (-not (Test-Path $objFile)) {
|
||||||
|
$hintSkill = switch ($typeName) {
|
||||||
|
"Subsystem" { "subsystem-compile" }
|
||||||
|
"Role" { "role-compile" }
|
||||||
|
default { "meta-compile" }
|
||||||
|
}
|
||||||
|
Write-Error @"
|
||||||
|
Object file not found: $typeDir/$objNameVal.xml
|
||||||
|
cf-edit add-childObject only references objects that already exist on disk.
|
||||||
|
To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||||
|
/$hintSkill with {"type":"$typeName","name":"$objNameVal"}
|
||||||
|
"@
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# Dedup check
|
# Dedup check
|
||||||
$existing = $false
|
$existing = $false
|
||||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||||
@@ -404,6 +564,309 @@ function Do-RemoveDefaultRole([string]$batchVal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Operation: set-panels ---
|
||||||
|
# Canonical English aliases — preferred form, used in docs and error messages.
|
||||||
|
$script:panelUuids = @{
|
||||||
|
"sections" = "b553047f-c9aa-4157-978d-448ecad24248"
|
||||||
|
"open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"
|
||||||
|
"favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3"
|
||||||
|
"history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99"
|
||||||
|
"functions" = "b2735bd3-d822-4430-ba59-c9e869693b24"
|
||||||
|
}
|
||||||
|
# Russian synonyms — silently accepted (cf-info displays Russian names; users
|
||||||
|
# may copy them straight into cf-edit value).
|
||||||
|
$script:panelSynonyms = @{
|
||||||
|
"разделов" = "sections"; "разделы" = "sections"
|
||||||
|
"открытых" = "open"; "открытые" = "open"
|
||||||
|
"избранного" = "favorites";"избранное" = "favorites"
|
||||||
|
"истории" = "history"; "история" = "history"
|
||||||
|
"функций" = "functions";"функции" = "functions"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-PanelEntryXml($entry, [string]$indent) {
|
||||||
|
# String alias -> <panel><uuid>...</uuid></panel>
|
||||||
|
if ($entry -is [string]) {
|
||||||
|
$key = $entry.ToLowerInvariant()
|
||||||
|
if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] }
|
||||||
|
if (-not $script:panelUuids.ContainsKey($key)) {
|
||||||
|
Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$u = $script:panelUuids[$key]
|
||||||
|
$instId = [guid]::NewGuid().ToString()
|
||||||
|
return "$indent<panel id=`"$instId`">`r`n$indent`t<uuid>$u</uuid>`r`n$indent</panel>"
|
||||||
|
}
|
||||||
|
# Object {group: [...]} -> <group id=""><group><panel/></group>...</group> (stack)
|
||||||
|
if ($entry.PSObject.Properties['group']) {
|
||||||
|
$children = $entry.group
|
||||||
|
if (-not $children -or $children.Count -eq 0) {
|
||||||
|
Write-Error "group must contain at least one entry"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$gid = [guid]::NewGuid().ToString()
|
||||||
|
$inner = ""
|
||||||
|
foreach ($child in $children) {
|
||||||
|
$childXml = Build-PanelEntryXml $child "$indent`t`t"
|
||||||
|
$inner += "$indent`t<group>`r`n$childXml`r`n$indent`t</group>`r`n"
|
||||||
|
}
|
||||||
|
return "$indent<group id=`"$gid`">`r`n$inner$indent</group>"
|
||||||
|
}
|
||||||
|
Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-SetPanels($valArg) {
|
||||||
|
# Accept string (JSON), PSCustomObject, or hashtable
|
||||||
|
$layout = $valArg
|
||||||
|
if ($layout -is [string]) {
|
||||||
|
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||||
|
Write-Error "set-panels value must be valid JSON object, got: $valArg"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $layout) {
|
||||||
|
Write-Error "set-panels value is empty"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$sides = @("top","left","right","bottom")
|
||||||
|
$bodyParts = @()
|
||||||
|
foreach ($side in $sides) {
|
||||||
|
$entries = $null
|
||||||
|
if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side }
|
||||||
|
if ($null -eq $entries) { continue }
|
||||||
|
# Normalize to array
|
||||||
|
if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) {
|
||||||
|
$entries = @($entries)
|
||||||
|
}
|
||||||
|
foreach ($entry in $entries) {
|
||||||
|
$entryXml = Build-PanelEntryXml $entry "`t`t"
|
||||||
|
$bodyParts += "`t<$side>`r`n$entryXml`r`n`t</$side>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reject unknown side keys (catches typos like "Top" vs "top")
|
||||||
|
foreach ($prop in $layout.PSObject.Properties) {
|
||||||
|
if ($sides -notcontains $prop.Name) {
|
||||||
|
Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $bodyParts -join "`r`n"
|
||||||
|
$declarations = @"
|
||||||
|
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
"@
|
||||||
|
$bodyBlock = if ($body) { "$body`r`n" } else { "" }
|
||||||
|
$caiXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
$bodyBlock$declarations
|
||||||
|
</ClientApplicationInterface>
|
||||||
|
"@
|
||||||
|
|
||||||
|
$extDir = Join-Path $script:configDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
|
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Wrote panel layout: $caiPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Operation: set-home-page ---
|
||||||
|
# Russian → English type aliases for form-ref normalization
|
||||||
|
$script:ruTypeMap = @{
|
||||||
|
"справочник" = "Catalog"
|
||||||
|
"документ" = "Document"
|
||||||
|
"перечисление" = "Enum"
|
||||||
|
"отчёт" = "Report"
|
||||||
|
"отчет" = "Report"
|
||||||
|
"обработка" = "DataProcessor"
|
||||||
|
"общаяформа" = "CommonForm"
|
||||||
|
"журналдокументов" = "DocumentJournal"
|
||||||
|
"планвидовхарактеристик" = "ChartOfCharacteristicTypes"
|
||||||
|
"плансчетов" = "ChartOfAccounts"
|
||||||
|
"планвидоврасчета" = "ChartOfCalculationTypes"
|
||||||
|
"планвидоврасчёта" = "ChartOfCalculationTypes"
|
||||||
|
"регистрсведений" = "InformationRegister"
|
||||||
|
"регистрнакопления" = "AccumulationRegister"
|
||||||
|
"регистрбухгалтерии" = "AccountingRegister"
|
||||||
|
"регистррасчета" = "CalculationRegister"
|
||||||
|
"регистррасчёта" = "CalculationRegister"
|
||||||
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
|
"планобмена" = "ExchangePlan"
|
||||||
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
|
}
|
||||||
|
# plural folder → singular type
|
||||||
|
$script:dirToType = @{}
|
||||||
|
foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k }
|
||||||
|
|
||||||
|
function Normalize-FormRef([string]$s) {
|
||||||
|
$s = $s.Trim()
|
||||||
|
if (-not $s) { return $s }
|
||||||
|
# UUID — leave as-is
|
||||||
|
if ($s -match '^[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}$') { return $s }
|
||||||
|
# Path form?
|
||||||
|
if ($s.Contains("/") -or $s.Contains("\")) {
|
||||||
|
$parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" }
|
||||||
|
# Strip trailing Form.xml
|
||||||
|
if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") {
|
||||||
|
$parts = @($parts[0..($parts.Count - 2)])
|
||||||
|
}
|
||||||
|
if ($parts.Count -ge 2) {
|
||||||
|
$typeDir = $parts[0]
|
||||||
|
$typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()]
|
||||||
|
if ($typeSingular) {
|
||||||
|
if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) {
|
||||||
|
return "CommonForm.$($parts[1])"
|
||||||
|
}
|
||||||
|
if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") {
|
||||||
|
return "$typeSingular.$($parts[1]).Form.$($parts[3])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $s
|
||||||
|
}
|
||||||
|
# Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form'
|
||||||
|
$segs = $s.Split(".")
|
||||||
|
if ($segs.Count -ge 1) {
|
||||||
|
$head = $segs[0].ToLowerInvariant()
|
||||||
|
if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] }
|
||||||
|
for ($i = 1; $i -lt $segs.Count; $i++) {
|
||||||
|
if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" }
|
||||||
|
}
|
||||||
|
# Auto-insert Form: for object types with 3 segments (Type.Object.FormName)
|
||||||
|
if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") {
|
||||||
|
$segs = @($segs[0], $segs[1], "Form", $segs[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ($segs -join ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Accept short DSL or canonical XML keys (silently)
|
||||||
|
function Get-FieldValue($obj, [string[]]$keys) {
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-HomePageItemXml($entry, [string]$indent) {
|
||||||
|
# Resolve fields
|
||||||
|
if ($entry -is [string]) {
|
||||||
|
$formRef = Normalize-FormRef $entry
|
||||||
|
$height = 10
|
||||||
|
$common = $true
|
||||||
|
$roles = $null
|
||||||
|
} else {
|
||||||
|
$formRaw = Get-FieldValue $entry @("form","Form")
|
||||||
|
if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 }
|
||||||
|
$formRef = Normalize-FormRef ([string]$formRaw)
|
||||||
|
$h = Get-FieldValue $entry @("height","Height")
|
||||||
|
$height = if ($null -ne $h) { [int]$h } else { 10 }
|
||||||
|
$vis = Get-FieldValue $entry @("visibility","Visibility")
|
||||||
|
$common = if ($null -ne $vis) { [bool]$vis } else { $true }
|
||||||
|
$roles = Get-FieldValue $entry @("roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
$visParts = @()
|
||||||
|
$visParts += "$indent`t`t<xr:Common>$($common.ToString().ToLower())</xr:Common>"
|
||||||
|
if ($roles) {
|
||||||
|
# roles is PSCustomObject {Role.X: bool, ...}
|
||||||
|
foreach ($prop in $roles.PSObject.Properties) {
|
||||||
|
$rname = $prop.Name
|
||||||
|
if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" }
|
||||||
|
$rval = ([bool]$prop.Value).ToString().ToLower()
|
||||||
|
$escName = [System.Security.SecurityElement]::Escape($rname)
|
||||||
|
$visParts += "$indent`t`t<xr:Value name=`"$escName`">$rval</xr:Value>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$visBlock = $visParts -join "`r`n"
|
||||||
|
$escForm = [System.Security.SecurityElement]::Escape($formRef)
|
||||||
|
return @"
|
||||||
|
$indent<Item>
|
||||||
|
$indent`t<Form>$escForm</Form>
|
||||||
|
$indent`t<Height>$height</Height>
|
||||||
|
$indent`t<Visibility>
|
||||||
|
$visBlock
|
||||||
|
$indent`t</Visibility>
|
||||||
|
$indent</Item>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-SetHomePage($valArg) {
|
||||||
|
$layout = $valArg
|
||||||
|
if ($layout -is [string]) {
|
||||||
|
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||||
|
Write-Error "set-home-page value must be valid JSON object"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
||||||
|
|
||||||
|
$allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth")
|
||||||
|
$tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate")
|
||||||
|
if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" }
|
||||||
|
if ($allowedTemplates -notcontains $tmpl) {
|
||||||
|
Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$leftItems = Get-FieldValue $layout @("left","LeftColumn")
|
||||||
|
$rightItems = Get-FieldValue $layout @("right","RightColumn")
|
||||||
|
|
||||||
|
# Reject unknown keys
|
||||||
|
$known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn")
|
||||||
|
foreach ($prop in $layout.PSObject.Properties) {
|
||||||
|
if ($known -notcontains $prop.Name) {
|
||||||
|
Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tmpl -eq "OneColumn" -and $rightItems) {
|
||||||
|
Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-Column([string]$tag, $items) {
|
||||||
|
if (-not $items) { return "`t<$tag/>" }
|
||||||
|
if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) {
|
||||||
|
$items = @($items)
|
||||||
|
}
|
||||||
|
if ($items.Count -eq 0) { return "`t<$tag/>" }
|
||||||
|
$itemBlocks = @()
|
||||||
|
foreach ($it in $items) {
|
||||||
|
$itemBlocks += Build-HomePageItemXml $it "`t`t"
|
||||||
|
}
|
||||||
|
$body = $itemBlocks -join "`r`n"
|
||||||
|
return "`t<$tag>`r`n$body`r`n`t</$tag>"
|
||||||
|
}
|
||||||
|
|
||||||
|
$leftXml = Build-Column "LeftColumn" $leftItems
|
||||||
|
$rightXml = Build-Column "RightColumn" $rightItems
|
||||||
|
|
||||||
|
$hpXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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">
|
||||||
|
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
||||||
|
$leftXml
|
||||||
|
$rightXml
|
||||||
|
</HomePageWorkArea>
|
||||||
|
"@
|
||||||
|
|
||||||
|
$extDir = Join-Path $script:configDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
|
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Wrote home page layout: $hpPath"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Operation: set-defaultRoles ---
|
# --- Operation: set-defaultRoles ---
|
||||||
function Do-SetDefaultRoles([string]$batchVal) {
|
function Do-SetDefaultRoles([string]$batchVal) {
|
||||||
$items = Parse-BatchValue $batchVal
|
$items = Parse-BatchValue $batchVal
|
||||||
@@ -468,15 +931,19 @@ if ($DefinitionFile) {
|
|||||||
|
|
||||||
foreach ($op in $operations) {
|
foreach ($op in $operations) {
|
||||||
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
||||||
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
|
# Pass value through as-is (object or string); set-panels needs object form
|
||||||
|
$opValue = if ($null -ne $op.value) { $op.value } else { $Value }
|
||||||
|
$opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" }
|
||||||
|
|
||||||
switch ($opName) {
|
switch ($opName) {
|
||||||
"modify-property" { Do-ModifyProperty $opValue }
|
"modify-property" { Do-ModifyProperty $opValueStr }
|
||||||
"add-childObject" { Do-AddChildObject $opValue }
|
"add-childObject" { Do-AddChildObject $opValueStr }
|
||||||
"remove-childObject" { Do-RemoveChildObject $opValue }
|
"remove-childObject" { Do-RemoveChildObject $opValueStr }
|
||||||
"add-defaultRole" { Do-AddDefaultRole $opValue }
|
"add-defaultRole" { Do-AddDefaultRole $opValueStr }
|
||||||
"remove-defaultRole" { Do-RemoveDefaultRole $opValue }
|
"remove-defaultRole" { Do-RemoveDefaultRole $opValueStr }
|
||||||
"set-defaultRoles" { Do-SetDefaultRoles $opValue }
|
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
||||||
|
"set-panels" { Do-SetPanels $opValue }
|
||||||
|
"set-home-page" { Do-SetHomePage $opValue }
|
||||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,177 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import uuid as _uuid
|
||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
|
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||||
|
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _sg_root_uuid(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str) and child.get("uuid"):
|
||||||
|
return child.get("uuid")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_get_edit_mode(cfg_dir):
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||||
|
if not pj:
|
||||||
|
return "deny"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src:
|
||||||
|
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
if db.get("editingAllowedCheck"):
|
||||||
|
return db["editingAllowedCheck"]
|
||||||
|
if proj.get("editingAllowedCheck"):
|
||||||
|
return proj["editingAllowedCheck"]
|
||||||
|
return "deny"
|
||||||
|
except Exception:
|
||||||
|
return "deny"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_edit_allowed(target_path, require):
|
||||||
|
try:
|
||||||
|
rp = os.path.abspath(target_path)
|
||||||
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
|
cfg_dir = None
|
||||||
|
bin_path = None
|
||||||
|
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||||
|
for _ in range(12):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
if not elem_uuid:
|
||||||
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
|
if not cfg_dir:
|
||||||
|
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||||
|
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||||
|
cfg_dir = d
|
||||||
|
bin_path = cand
|
||||||
|
if elem_uuid and cfg_dir:
|
||||||
|
break
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
if not elem_uuid and cfg_dir:
|
||||||
|
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||||
|
if not bin_path or not os.path.exists(bin_path):
|
||||||
|
return
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return
|
||||||
|
best = None
|
||||||
|
if elem_uuid:
|
||||||
|
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||||
|
f1 = int(m.group(1))
|
||||||
|
if best is None or f1 < best:
|
||||||
|
best = f1
|
||||||
|
blocked = False
|
||||||
|
code = ""
|
||||||
|
reason = ""
|
||||||
|
if g == 1:
|
||||||
|
blocked = True
|
||||||
|
code = "capability-off"
|
||||||
|
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||||
|
elif require == "removed":
|
||||||
|
if best is not None and best != 2:
|
||||||
|
blocked = True
|
||||||
|
code = "not-removed"
|
||||||
|
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||||
|
else:
|
||||||
|
if best is not None and best == 0:
|
||||||
|
blocked = True
|
||||||
|
code = "locked"
|
||||||
|
reason = "объект на замке — редактирование сломает обновления"
|
||||||
|
if not blocked:
|
||||||
|
return
|
||||||
|
mode = _sg_get_edit_mode(cfg_dir)
|
||||||
|
if mode == "off":
|
||||||
|
return
|
||||||
|
if mode == "warn":
|
||||||
|
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||||
|
return
|
||||||
|
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if code == "capability-off":
|
||||||
|
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||||
|
fix = (
|
||||||
|
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||||
|
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||||
|
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||||
|
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
)
|
||||||
|
elif code == "not-removed":
|
||||||
|
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
fix = (
|
||||||
|
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
fix = (
|
||||||
|
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||||
|
)
|
||||||
|
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
@@ -20,7 +182,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -32,6 +194,22 @@ TYPE_ORDER = [
|
|||||||
"BusinessProcess", "Task", "IntegrationService",
|
"BusinessProcess", "Task", "IntegrationService",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Type → on-disk directory name (plural)
|
||||||
|
TYPE_TO_DIR = {
|
||||||
|
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||||
|
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||||
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||||
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
|
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||||
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||||
|
"Constant": "Constants", "CommonForm": "CommonForms", "Catalog": "Catalogs", "Document": "Documents",
|
||||||
|
"DocumentNumerator": "DocumentNumerators", "Sequence": "Sequences", "DocumentJournal": "DocumentJournals", "Enum": "Enums",
|
||||||
|
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||||||
|
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
|
||||||
|
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
|
||||||
|
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
|
||||||
|
}
|
||||||
|
|
||||||
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
||||||
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
||||||
REF_PROPS = ["DefaultLanguage"]
|
REF_PROPS = ["DefaultLanguage"]
|
||||||
@@ -143,9 +321,9 @@ 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")
|
||||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", required=True)
|
parser.add_argument("-ConfigPath", "-Path", required=True)
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
parser.add_argument("-DefinitionFile", default=None)
|
||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -171,6 +349,9 @@ def main():
|
|||||||
print(f"File not found: {config_path}", file=sys.stderr)
|
print(f"File not found: {config_path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
resolved_path = os.path.abspath(config_path)
|
resolved_path = os.path.abspath(config_path)
|
||||||
|
config_dir = os.path.dirname(resolved_path)
|
||||||
|
|
||||||
|
assert_edit_allowed(resolved_path, "editable")
|
||||||
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
tree = etree.parse(resolved_path, xml_parser)
|
||||||
@@ -285,6 +466,25 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
type_idx = TYPE_ORDER.index(type_name)
|
type_idx = TYPE_ORDER.index(type_name)
|
||||||
|
|
||||||
|
# Check that the referenced object actually exists on disk.
|
||||||
|
# cf-edit add-childObject is a low-level operation for rare scenarios
|
||||||
|
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
||||||
|
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
||||||
|
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
||||||
|
# unnecessary and error-prone.
|
||||||
|
type_dir = TYPE_TO_DIR.get(type_name)
|
||||||
|
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
|
||||||
|
if not os.path.exists(obj_file):
|
||||||
|
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
|
||||||
|
print(
|
||||||
|
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
|
||||||
|
f"cf-edit add-childObject only references objects that already exist on disk.\n"
|
||||||
|
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
|
||||||
|
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Dedup
|
# Dedup
|
||||||
exists = False
|
exists = False
|
||||||
for child in child_objs_el:
|
for child in child_objs_el:
|
||||||
@@ -457,6 +657,270 @@ def main():
|
|||||||
modify_count += 1
|
modify_count += 1
|
||||||
info(f"Set DefaultRoles: {len(items)} roles")
|
info(f"Set DefaultRoles: {len(items)} roles")
|
||||||
|
|
||||||
|
# --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) ---
|
||||||
|
# Canonical English aliases — preferred form, used in docs and error messages.
|
||||||
|
PANEL_UUIDS = {
|
||||||
|
"sections": "b553047f-c9aa-4157-978d-448ecad24248",
|
||||||
|
"open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75",
|
||||||
|
"favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3",
|
||||||
|
"history": "c933ac92-92cd-459d-81cc-e0c8a83ced99",
|
||||||
|
"functions": "b2735bd3-d822-4430-ba59-c9e869693b24",
|
||||||
|
}
|
||||||
|
# Russian synonyms — silently accepted (cf-info displays Russian names;
|
||||||
|
# users may copy them straight into cf-edit value).
|
||||||
|
PANEL_SYNONYMS = {
|
||||||
|
"разделов": "sections", "разделы": "sections",
|
||||||
|
"открытых": "open", "открытые": "open",
|
||||||
|
"избранного": "favorites","избранное": "favorites",
|
||||||
|
"истории": "history", "история": "history",
|
||||||
|
"функций": "functions", "функции": "functions",
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_panel_entry_xml(entry, indent):
|
||||||
|
if isinstance(entry, str):
|
||||||
|
key = entry.lower()
|
||||||
|
key = PANEL_SYNONYMS.get(key, key)
|
||||||
|
if key not in PANEL_UUIDS:
|
||||||
|
allowed = ", ".join(sorted(PANEL_UUIDS.keys()))
|
||||||
|
print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
inst = str(_uuid.uuid4())
|
||||||
|
return f'{indent}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
|
||||||
|
if isinstance(entry, dict) and "group" in entry:
|
||||||
|
children = entry["group"]
|
||||||
|
if not children:
|
||||||
|
print("group must contain at least one entry", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
gid = str(_uuid.uuid4())
|
||||||
|
inner = ""
|
||||||
|
for child in children:
|
||||||
|
child_xml = build_panel_entry_xml(child, indent + "\t\t")
|
||||||
|
inner += f"{indent}\t<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
|
||||||
|
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
|
||||||
|
print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def do_set_panels(value):
|
||||||
|
nonlocal modify_count
|
||||||
|
layout = value
|
||||||
|
if isinstance(layout, str):
|
||||||
|
try:
|
||||||
|
layout = json.loads(layout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not isinstance(layout, dict) or not layout:
|
||||||
|
print("set-panels value must be non-empty object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sides = ("top", "left", "right", "bottom")
|
||||||
|
# Reject unknown side keys
|
||||||
|
for k in layout.keys():
|
||||||
|
if k not in sides:
|
||||||
|
print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
body_parts = []
|
||||||
|
for side in sides:
|
||||||
|
entries = layout.get(side)
|
||||||
|
if entries is None:
|
||||||
|
continue
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
entries = [entries]
|
||||||
|
for entry in entries:
|
||||||
|
entry_xml = build_panel_entry_xml(entry, "\t\t")
|
||||||
|
body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t</{side}>")
|
||||||
|
body = "\r\n".join(body_parts)
|
||||||
|
body_block = body + "\r\n" if body else ""
|
||||||
|
declarations = (
|
||||||
|
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
|
||||||
|
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
|
||||||
|
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
|
||||||
|
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
|
||||||
|
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
|
||||||
|
)
|
||||||
|
cai_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||||
|
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
|
||||||
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||||
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||||||
|
'xsi:type="InterfaceLayouter">\r\n'
|
||||||
|
f'{body_block}{declarations}\r\n'
|
||||||
|
'</ClientApplicationInterface>'
|
||||||
|
)
|
||||||
|
ext_dir = os.path.join(config_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(cai_xml)
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Wrote panel layout: {cai_path}")
|
||||||
|
|
||||||
|
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
||||||
|
RU_TYPE_MAP = {
|
||||||
|
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||||
|
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||||
|
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||||
|
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||||
|
"плансчетов": "ChartOfAccounts",
|
||||||
|
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||||
|
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||||
|
"регистрсведений": "InformationRegister",
|
||||||
|
"регистрнакопления": "AccumulationRegister",
|
||||||
|
"регистрбухгалтерии": "AccountingRegister",
|
||||||
|
"регистррасчета": "CalculationRegister",
|
||||||
|
"регистррасчёта": "CalculationRegister",
|
||||||
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
|
"хранилищенастроек": "SettingsStorage",
|
||||||
|
}
|
||||||
|
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
||||||
|
UUID_RE = __import__("re").compile(r"^[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}$")
|
||||||
|
|
||||||
|
def normalize_form_ref(s):
|
||||||
|
s = (s or "").strip()
|
||||||
|
if not s:
|
||||||
|
return s
|
||||||
|
if UUID_RE.match(s):
|
||||||
|
return s
|
||||||
|
if "/" in s or "\\" in s:
|
||||||
|
parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"]
|
||||||
|
if parts and parts[-1].lower() == "form.xml":
|
||||||
|
parts = parts[:-1]
|
||||||
|
if len(parts) >= 2:
|
||||||
|
type_dir = parts[0]
|
||||||
|
type_singular = DIR_TO_TYPE.get(type_dir.lower())
|
||||||
|
if type_singular:
|
||||||
|
if type_singular == "CommonForm" and len(parts) >= 2:
|
||||||
|
return f"CommonForm.{parts[1]}"
|
||||||
|
if len(parts) >= 4 and parts[2].lower() == "forms":
|
||||||
|
return f"{type_singular}.{parts[1]}.Form.{parts[3]}"
|
||||||
|
return s
|
||||||
|
segs = s.split(".")
|
||||||
|
if segs:
|
||||||
|
head = segs[0].lower()
|
||||||
|
if head in RU_TYPE_MAP:
|
||||||
|
segs[0] = RU_TYPE_MAP[head]
|
||||||
|
for i in range(1, len(segs)):
|
||||||
|
if segs[i] == "Форма":
|
||||||
|
segs[i] = "Form"
|
||||||
|
if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm":
|
||||||
|
segs = [segs[0], segs[1], "Form", segs[2]]
|
||||||
|
return ".".join(segs)
|
||||||
|
|
||||||
|
def get_field(obj, keys):
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(obj, dict) and k in obj:
|
||||||
|
return obj[k]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_home_page_item_xml(entry, indent):
|
||||||
|
if isinstance(entry, str):
|
||||||
|
form_ref = normalize_form_ref(entry)
|
||||||
|
height = 10
|
||||||
|
common = True
|
||||||
|
roles = None
|
||||||
|
elif isinstance(entry, dict):
|
||||||
|
form_raw = get_field(entry, ["form", "Form"])
|
||||||
|
if not form_raw:
|
||||||
|
print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
form_ref = normalize_form_ref(str(form_raw))
|
||||||
|
h = get_field(entry, ["height", "Height"])
|
||||||
|
height = int(h) if h is not None else 10
|
||||||
|
vis = get_field(entry, ["visibility", "Visibility"])
|
||||||
|
common = bool(vis) if vis is not None else True
|
||||||
|
roles = get_field(entry, ["roles"])
|
||||||
|
else:
|
||||||
|
print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
vis_parts = [f"{indent}\t\t<xr:Common>{str(common).lower()}</xr:Common>"]
|
||||||
|
if roles and isinstance(roles, dict):
|
||||||
|
for rname, rval in roles.items():
|
||||||
|
if not rname.startswith("Role.") and not UUID_RE.match(rname):
|
||||||
|
rname = f"Role.{rname}"
|
||||||
|
rval_s = str(bool(rval)).lower()
|
||||||
|
vis_parts.append(f'{indent}\t\t<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
|
||||||
|
vis_block = "\r\n".join(vis_parts)
|
||||||
|
esc_form = html_escape(form_ref, quote=True)
|
||||||
|
return (
|
||||||
|
f"{indent}<Item>\r\n"
|
||||||
|
f"{indent}\t<Form>{esc_form}</Form>\r\n"
|
||||||
|
f"{indent}\t<Height>{height}</Height>\r\n"
|
||||||
|
f"{indent}\t<Visibility>\r\n"
|
||||||
|
f"{vis_block}\r\n"
|
||||||
|
f"{indent}\t</Visibility>\r\n"
|
||||||
|
f"{indent}</Item>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_set_home_page(value):
|
||||||
|
nonlocal modify_count
|
||||||
|
layout = value
|
||||||
|
if isinstance(layout, str):
|
||||||
|
try:
|
||||||
|
layout = json.loads(layout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not isinstance(layout, dict) or not layout:
|
||||||
|
print("set-home-page value must be non-empty object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth")
|
||||||
|
tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth"
|
||||||
|
if tmpl not in allowed_templates:
|
||||||
|
print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
left_items = get_field(layout, ["left", "LeftColumn"])
|
||||||
|
right_items = get_field(layout, ["right", "RightColumn"])
|
||||||
|
|
||||||
|
known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"}
|
||||||
|
for k in layout.keys():
|
||||||
|
if k not in known:
|
||||||
|
print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if tmpl == "OneColumn" and right_items:
|
||||||
|
print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def build_column(tag, items):
|
||||||
|
if not items:
|
||||||
|
return f"\t<{tag}/>"
|
||||||
|
if not isinstance(items, list):
|
||||||
|
items = [items]
|
||||||
|
if not items:
|
||||||
|
return f"\t<{tag}/>"
|
||||||
|
blocks = [build_home_page_item_xml(it, "\t\t") for it in items]
|
||||||
|
body = "\r\n".join(blocks)
|
||||||
|
return f"\t<{tag}>\r\n{body}\r\n\t</{tag}>"
|
||||||
|
|
||||||
|
left_xml = build_column("LeftColumn", left_items)
|
||||||
|
right_xml = build_column("RightColumn", right_items)
|
||||||
|
|
||||||
|
hp_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||||
|
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
||||||
|
'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">\r\n'
|
||||||
|
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
||||||
|
f'{left_xml}\r\n'
|
||||||
|
f'{right_xml}\r\n'
|
||||||
|
'</HomePageWorkArea>'
|
||||||
|
)
|
||||||
|
|
||||||
|
ext_dir = os.path.join(config_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml")
|
||||||
|
with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(hp_xml)
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Wrote home page layout: {hp_path}")
|
||||||
|
|
||||||
# --- Execute operations ---
|
# --- Execute operations ---
|
||||||
operations = []
|
operations = []
|
||||||
if args.DefinitionFile:
|
if args.DefinitionFile:
|
||||||
@@ -477,17 +941,21 @@ def main():
|
|||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "modify-property":
|
if op_name == "modify-property":
|
||||||
do_modify_property(op_value)
|
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-childObject":
|
elif op_name == "add-childObject":
|
||||||
do_add_child_object(op_value)
|
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-childObject":
|
elif op_name == "remove-childObject":
|
||||||
do_remove_child_object(op_value)
|
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-defaultRole":
|
elif op_name == "add-defaultRole":
|
||||||
do_add_default_role(op_value)
|
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-defaultRole":
|
elif op_name == "remove-defaultRole":
|
||||||
do_remove_default_role(op_value)
|
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-defaultRoles":
|
elif op_name == "set-defaultRoles":
|
||||||
do_set_default_roles(op_value)
|
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "set-panels":
|
||||||
|
do_set_panels(op_value)
|
||||||
|
elif op_name == "set-home-page":
|
||||||
|
do_set_home_page(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cf-info
|
name: cf-info
|
||||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||||
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -18,11 +18,12 @@ allowed-tools:
|
|||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||||
|
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
||||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
@@ -37,14 +38,17 @@ powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -Conf
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор пустой конфигурации
|
# Обзор пустой конфигурации
|
||||||
... -ConfigPath upload/cfempty
|
... -ConfigPath src
|
||||||
|
|
||||||
# Краткая сводка реальной конфигурации
|
# Краткая сводка реальной конфигурации
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
... -ConfigPath src -Mode brief
|
||||||
|
|
||||||
# Полная информация
|
# Полная информация
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full
|
... -ConfigPath src -Mode full
|
||||||
|
|
||||||
# С пагинацией
|
# С пагинацией
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
||||||
|
|
||||||
|
# Drill-down: только начальная страница (раскладка форм с ролями)
|
||||||
|
... -ConfigPath src -Section home-page
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
[ValidateSet("overview","brief","full")]
|
[ValidateSet("overview","brief","full")]
|
||||||
[string]$Mode = "overview",
|
[string]$Mode = "overview",
|
||||||
|
[Alias('Name')]
|
||||||
|
[ValidateSet("home-page")]
|
||||||
|
[string]$Section,
|
||||||
[int]$Limit = 150,
|
[int]$Limit = 150,
|
||||||
[int]$Offset = 0,
|
[int]$Offset = 0,
|
||||||
[string]$OutFile
|
[string]$OutFile
|
||||||
@@ -86,7 +89,7 @@ function Get-PropML([string]$propName) {
|
|||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -102,6 +105,7 @@ $typeRuNames = @{
|
|||||||
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
||||||
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
||||||
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
||||||
|
"Bot"="Боты"
|
||||||
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
||||||
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
||||||
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
||||||
@@ -118,6 +122,187 @@ $typeRuNames = @{
|
|||||||
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||||
|
$script:panelNames = @{
|
||||||
|
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых"
|
||||||
|
"b553047f-c9aa-4157-978d-448ecad24248" = "Разделов"
|
||||||
|
"13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного"
|
||||||
|
"c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История"
|
||||||
|
"b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PanelsLayout {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml"
|
||||||
|
if (-not (Test-Path $caiPath)) { return $null }
|
||||||
|
try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null }
|
||||||
|
if (-not $caiDoc.DocumentElement) { return $null }
|
||||||
|
$caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable)
|
||||||
|
$caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core")
|
||||||
|
$layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() }
|
||||||
|
foreach ($side in @("top","left","right","bottom")) {
|
||||||
|
foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) {
|
||||||
|
$slot = @()
|
||||||
|
foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) {
|
||||||
|
$key = $u.InnerText.Trim()
|
||||||
|
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
||||||
|
$slot += $nm
|
||||||
|
}
|
||||||
|
if ($slot.Count -gt 0) { $layout[$side] += ,$slot }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) {
|
||||||
|
$key = $pd.GetAttribute("id")
|
||||||
|
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
||||||
|
$layout.declared += $nm
|
||||||
|
}
|
||||||
|
return $layout
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-LayoutSlots($slots) {
|
||||||
|
# slots is array of arrays (each inner array = one side-tag's panels, may be 1+)
|
||||||
|
# Single inner array, single panel -> just name
|
||||||
|
# Single inner array, multiple panels -> "Стек(a, b)"
|
||||||
|
# Multiple inner arrays -> separate entries joined by " | "
|
||||||
|
if (-not $slots -or $slots.Count -eq 0) { return "" }
|
||||||
|
$parts = @()
|
||||||
|
foreach ($slot in $slots) {
|
||||||
|
if ($slot.Count -eq 1) { $parts += $slot[0] }
|
||||||
|
else { $parts += ("Стек(" + ($slot -join ", ") + ")") }
|
||||||
|
}
|
||||||
|
return ($parts -join " | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:panelLayout = Get-PanelsLayout
|
||||||
|
|
||||||
|
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
||||||
|
function Get-HomePageLayout {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
||||||
|
if (-not (Test-Path $hpPath)) { return $null }
|
||||||
|
try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null }
|
||||||
|
if (-not $hpDoc.DocumentElement) { return $null }
|
||||||
|
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
||||||
|
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||||
|
$hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||||
|
$result = [ordered]@{ template = ""; left = @(); right = @() }
|
||||||
|
$tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs)
|
||||||
|
if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() }
|
||||||
|
foreach ($colName in @("LeftColumn","RightColumn")) {
|
||||||
|
$colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs)
|
||||||
|
if (-not $colNode) { continue }
|
||||||
|
$items = @()
|
||||||
|
foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) {
|
||||||
|
$f = $item.SelectSingleNode("hp:Form", $hpNs)
|
||||||
|
$h = $item.SelectSingleNode("hp:Height", $hpNs)
|
||||||
|
$visNode = $item.SelectSingleNode("hp:Visibility", $hpNs)
|
||||||
|
$common = $true
|
||||||
|
$roles = @()
|
||||||
|
if ($visNode) {
|
||||||
|
$cn = $visNode.SelectSingleNode("xr:Common", $hpNs)
|
||||||
|
if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") }
|
||||||
|
foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) {
|
||||||
|
$roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$items += [ordered]@{
|
||||||
|
form = if ($f) { $f.InnerText.Trim() } else { "" }
|
||||||
|
height = if ($h) { [int]$h.InnerText.Trim() } else { 10 }
|
||||||
|
common = $common
|
||||||
|
roles = $roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:homePage = Get-HomePageLayout
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
|
||||||
|
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
|
||||||
|
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
|
||||||
|
# vendor block separately); only computed when G=0.
|
||||||
|
function Read-SupportState([string]$binPath) {
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
|
||||||
|
$startIdx = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
|
||||||
|
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $h.Success) { return $null }
|
||||||
|
$G = [int]$h.Groups[1].Value
|
||||||
|
$K = [int]$h.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return @{ State = 'removed' } }
|
||||||
|
# Vendor descriptors: ...,"ver","vendor","name",count,
|
||||||
|
$vendors = @()
|
||||||
|
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
|
||||||
|
foreach ($m in $vRe.Matches($text)) {
|
||||||
|
$vendors += @{
|
||||||
|
Version = ($m.Groups[1].Value -replace '""','"')
|
||||||
|
Vendor = ($m.Groups[2].Value -replace '""','"')
|
||||||
|
Name = ($m.Groups[3].Value -replace '""','"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
|
||||||
|
# whole config is read-only and stored f1 values are the inactive default.
|
||||||
|
$counts = $null
|
||||||
|
if ($G -eq 0) {
|
||||||
|
$counts = @(0, 0, 0)
|
||||||
|
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
|
||||||
|
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||||
|
foreach ($m in $rRe.Matches($text)) {
|
||||||
|
$counts[[int]$m.Groups[1].Value]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
|
||||||
|
} catch { return $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SupportLines {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
|
||||||
|
$st = Read-SupportState $binPath
|
||||||
|
$out = @()
|
||||||
|
if (-not $st -or $st.State -eq 'absent') {
|
||||||
|
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
|
||||||
|
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
if ($st.State -eq 'removed') {
|
||||||
|
$out += "Поддержка: снята с поддержки полностью"
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
$out += "Поддержка: на поддержке"
|
||||||
|
if ($st.G -eq 0) {
|
||||||
|
$out += " Возможность изменения: включена"
|
||||||
|
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
|
||||||
|
} else {
|
||||||
|
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
|
||||||
|
}
|
||||||
|
$out += " Конфигураций поставщика: $($st.K)"
|
||||||
|
if ($st.K -gt 1) {
|
||||||
|
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" }
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-HomePageItem($it, [bool]$detailed) {
|
||||||
|
$badges = @()
|
||||||
|
$badges += "h=$($it.height)"
|
||||||
|
if (-not $it.common) { $badges += "скрыта" }
|
||||||
|
if ($it.roles.Count -gt 0) {
|
||||||
|
if ($detailed) { $badges += "роли: $($it.roles.Count)" }
|
||||||
|
else { $badges += "+$($it.roles.Count) ролей" }
|
||||||
|
}
|
||||||
|
$tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" }
|
||||||
|
return " $($it.form)$tail"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
$objectCounts = [ordered]@{}
|
$objectCounts = [ordered]@{}
|
||||||
$totalObjects = 0
|
$totalObjects = 0
|
||||||
@@ -141,6 +326,7 @@ $cfgVersion = Get-PropText "Version"
|
|||||||
$cfgVendor = Get-PropText "Vendor"
|
$cfgVendor = Get-PropText "Vendor"
|
||||||
$cfgCompat = Get-PropText "CompatibilityMode"
|
$cfgCompat = Get-PropText "CompatibilityMode"
|
||||||
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
||||||
|
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
|
||||||
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
||||||
$cfgScript = Get-PropText "ScriptVariant"
|
$cfgScript = Get-PropText "ScriptVariant"
|
||||||
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
||||||
@@ -154,7 +340,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
|
|||||||
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if ($Mode -eq "brief") {
|
if ($Mode -eq "brief" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
||||||
@@ -162,7 +348,7 @@ if ($Mode -eq "brief") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if ($Mode -eq "overview") {
|
if ($Mode -eq "overview" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -172,6 +358,7 @@ if ($Mode -eq "overview") {
|
|||||||
Out "Формат: $version"
|
Out "Формат: $version"
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
Out "Совместимость: $cfgCompat"
|
Out "Совместимость: $cfgCompat"
|
||||||
Out "Режим запуска: $cfgDefaultRun"
|
Out "Режим запуска: $cfgDefaultRun"
|
||||||
Out "Язык скриптов: $cfgScript"
|
Out "Язык скриптов: $cfgScript"
|
||||||
@@ -181,6 +368,33 @@ if ($Mode -eq "overview") {
|
|||||||
Out "Интерфейс: $cfgIntfCompat"
|
Out "Интерфейс: $cfgIntfCompat"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
|
# Panel layout (if file exists)
|
||||||
|
if ($script:panelLayout) {
|
||||||
|
$hasPlaced = $false
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break }
|
||||||
|
}
|
||||||
|
if ($hasPlaced) {
|
||||||
|
Out "--- Раскладка панелей ---"
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
if ($script:panelLayout[$s].Count -gt 0) {
|
||||||
|
Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Home page layout (brief summary)
|
||||||
|
if ($script:homePage) {
|
||||||
|
$ln = $script:homePage.left.Count
|
||||||
|
$rn = $script:homePage.right.Count
|
||||||
|
Out "--- Начальная страница ---"
|
||||||
|
Out " Шаблон: $($script:homePage.template)"
|
||||||
|
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
Out "--- Состав ($totalObjects объектов) ---"
|
Out "--- Состав ($totalObjects объектов) ---"
|
||||||
Out ""
|
Out ""
|
||||||
@@ -203,8 +417,34 @@ if ($Mode -eq "overview") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Drill-down: -Section home-page ---
|
||||||
|
if ($Section -eq "home-page") {
|
||||||
|
if (-not $script:homePage) {
|
||||||
|
Out "Файл Ext/HomePageWorkArea.xml не найден"
|
||||||
|
} else {
|
||||||
|
Out "=== Начальная страница: $cfgName ==="
|
||||||
|
Out ""
|
||||||
|
Out "Шаблон: $($script:homePage.template)"
|
||||||
|
Out ""
|
||||||
|
foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) {
|
||||||
|
$items = $script:homePage[$side[1]]
|
||||||
|
$lbl = $side[0]
|
||||||
|
if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue }
|
||||||
|
Out "${lbl} ($($items.Count)):"
|
||||||
|
foreach ($it in $items) {
|
||||||
|
Out (Format-HomePageItem $it $true)
|
||||||
|
foreach ($r in $it.roles) {
|
||||||
|
$rval = if ($r.value) { "true" } else { "false" }
|
||||||
|
Out " $($r.name): $rval"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
if ($Mode -eq "full") {
|
if ($Mode -eq "full" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -221,6 +461,7 @@ if ($Mode -eq "full") {
|
|||||||
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
||||||
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
||||||
Out ""
|
Out ""
|
||||||
@@ -275,6 +516,33 @@ if ($Mode -eq "full") {
|
|||||||
Out "Обычн.формы в управл.: $useOF"
|
Out "Обычн.формы в управл.: $useOF"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
|
# --- Section: Panel layout ---
|
||||||
|
if ($script:panelLayout) {
|
||||||
|
Out "--- Раскладка панелей ---"
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
$slots = $script:panelLayout[$s]
|
||||||
|
if ($slots.Count -gt 0) {
|
||||||
|
Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)"
|
||||||
|
} else {
|
||||||
|
Out " $($s.PadRight(7)) —"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($script:panelLayout.declared.Count -gt 0) {
|
||||||
|
Out " объявлено: $($script:panelLayout.declared -join ', ')"
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Section: Home page (brief summary) ---
|
||||||
|
if ($script:homePage) {
|
||||||
|
$ln = $script:homePage.left.Count
|
||||||
|
$rn = $script:homePage.right.Count
|
||||||
|
Out "--- Начальная страница ---"
|
||||||
|
Out " Шаблон: $($script:homePage.template)"
|
||||||
|
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
Out "--- Хранилища и формы по умолчанию ---"
|
Out "--- Хранилища и формы по умолчанию ---"
|
||||||
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
# cf-info v1.4 — 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
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -13,8 +14,9 @@ sys.stderr.reconfigure(encoding="utf-8")
|
|||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||||
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
||||||
|
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
|
||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
@@ -93,7 +95,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -109,6 +111,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
@@ -125,6 +128,173 @@ type_ru_names = {
|
|||||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||||
|
PANEL_NAMES = {
|
||||||
|
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
|
||||||
|
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
|
||||||
|
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
|
||||||
|
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
|
||||||
|
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
|
||||||
|
}
|
||||||
|
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
|
||||||
|
|
||||||
|
def get_panels_layout():
|
||||||
|
cfg_dir = os.path.dirname(config_path)
|
||||||
|
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
|
||||||
|
if not os.path.isfile(cai_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
cai_tree = etree.parse(cai_path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
cai_root = cai_tree.getroot()
|
||||||
|
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
|
||||||
|
for side in ("top", "left", "right", "bottom"):
|
||||||
|
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
|
||||||
|
slot = []
|
||||||
|
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
|
||||||
|
key = (u.text or "").strip()
|
||||||
|
slot.append(PANEL_NAMES.get(key, f"?{key}"))
|
||||||
|
if slot:
|
||||||
|
layout[side].append(slot)
|
||||||
|
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
|
||||||
|
key = pd.get("id", "")
|
||||||
|
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def format_layout_slots(slots):
|
||||||
|
if not slots:
|
||||||
|
return ""
|
||||||
|
parts = []
|
||||||
|
for slot in slots:
|
||||||
|
if len(slot) == 1:
|
||||||
|
parts.append(slot[0])
|
||||||
|
else:
|
||||||
|
parts.append("Стек(" + ", ".join(slot) + ")")
|
||||||
|
return " | ".join(parts)
|
||||||
|
|
||||||
|
panel_layout = get_panels_layout()
|
||||||
|
|
||||||
|
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
||||||
|
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
||||||
|
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
|
|
||||||
|
def get_home_page_layout():
|
||||||
|
cfg_dir = os.path.dirname(config_path)
|
||||||
|
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
|
||||||
|
if not os.path.isfile(hp_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
hp_tree = etree.parse(hp_path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
hp_root = hp_tree.getroot()
|
||||||
|
result = {"template": "", "left": [], "right": []}
|
||||||
|
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
|
||||||
|
if tn is not None and tn.text:
|
||||||
|
result["template"] = tn.text.strip()
|
||||||
|
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||||
|
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
|
||||||
|
if col is None:
|
||||||
|
continue
|
||||||
|
items = []
|
||||||
|
for it in col.findall(f"{{{HP_NS}}}Item"):
|
||||||
|
f = it.find(f"{{{HP_NS}}}Form")
|
||||||
|
h = it.find(f"{{{HP_NS}}}Height")
|
||||||
|
vis = it.find(f"{{{HP_NS}}}Visibility")
|
||||||
|
common = True
|
||||||
|
roles = []
|
||||||
|
if vis is not None:
|
||||||
|
cn = vis.find(f"{{{XR_NS_HP}}}Common")
|
||||||
|
if cn is not None and cn.text:
|
||||||
|
common = cn.text.strip() == "true"
|
||||||
|
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
|
||||||
|
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
|
||||||
|
items.append({
|
||||||
|
"form": (f.text or "").strip() if f is not None else "",
|
||||||
|
"height": int((h.text or "10").strip()) if h is not None else 10,
|
||||||
|
"common": common,
|
||||||
|
"roles": roles,
|
||||||
|
})
|
||||||
|
result[key] = items
|
||||||
|
return result
|
||||||
|
|
||||||
|
home_page = get_home_page_layout()
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
||||||
|
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
||||||
|
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
||||||
|
# vendor block separately); only computed when g==0.
|
||||||
|
def read_support_state(bin_path):
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(bin_path):
|
||||||
|
return {"state": "absent"}
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return {"state": "removed"}
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return None
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return {"state": "removed"}
|
||||||
|
vendors = []
|
||||||
|
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
||||||
|
vendors.append({
|
||||||
|
"version": m.group(1).replace('""', '"'),
|
||||||
|
"vendor": m.group(2).replace('""', '"'),
|
||||||
|
"name": m.group(3).replace('""', '"'),
|
||||||
|
})
|
||||||
|
counts = None
|
||||||
|
if g == 0:
|
||||||
|
counts = [0, 0, 0]
|
||||||
|
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
||||||
|
counts[int(m.group(1))] += 1
|
||||||
|
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_support_lines():
|
||||||
|
config_dir = os.path.dirname(config_path)
|
||||||
|
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
||||||
|
st = read_support_state(bin_path)
|
||||||
|
res = []
|
||||||
|
if not st or st["state"] == "absent":
|
||||||
|
if cfg_ext_purpose:
|
||||||
|
res.append("Поддержка: расширение (CFE), правки свободны")
|
||||||
|
else:
|
||||||
|
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
||||||
|
return res
|
||||||
|
if st["state"] == "removed":
|
||||||
|
res.append("Поддержка: снята с поддержки полностью")
|
||||||
|
return res
|
||||||
|
res.append("Поддержка: на поддержке")
|
||||||
|
if st["g"] == 0:
|
||||||
|
res.append(" Возможность изменения: включена")
|
||||||
|
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
||||||
|
else:
|
||||||
|
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
||||||
|
res.append(f" Конфигураций поставщика: {st['k']}")
|
||||||
|
if st["k"] > 1:
|
||||||
|
for v in st["vendors"]:
|
||||||
|
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def format_home_page_item(it, detailed):
|
||||||
|
badges = [f"h={it['height']}"]
|
||||||
|
if not it["common"]:
|
||||||
|
badges.append("скрыта")
|
||||||
|
if it["roles"]:
|
||||||
|
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
|
||||||
|
tail = f" ({', '.join(badges)})" if badges else ""
|
||||||
|
return f" {it['form']}{tail}"
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
object_counts = OrderedDict()
|
object_counts = OrderedDict()
|
||||||
total_objects = 0
|
total_objects = 0
|
||||||
@@ -146,6 +316,7 @@ cfg_version = get_prop_text("Version")
|
|||||||
cfg_vendor = get_prop_text("Vendor")
|
cfg_vendor = get_prop_text("Vendor")
|
||||||
cfg_compat = get_prop_text("CompatibilityMode")
|
cfg_compat = get_prop_text("CompatibilityMode")
|
||||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||||
|
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||||
cfg_script = get_prop_text("ScriptVariant")
|
cfg_script = get_prop_text("ScriptVariant")
|
||||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||||
@@ -159,14 +330,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
|||||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if args.Mode == "brief":
|
if args.Mode == "brief" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if args.Mode == "overview":
|
if args.Mode == "overview" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -178,6 +349,8 @@ if args.Mode == "overview":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
out(f"Совместимость: {cfg_compat}")
|
out(f"Совместимость: {cfg_compat}")
|
||||||
out(f"Режим запуска: {cfg_default_run}")
|
out(f"Режим запуска: {cfg_default_run}")
|
||||||
out(f"Язык скриптов: {cfg_script}")
|
out(f"Язык скриптов: {cfg_script}")
|
||||||
@@ -187,6 +360,20 @@ if args.Mode == "overview":
|
|||||||
out(f"Интерфейс: {cfg_intf_compat}")
|
out(f"Интерфейс: {cfg_intf_compat}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
|
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
|
||||||
|
out("--- Раскладка панелей ---")
|
||||||
|
for s in ("top", "left", "right", "bottom"):
|
||||||
|
if panel_layout[s]:
|
||||||
|
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
# Home page (brief summary)
|
||||||
|
if home_page:
|
||||||
|
out("--- Начальная страница ---")
|
||||||
|
out(f" Шаблон: {home_page['template']}")
|
||||||
|
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||||
|
out()
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
out(f"--- Состав ({total_objects} объектов) ---")
|
out(f"--- Состав ({total_objects} объектов) ---")
|
||||||
out()
|
out()
|
||||||
@@ -207,7 +394,30 @@ if args.Mode == "overview":
|
|||||||
out(f" {padded} {count}")
|
out(f" {padded} {count}")
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
if args.Mode == "full":
|
# --- Drill-down: -Section home-page ---
|
||||||
|
if args.Section == "home-page":
|
||||||
|
if not home_page:
|
||||||
|
out("Файл Ext/HomePageWorkArea.xml не найден")
|
||||||
|
else:
|
||||||
|
out(f"=== Начальная страница: {cfg_name} ===")
|
||||||
|
out()
|
||||||
|
out(f"Шаблон: {home_page['template']}")
|
||||||
|
out()
|
||||||
|
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||||
|
items = home_page[col_key]
|
||||||
|
if not items:
|
||||||
|
out(f"{col_lbl}: —")
|
||||||
|
out()
|
||||||
|
continue
|
||||||
|
out(f"{col_lbl} ({len(items)}):")
|
||||||
|
for it in items:
|
||||||
|
out(format_home_page_item(it, True))
|
||||||
|
for r in it["roles"]:
|
||||||
|
rval = "true" if r["value"] else "false"
|
||||||
|
out(f" {r['name']}: {rval}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
if args.Mode == "full" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -229,6 +439,8 @@ if args.Mode == "full":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||||
if cfg_update_addr:
|
if cfg_update_addr:
|
||||||
out(f"Каталог обн.: {cfg_update_addr}")
|
out(f"Каталог обн.: {cfg_update_addr}")
|
||||||
@@ -283,6 +495,26 @@ if args.Mode == "full":
|
|||||||
out(f"Обычн.формы в управл.: {use_of}")
|
out(f"Обычн.формы в управл.: {use_of}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
|
# --- Section: Panel layout ---
|
||||||
|
if panel_layout:
|
||||||
|
out("--- Раскладка панелей ---")
|
||||||
|
for s in ("top", "left", "right", "bottom"):
|
||||||
|
slots = panel_layout[s]
|
||||||
|
if slots:
|
||||||
|
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
|
||||||
|
else:
|
||||||
|
out(f" {s.ljust(7)} —")
|
||||||
|
if panel_layout["declared"]:
|
||||||
|
out(f" объявлено: {', '.join(panel_layout['declared'])}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
# --- Section: Home page (brief summary) ---
|
||||||
|
if home_page:
|
||||||
|
out("--- Начальная страница ---")
|
||||||
|
out(f" Шаблон: {home_page['template']}")
|
||||||
|
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||||
|
out()
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
out("--- Хранилища и формы по умолчанию ---")
|
out("--- Хранилища и формы по умолчанию ---")
|
||||||
storage_props = [
|
storage_props = [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
|
python "${CLAUDE_SKILL_DIR}/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.1 — Create empty 1C configuration scaffold
|
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -192,6 +192,33 @@ $langXml = @"
|
|||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
$openPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$caiXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
<top>
|
||||||
|
<panel id="$openPanelInst">
|
||||||
|
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
</panel>
|
||||||
|
</top>
|
||||||
|
<left>
|
||||||
|
<panel id="$sectionsPanelInst">
|
||||||
|
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
</panel>
|
||||||
|
</left>
|
||||||
|
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>
|
||||||
|
"@
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
if (-not (Test-Path $OutputDir)) {
|
if (-not (Test-Path $OutputDir)) {
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
@@ -200,6 +227,10 @@ $langDir = Join-Path $OutputDir "Languages"
|
|||||||
if (-not (Test-Path $langDir)) {
|
if (-not (Test-Path $langDir)) {
|
||||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
$extDir = Join-Path $OutputDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -207,9 +238,12 @@ $enc = New-Object System.Text.UTF8Encoding($true)
|
|||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||||
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
Write-Host " Каталог: $OutputDir"
|
Write-Host " Каталог: $OutputDir"
|
||||||
Write-Host " Configuration.xml: $cfgFile"
|
Write-Host " Configuration.xml: $cfgFile"
|
||||||
Write-Host " Languages: $langFile"
|
Write-Host " Languages: $langFile"
|
||||||
|
Write-Host " Ext/CAI: $caiFile"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.1 — Create empty 1C configuration scaffold
|
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||||
# 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, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -184,20 +184,50 @@ def main():
|
|||||||
\t</Language>
|
\t</Language>
|
||||||
</MetaDataObject>'''
|
</MetaDataObject>'''
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
open_panel_inst = new_uuid()
|
||||||
|
sections_panel_inst = new_uuid()
|
||||||
|
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
\t<top>
|
||||||
|
\t\t<panel id="{open_panel_inst}">
|
||||||
|
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</top>
|
||||||
|
\t<left>
|
||||||
|
\t\t<panel id="{sections_panel_inst}">
|
||||||
|
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</left>
|
||||||
|
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>'''
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
lang_dir = os.path.join(output_dir, "Languages")
|
lang_dir = os.path.join(output_dir, "Languages")
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
ext_dir = os.path.join(output_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_utf8_bom(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_utf8_bom(lang_file, lang_xml)
|
||||||
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
write_utf8_bom(cai_file, cai_xml)
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
print(f" Каталог: {output_dir}")
|
print(f" Каталог: {output_dir}")
|
||||||
print(f" Configuration.xml: {cfg_file}")
|
print(f" Configuration.xml: {cfg_file}")
|
||||||
print(f" Languages: {lang_file}")
|
print(f" Languages: {lang_file}")
|
||||||
|
print(f" Ext/CAI: {cai_file}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# cf-validate v1.1 — Validate 1C configuration root structure
|
# cf-validate v1.4 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[Alias('Path')]
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -103,11 +104,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -124,6 +125,7 @@ $childTypeDirMap = @{
|
|||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
|
"Bot"="Bots"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
@@ -145,17 +147,17 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# Valid enum values for Configuration properties
|
# Valid enum values for Configuration 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")
|
"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")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||||
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
||||||
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
||||||
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
||||||
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
|
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
|
||||||
"CompatibilityMode" = @("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")
|
"CompatibilityMode" = @("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")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -203,8 +205,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("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 -ne "2.17" -and $version -ne "2.20") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -535,6 +537,72 @@ if ($childObjNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
||||||
|
function Test-FormRef([string]$ref) {
|
||||||
|
if (-not $ref) { return $true }
|
||||||
|
# UUID — cannot verify without scanning all forms; skip
|
||||||
|
if ($ref -match $guidPattern) { return $true }
|
||||||
|
$parts = $ref.Split(".")
|
||||||
|
if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") {
|
||||||
|
$p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml"
|
||||||
|
$pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml"
|
||||||
|
return (Test-Path $p) -or (Test-Path $pExt)
|
||||||
|
}
|
||||||
|
if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) {
|
||||||
|
$dir = $childTypeDirMap[$parts[0]]
|
||||||
|
$p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml"
|
||||||
|
$pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml"
|
||||||
|
return (Test-Path $p) -or (Test-Path $pExt)
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$formRefsChecked = 0
|
||||||
|
$formRefErrors = @()
|
||||||
|
|
||||||
|
# HomePageWorkArea
|
||||||
|
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
||||||
|
if (Test-Path $hpPath) {
|
||||||
|
try {
|
||||||
|
[xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8
|
||||||
|
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
||||||
|
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||||
|
foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) {
|
||||||
|
$ref = $f.InnerText.Trim()
|
||||||
|
if (-not $ref) { continue }
|
||||||
|
$formRefsChecked++
|
||||||
|
if (-not (Test-FormRef $ref)) {
|
||||||
|
$formRefErrors += "HomePageWorkArea.Form '$ref' — file not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Properties: DefaultXxxForm refs
|
||||||
|
if ($propsNode) {
|
||||||
|
$formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm")
|
||||||
|
foreach ($pn in $formProps) {
|
||||||
|
$node = $propsNode.SelectSingleNode("md:$pn", $ns)
|
||||||
|
if ($node -and $node.InnerText.Trim()) {
|
||||||
|
$ref = $node.InnerText.Trim()
|
||||||
|
$formRefsChecked++
|
||||||
|
if (-not (Test-FormRef $ref)) {
|
||||||
|
$formRefErrors += "Properties.$pn '$ref' — form not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formRefsChecked -eq 0) {
|
||||||
|
Report-OK "9. Form references: none to check"
|
||||||
|
} elseif ($formRefErrors.Count -eq 0) {
|
||||||
|
Report-OK "9. Form references: $formRefsChecked verified"
|
||||||
|
} else {
|
||||||
|
foreach ($err in $formRefErrors) { Report-Error "9. $err" }
|
||||||
|
}
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
& $finalize
|
& $finalize
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.1 — Validate 1C configuration XML structure
|
# cf-validate v1.4 — 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
|
||||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -82,7 +83,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
@@ -90,7 +91,10 @@ VALID_ENUM_VALUES = {
|
|||||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||||
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
'InterfaceCompatibilityMode': [
|
||||||
|
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||||
|
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||||
|
],
|
||||||
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
||||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||||
'CompatibilityMode': [
|
'CompatibilityMode': [
|
||||||
@@ -100,7 +104,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +166,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration XML structure', allow_abbrev=False
|
description='Validate 1C configuration XML structure', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
|
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
|
||||||
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='')
|
||||||
@@ -228,8 +232,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('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.20'):
|
elif version not in ('2.17', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -534,6 +538,60 @@ def main():
|
|||||||
else:
|
else:
|
||||||
pass # no ChildObjects
|
pass # no ChildObjects
|
||||||
|
|
||||||
|
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
||||||
|
def test_form_ref(ref):
|
||||||
|
if not ref:
|
||||||
|
return True
|
||||||
|
if GUID_PATTERN.match(ref):
|
||||||
|
return True
|
||||||
|
parts = ref.split('.')
|
||||||
|
if len(parts) == 2 and parts[0] == 'CommonForm':
|
||||||
|
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
|
||||||
|
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
|
||||||
|
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||||
|
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
|
||||||
|
d = CHILD_TYPE_DIR_MAP[parts[0]]
|
||||||
|
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
|
||||||
|
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
|
||||||
|
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||||
|
return False
|
||||||
|
|
||||||
|
form_refs_checked = 0
|
||||||
|
form_ref_errors = []
|
||||||
|
|
||||||
|
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
|
||||||
|
if os.path.isfile(hp_path):
|
||||||
|
try:
|
||||||
|
hp_tree = etree.parse(hp_path)
|
||||||
|
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
||||||
|
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
|
||||||
|
ref = (f.text or '').strip()
|
||||||
|
if not ref:
|
||||||
|
continue
|
||||||
|
form_refs_checked += 1
|
||||||
|
if not test_form_ref(ref):
|
||||||
|
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
|
||||||
|
except Exception as e:
|
||||||
|
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
|
||||||
|
|
||||||
|
if props_node is not None:
|
||||||
|
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
|
||||||
|
for pn in form_props:
|
||||||
|
node = props_node.find(f'md:{pn}', NS)
|
||||||
|
if node is not None and node.text and node.text.strip():
|
||||||
|
ref = node.text.strip()
|
||||||
|
form_refs_checked += 1
|
||||||
|
if not test_form_ref(ref):
|
||||||
|
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
|
||||||
|
|
||||||
|
if form_refs_checked == 0:
|
||||||
|
r.ok('9. Form references: none to check')
|
||||||
|
elif not form_ref_errors:
|
||||||
|
r.ok(f'9. Form references: {form_refs_checked} verified')
|
||||||
|
else:
|
||||||
|
for err in form_ref_errors:
|
||||||
|
r.error(f'9. {err}')
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.8 — 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,
|
||||||
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
||||||
|
function Strip-FormBindings {
|
||||||
|
param([string]$xml, [bool]$keepObjekt)
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
if ($keepObjekt) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
||||||
|
} else {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tag in $script:formBindingPictureTags) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 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
|
||||||
@@ -316,6 +341,25 @@ function Expand-SelfClosingElement($container, $parentIndent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 7b. Detect format version ---
|
||||||
|
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).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"
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:formatVersion = Detect-FormatVersion $extDir
|
||||||
|
|
||||||
# --- 8. Namespaces declaration for object XML ---
|
# --- 8. Namespaces declaration for object XML ---
|
||||||
$script: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"'
|
$script: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"'
|
||||||
|
|
||||||
@@ -400,6 +444,14 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if ($typeName -eq "DefinedType") {
|
||||||
|
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
||||||
|
if ($typeNode) {
|
||||||
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
@@ -462,11 +514,26 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||||
|
|
||||||
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
# 3. Generate form metadata XML (ФормаЭлемента.xml).
|
||||||
$newFormUuid = [guid]::NewGuid().ToString()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
|
||||||
|
$newFormUuid = ""
|
||||||
|
if (Test-Path $formMetaFileExisting) {
|
||||||
|
try {
|
||||||
|
$existingDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$existingDoc.Load($formMetaFileExisting)
|
||||||
|
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
|
||||||
|
if ($existingFormNode) {
|
||||||
|
$existingUuid = $existingFormNode.GetAttribute("uuid")
|
||||||
|
if ($existingUuid) { $newFormUuid = $existingUuid }
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
|
||||||
$formMetaSb = New-Object System.Text.StringBuilder
|
$formMetaSb = New-Object System.Text.StringBuilder
|
||||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
|
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
|
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
|
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
|
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
|
||||||
@@ -497,8 +564,10 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
$formVersion = $srcFormEl.GetAttribute("version")
|
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
||||||
if (-not $formVersion) { $formVersion = "2.17" }
|
# extension stays uniform — otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
||||||
|
$formVersion = $script:formatVersion
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
@@ -533,13 +602,8 @@ function Borrow-Form {
|
|||||||
$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)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
} else {
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -549,17 +613,9 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
} else {
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
}
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# 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)
|
||||||
@@ -836,14 +892,19 @@ function Borrow-Form {
|
|||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
if (Test-Path $moduleBslFile) {
|
||||||
Info " Created: $moduleBslFile"
|
Info " Preserved existing Module.bsl"
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
|
Info " Created: $moduleBslFile"
|
||||||
|
}
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -991,8 +1052,29 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
foreach ($m in $matches2) {
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
|
foreach ($m in $bms) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
if ($segments.Count -ge 2) {
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardFields -contains $seg1) { continue }
|
||||||
|
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
||||||
|
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
||||||
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1005,21 +1087,11 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
|
||||||
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
|
||||||
foreach ($m in $matches3) {
|
|
||||||
$path = $m.Groups[1].Value
|
|
||||||
$segments = $path.Split(".")
|
|
||||||
$seg0 = $segments[0]
|
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
|
||||||
$firstLevel[$seg0] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1123,7 +1195,8 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
$extraProps = @{}
|
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
||||||
|
$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",
|
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
@@ -1356,28 +1429,45 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
|
$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)
|
||||||
|
$existingChildNames = @{}
|
||||||
|
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) })
|
||||||
|
$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 $srcAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $insertTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Read existing object XML and inject
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
|
$objPropsBlock = ""
|
||||||
|
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
|
if ($objPropsBlock -match "<$pName>") { continue }
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
if ($propsSb.Length -gt 0) {
|
||||||
|
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1435,79 +1525,46 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
$realDeep = @()
|
$deepByAttr = @{}
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
|
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
||||||
}
|
}
|
||||||
|
if ($deepByAttr.Count -gt 0) {
|
||||||
if ($realDeep.Count -gt 0) {
|
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
||||||
Info " Processing $($realDeep.Count) deep path(s)..."
|
|
||||||
|
|
||||||
# Group by ObjectAttr → target catalog
|
|
||||||
$deepByAttr = @{}
|
|
||||||
foreach ($dp in $realDeep) {
|
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
|
||||||
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
# Find the attribute's type to determine target catalog
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
if (-not $attrInfo) { continue }
|
if (-not $attrInfo) { continue }
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$targetTypeName = $catMatch.Groups[1].Value
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
$targetObjName = $catMatch.Groups[2].Value
|
$tsDeepByCol = @{}
|
||||||
|
foreach ($dp in $deepPaths) {
|
||||||
# Ensure target is borrowed
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
if (-not $dp.SubSubAttr) { continue }
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
||||||
if (-not (Test-Path $tTargetDir)) {
|
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
||||||
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
}
|
||||||
}
|
if ($tsDeepByCol.Count -gt 0) {
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
foreach ($k in $tsDeepByCol.Keys) {
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
$parts = $k.Split("|")
|
||||||
$script:borrowedFiles += $tTargetFile
|
$tsName = $parts[0]; $colName = $parts[1]
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
||||||
}
|
if (-not $tsInfo) { continue }
|
||||||
|
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
||||||
# Resolve sub-attributes in target catalog
|
if (-not $colInfo) { continue }
|
||||||
$subNames = @{}
|
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
if (-not $catMatch.Success) { continue }
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
||||||
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
|
||||||
$subTypeXmls = @()
|
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
|
||||||
foreach ($srt in $subRefTypes) {
|
|
||||||
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
|
||||||
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
|
||||||
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
|
||||||
if (-not (Test-Path $sSrcFile)) { continue }
|
|
||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
|
||||||
if (-not (Test-Path $sTargetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
|
||||||
$script:borrowedFiles += $sTargetFile
|
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1515,6 +1572,57 @@ function Borrow-MainAttribute {
|
|||||||
Info " Main attribute borrowing complete"
|
Info " Main attribute borrowing complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
||||||
|
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
||||||
|
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
||||||
|
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
||||||
|
function Borrow-DeepTargetAttrs {
|
||||||
|
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
||||||
|
|
||||||
|
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# Ensure target is borrowed (shell)
|
||||||
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
|
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
||||||
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
|
$script:borrowedFiles += $tTargetFile
|
||||||
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve sub-attributes in target catalog and merge them in
|
||||||
|
$subNames = @{}
|
||||||
|
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
||||||
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
|
# Borrow ref types referenced by the sub-attributes
|
||||||
|
$subTypeXmls = @()
|
||||||
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
|
foreach ($srt in $subRefTypes) {
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
||||||
|
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
||||||
|
if (-not (Test-Path $sSrcFile)) { continue }
|
||||||
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
|
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
||||||
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
|
$script:borrowedFiles += $sTargetFile
|
||||||
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
param(
|
param(
|
||||||
@@ -1529,7 +1637,7 @@ function Build-BorrowedObjectXml {
|
|||||||
|
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
|
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
|
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
|
||||||
|
|
||||||
# InternalInfo
|
# InternalInfo
|
||||||
@@ -1553,6 +1661,11 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | 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)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.8 — 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
|
||||||
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||||
|
|
||||||
|
|
||||||
|
def strip_form_bindings(xml, keep_objekt):
|
||||||
|
"""Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
if keep_objekt:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
||||||
|
else:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
for tag in FORM_BINDING_PICTURE_TAGS:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def decode_numeric_entities(s):
|
||||||
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
|
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||||
|
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
||||||
|
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
||||||
|
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
@@ -254,6 +284,22 @@ XMLNS_DECL = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
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 get_child_indent(container):
|
def get_child_indent(container):
|
||||||
if container.text and "\n" in container.text:
|
if container.text and "\n" in container.text:
|
||||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||||
@@ -365,6 +411,8 @@ def main():
|
|||||||
cfg_resolved = os.path.abspath(cfg_path)
|
cfg_resolved = os.path.abspath(cfg_path)
|
||||||
cfg_dir = os.path.dirname(cfg_resolved)
|
cfg_dir = os.path.dirname(cfg_resolved)
|
||||||
|
|
||||||
|
format_version = detect_format_version(ext_dir)
|
||||||
|
|
||||||
# --- 2. Load extension Configuration.xml ---
|
# --- 2. Load extension Configuration.xml ---
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(ext_resolved, xml_parser)
|
tree = etree.parse(ext_resolved, xml_parser)
|
||||||
@@ -444,6 +492,13 @@ def main():
|
|||||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||||
if prop_node is not None:
|
if prop_node is not None:
|
||||||
src_props[prop_name] = (prop_node.text or "").strip()
|
src_props[prop_name] = (prop_node.text or "").strip()
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if type_name == "DefinedType":
|
||||||
|
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
||||||
|
if type_node is not None:
|
||||||
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
@@ -501,7 +556,7 @@ def main():
|
|||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||||||
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
|
lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
|
||||||
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
|
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
|
||||||
lines.append(internal_info_xml)
|
lines.append(internal_info_xml)
|
||||||
lines.append("\t\t<Properties>")
|
lines.append("\t\t<Properties>")
|
||||||
@@ -515,6 +570,10 @@ def main():
|
|||||||
prop_val = source_props.get(prop_name, "false")
|
prop_val = source_props.get(prop_name, "false")
|
||||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
||||||
|
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
@@ -626,7 +685,26 @@ def main():
|
|||||||
first_level = {}
|
first_level = {}
|
||||||
deep_paths = []
|
deep_paths = []
|
||||||
|
|
||||||
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
|
||||||
|
path = m.group(1)
|
||||||
|
segments = path.split(".")
|
||||||
|
seg0 = segments[0]
|
||||||
|
if seg0 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
first_level[seg0] = True
|
||||||
|
if len(segments) >= 2:
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
@@ -637,22 +715,14 @@ def main():
|
|||||||
seg1 = segments[1]
|
seg1 = segments[1]
|
||||||
if seg1 in STANDARD_FIELDS:
|
if seg1 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
# Also collect from TitleDataPath
|
|
||||||
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
|
||||||
path = m.group(1)
|
|
||||||
segments = path.split(".")
|
|
||||||
seg0 = segments[0]
|
|
||||||
if seg0 in STANDARD_FIELDS:
|
|
||||||
continue
|
|
||||||
first_level[seg0] = True
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
for dp in deep_paths:
|
for dp in deep_paths:
|
||||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_deep.append(dp)
|
unique_deep.append(dp)
|
||||||
@@ -923,26 +993,40 @@ def main():
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
content_parts = []
|
|
||||||
for attr in src_attrs:
|
|
||||||
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
||||||
content_parts.append(attr_xml)
|
|
||||||
for ts in src_ts:
|
|
||||||
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
|
||||||
content_parts.append(ts_xml)
|
|
||||||
adopted_content = "\n".join(content_parts).rstrip()
|
|
||||||
|
|
||||||
# Read existing object XML and inject
|
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
existing_child_names = set()
|
||||||
|
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_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
||||||
|
|
||||||
|
# Generate full object XML with attributes and TS
|
||||||
|
content_parts = []
|
||||||
|
for attr in insert_attrs:
|
||||||
|
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
||||||
|
for ts in insert_ts:
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
if extra_props:
|
if extra_props:
|
||||||
|
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||||
|
obj_props_block = m_props.group(1) if m_props else ""
|
||||||
props_xml = ""
|
props_xml = ""
|
||||||
for p_name, p_val in extra_props.items():
|
for p_name, p_val in extra_props.items():
|
||||||
|
if f"<{p_name}>" in obj_props_block:
|
||||||
|
continue
|
||||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
if props_xml:
|
||||||
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
@@ -994,79 +1078,93 @@ def main():
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if mode == "Form" and deep_paths:
|
if mode == "Form" and deep_paths:
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
deep_by_attr = {}
|
||||||
|
for dp in deep_paths:
|
||||||
if real_deep:
|
if dp["ObjectAttr"] in ts_names:
|
||||||
info(f" Processing {len(real_deep)} deep path(s)...")
|
continue
|
||||||
|
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||||
# Group by ObjectAttr -> target catalog
|
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||||
deep_by_attr = {}
|
|
||||||
for dp in real_deep:
|
|
||||||
if dp["ObjectAttr"] not in deep_by_attr:
|
|
||||||
deep_by_attr[dp["ObjectAttr"]] = []
|
|
||||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||||
|
if deep_by_attr:
|
||||||
|
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
||||||
for attr_name, sub_attr_names in deep_by_attr.items():
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
||||||
# Find the attribute's type to determine target catalog
|
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||||
attr_info = None
|
|
||||||
for a in src_attrs:
|
|
||||||
if a["Name"] == attr_name:
|
|
||||||
attr_info = a
|
|
||||||
break
|
|
||||||
if not attr_info:
|
if not attr_info:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||||
if not cat_match:
|
if not cat_match:
|
||||||
continue
|
continue
|
||||||
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
|
|
||||||
target_type_name = cat_match.group(1)
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
target_obj_name = cat_match.group(2)
|
ts_deep_by_col = {}
|
||||||
|
for dp in deep_paths:
|
||||||
# Ensure target is borrowed
|
if dp["ObjectAttr"] not in ts_names:
|
||||||
if not test_object_borrowed(target_type_name, target_obj_name):
|
continue
|
||||||
t_src = read_source_object(target_type_name, target_obj_name)
|
if not dp.get("SubSubAttr"):
|
||||||
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
continue
|
||||||
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||||
os.makedirs(t_target_dir, exist_ok=True)
|
continue
|
||||||
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||||
save_text_bom(t_target_file, t_borrowed_xml)
|
ts_deep_by_col.setdefault(k, [])
|
||||||
add_to_child_objects(target_type_name, target_obj_name)
|
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||||
borrowed_files.append(t_target_file)
|
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
if ts_deep_by_col:
|
||||||
|
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||||
# Resolve sub-attributes in target catalog
|
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||||
sub_names = {sn: True for sn in sub_attr_names}
|
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||||
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
if not ts_info:
|
||||||
|
continue
|
||||||
if sub_resolved["Attributes"]:
|
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||||
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
if not col_info:
|
||||||
|
continue
|
||||||
# Collect and borrow ref types from deep attributes
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||||
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
if not cat_match:
|
||||||
sub_ref_types = collect_reference_types(sub_type_xmls)
|
continue
|
||||||
for srt in sub_ref_types:
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
||||||
continue
|
|
||||||
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
|
||||||
continue
|
|
||||||
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
|
||||||
if not os.path.isfile(s_src_file):
|
|
||||||
continue
|
|
||||||
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
|
||||||
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
|
||||||
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
|
||||||
os.makedirs(s_target_dir, exist_ok=True)
|
|
||||||
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
|
||||||
save_text_bom(s_target_file, s_borrowed_xml)
|
|
||||||
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
|
||||||
borrowed_files.append(s_target_file)
|
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
|
||||||
|
|
||||||
info(" Main attribute borrowing complete")
|
info(" Main attribute borrowing complete")
|
||||||
|
|
||||||
|
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
||||||
|
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
||||||
|
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
||||||
|
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
||||||
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
||||||
|
t_src = read_source_object(target_type_name, target_obj_name)
|
||||||
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
||||||
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
||||||
|
os.makedirs(t_target_dir, exist_ok=True)
|
||||||
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
||||||
|
save_text_bom(t_target_file, t_borrowed_xml)
|
||||||
|
add_to_child_objects(target_type_name, target_obj_name)
|
||||||
|
borrowed_files.append(t_target_file)
|
||||||
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
|
|
||||||
|
sub_names = {sn: True for sn in sub_attr_names}
|
||||||
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
||||||
|
if sub_resolved["Attributes"]:
|
||||||
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
||||||
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
||||||
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
||||||
|
for srt in sub_ref_types:
|
||||||
|
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
||||||
|
continue
|
||||||
|
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
||||||
|
if not os.path.isfile(s_src_file):
|
||||||
|
continue
|
||||||
|
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
||||||
|
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
||||||
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
||||||
|
os.makedirs(s_target_dir, exist_ok=True)
|
||||||
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
||||||
|
save_text_bom(s_target_file, s_borrowed_xml)
|
||||||
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
||||||
|
borrowed_files.append(s_target_file)
|
||||||
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
|
|
||||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
|
||||||
@@ -1082,11 +1180,25 @@ def main():
|
|||||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
src_form_content = fh.read()
|
src_form_content = fh.read()
|
||||||
|
|
||||||
# 3. Generate form metadata XML
|
# 3. Generate form metadata XML.
|
||||||
new_form_uuid = new_guid()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
||||||
|
new_form_uuid = ""
|
||||||
|
if os.path.isfile(existing_wrapper):
|
||||||
|
try:
|
||||||
|
existing_root = etree.parse(existing_wrapper).getroot()
|
||||||
|
for c in existing_root:
|
||||||
|
if isinstance(c.tag, str) and localname(c) == "Form":
|
||||||
|
new_form_uuid = c.get("uuid", "") or ""
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
new_form_uuid = ""
|
||||||
|
if not new_form_uuid:
|
||||||
|
new_form_uuid = new_guid()
|
||||||
form_meta_lines = [
|
form_meta_lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
|
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
||||||
f'\t<Form uuid="{new_form_uuid}">',
|
f'\t<Form uuid="{new_form_uuid}">',
|
||||||
'\t\t<InternalInfo/>',
|
'\t\t<InternalInfo/>',
|
||||||
'\t\t<Properties>',
|
'\t\t<Properties>',
|
||||||
@@ -1113,7 +1225,10 @@ def main():
|
|||||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||||
src_form_el = src_form_tree.getroot()
|
src_form_el = src_form_tree.getroot()
|
||||||
|
|
||||||
form_version = src_form_el.get("version", "2.17")
|
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
||||||
|
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
||||||
|
form_version = format_version
|
||||||
|
|
||||||
src_auto_cmd = None
|
src_auto_cmd = None
|
||||||
form_props = []
|
form_props = []
|
||||||
@@ -1131,25 +1246,21 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(etree.tostring(fc, encoding="unicode"))
|
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
# 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 = etree.tostring(src_auto_cmd, encoding="unicode")
|
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
||||||
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)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
else:
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1160,20 +1271,12 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
||||||
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 DataPath / TitleDataPath / RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
else:
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1410,12 +1513,16 @@ def main():
|
|||||||
save_text_bom(form_xml_file, "".join(parts))
|
save_text_bom(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
module_dir = os.path.join(form_xml_dir, "Form")
|
module_dir = os.path.join(form_xml_dir, "Form")
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||||
save_text_bom(module_bsl_file, "")
|
if os.path.isfile(module_bsl_file):
|
||||||
info(f" Created: {module_bsl_file}")
|
info(" Preserved existing Module.bsl")
|
||||||
|
else:
|
||||||
|
save_text_bom(module_bsl_file, "")
|
||||||
|
info(f" Created: {module_bsl_file}")
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
register_form_in_object(type_name, obj_name, form_name)
|
register_form_in_object(type_name, obj_name, form_name)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-init.py" -Name "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (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)]
|
||||||
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
$formatVersion = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -75,6 +79,11 @@ if ($ConfigPath) {
|
|||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if ($fmtVer) {
|
||||||
|
$formatVersion = $fmtVer
|
||||||
|
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||||
|
}
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -138,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -203,7 +212,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -220,7 +229,7 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# 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 extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -50,6 +50,10 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
format_version = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -88,6 +92,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
|
fmt_ver = base_cfg_root.get("version")
|
||||||
|
if fmt_ver:
|
||||||
|
format_version = fmt_ver
|
||||||
|
print(f"[INFO] Base config format version: {format_version}")
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -155,7 +163,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -190,7 +198,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -205,7 +213,7 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.py" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src"
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
|
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.4 — Validate 1C configuration extension structure (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)]
|
||||||
|
[Alias('Path')]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -145,10 +146,10 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# 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")
|
"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")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -196,8 +197,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("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 -ne "2.17" -and $version -ne "2.20") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.4 — 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
|
||||||
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
'InterfaceCompatibilityMode': [
|
||||||
|
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||||
|
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
@@ -147,7 +150,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
|
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
|
||||||
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='')
|
||||||
@@ -213,8 +216,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('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.20'):
|
elif version not in ('2.17', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-create
|
name: db-create
|
||||||
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
|
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
|
||||||
argument-hint: <path|name>
|
argument-hint: <path|name>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -25,20 +25,20 @@ allowed-tools:
|
|||||||
## Параметры подключения
|
## Параметры подключения
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -48,31 +48,23 @@ powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После создания
|
## После создания
|
||||||
|
|
||||||
1. Прочитай лог-файл и покажи результат
|
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||||
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
|
||||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-create v1.0 — Create 1C information base
|
# db-create v1.6 — Create 1C information base
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -67,25 +68,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -101,6 +162,31 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
|
||||||
|
if ($UseTemplate) {
|
||||||
|
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
|
||||||
|
$arguments += "--restore=$UseTemplate"
|
||||||
|
} else {
|
||||||
|
$arguments += "--load=$UseTemplate", "--apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.0 — Create 1C information base
|
# db-create v1.6 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def _find_project_v8path():
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
if not v8path:
|
d = os.getcwd()
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
while True:
|
||||||
if found:
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
return found[-1]
|
if os.path.isfile(pf):
|
||||||
else:
|
try:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
sys.exit(1)
|
data = json.load(f)
|
||||||
elif os.path.isdir(v8path):
|
v = data.get("v8path")
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -47,9 +116,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -58,6 +132,29 @@ def main():
|
|||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
|
||||||
|
if args.UseTemplate:
|
||||||
|
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
|
||||||
|
arguments.append(f"--restore={args.UseTemplate}")
|
||||||
|
else:
|
||||||
|
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -67,9 +164,11 @@ def main():
|
|||||||
arguments = ["CREATEINFOBASE"]
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
||||||
|
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
||||||
|
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
||||||
else:
|
else:
|
||||||
arguments.append(f'File="{args.InfoBasePath}"')
|
arguments.append(f'File={args.InfoBasePath}')
|
||||||
|
|
||||||
# --- Template ---
|
# --- Template ---
|
||||||
if args.UseTemplate:
|
if args.UseTemplate:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-cf
|
name: db-dump-cf
|
||||||
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
||||||
argument-hint: "[database] [output.cf]"
|
argument-hint: "[database] [output.cf]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -54,26 +54,15 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
|
||||||
|
|
||||||
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка конфигурации (файловая база)
|
# Выгрузка конфигурации (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -76,25 +77,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -110,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def _find_project_v8path():
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
if not v8path:
|
d = os.getcwd()
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
while True:
|
||||||
if found:
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
return found[-1]
|
if os.path.isfile(pf):
|
||||||
else:
|
try:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
sys.exit(1)
|
data = json.load(f)
|
||||||
elif os.path.isdir(v8path):
|
v = data.get("v8path")
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -49,9 +118,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -60,6 +134,34 @@ def main():
|
|||||||
if out_dir and not os.path.isdir(out_dir):
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: db-dump-dt
|
||||||
|
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
|
||||||
|
argument-hint: "[database] [output.dt]"
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# /db-dump-dt — Выгрузка информационной базы в DT-файл
|
||||||
|
|
||||||
|
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
|
||||||
|
|
||||||
|
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
|
||||||
|
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/db-dump-dt [database] [output.dt]
|
||||||
|
/db-dump-dt dev backup.dt
|
||||||
|
/db-dump-dt — база по умолчанию, имя файла по базе и дате
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параметры подключения
|
||||||
|
|
||||||
|
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||||
|
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||||
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
|
4. Если ветка не совпала — используй `default`
|
||||||
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
|
Если файла нет — предложи `/db-list add`.
|
||||||
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" <параметры>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Параметры скрипта
|
||||||
|
|
||||||
|
| Параметр | Обязательный | Описание |
|
||||||
|
|----------|:------------:|----------|
|
||||||
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
|
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
||||||
|
|
||||||
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Выгрузка ИБ (файловая база)
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
|
# Серверная база
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связанные навыки
|
||||||
|
|
||||||
|
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
|
||||||
|
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
|
||||||
|
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Выгрузка информационной базы 1С в DT-файл
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
|
||||||
|
|
||||||
|
.PARAMETER V8Path
|
||||||
|
Путь к каталогу bin платформы или к 1cv8.exe
|
||||||
|
|
||||||
|
.PARAMETER InfoBasePath
|
||||||
|
Путь к файловой информационной базе
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseServer
|
||||||
|
Сервер 1С (для серверной базы)
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseRef
|
||||||
|
Имя базы на сервере
|
||||||
|
|
||||||
|
.PARAMETER UserName
|
||||||
|
Имя пользователя 1С
|
||||||
|
|
||||||
|
.PARAMETER Password
|
||||||
|
Пароль пользователя
|
||||||
|
|
||||||
|
.PARAMETER OutputFile
|
||||||
|
Путь к выходному DT-файлу
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$V8Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseServer,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseRef,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UserName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Password,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$OutputFile
|
||||||
|
)
|
||||||
|
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $V8Path)) {
|
||||||
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
$outDir = Split-Path $OutputFile -Parent
|
||||||
|
if ($outDir -and -not (Test-Path $outDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
|
# --- Build arguments ---
|
||||||
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||||
|
} else {
|
||||||
|
$arguments += "/F", "`"$InfoBasePath`""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
$arguments += "/DumpIB", "`"$OutputFile`""
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
||||||
|
$arguments += "/Out", "`"$outFile`""
|
||||||
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||||
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $outFile) {
|
||||||
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($logContent) {
|
||||||
|
Write-Host "--- Log ---"
|
||||||
|
Write-Host $logContent
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tempDir) {
|
||||||
|
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C information base to DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-OutputFile", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
out_dir = os.path.dirname(args.OutputFile)
|
||||||
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/DumpIB", args.OutputFile])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-xml
|
name: db-dump-xml
|
||||||
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
||||||
argument-hint: "[database] [outputDir]"
|
argument-hint: "[database] [outputDir]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
@@ -37,14 +37,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -68,30 +68,23 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.p
|
|||||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная выгрузка (файловая база)
|
# Полная выгрузка (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/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
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
||||||
|
|
||||||
# Частичная выгрузка
|
# Частичная выгрузка
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
# db-dump-xml v1.8 — Dump 1C configuration to 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -99,25 +100,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -139,6 +200,44 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "UpdateInfo") {
|
||||||
|
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
} elseif ($Mode -eq "Partial") {
|
||||||
|
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
$arguments = @("infobase", "config", "export", "objects") + $objList
|
||||||
|
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
# db-dump-xml v1.8 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -65,9 +132,14 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -81,6 +153,46 @@ def main():
|
|||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
print(f"Created output directory: {args.ConfigDir}")
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "UpdateInfo":
|
||||||
|
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif args.Mode == "Partial":
|
||||||
|
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||||
|
arguments = ["infobase", "config", "export", "objects"] + obj_list
|
||||||
|
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
|
else:
|
||||||
|
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-list
|
name: db-list
|
||||||
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
|
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
|
||||||
argument-hint: "[add|remove|show]"
|
argument-hint: "[add|remove|show]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-cf
|
name: db-load-cf
|
||||||
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
|
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
|
||||||
argument-hint: <input.cf> [database]
|
argument-hint: <input.cf> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,21 +29,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -55,27 +55,19 @@ powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Прочитай лог-файл и покажи результат
|
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||||
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
# db-load-cf v1.6 — Load 1C configuration from CF file
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -76,25 +77,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -110,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
# db-load-cf v1.6 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def _find_project_v8path():
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
if not v8path:
|
d = os.getcwd()
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
while True:
|
||||||
if found:
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
return found[-1]
|
if os.path.isfile(pf):
|
||||||
else:
|
try:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
sys.exit(1)
|
data = json.load(f)
|
||||||
elif os.path.isdir(v8path):
|
v = data.get("v8path")
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -49,9 +118,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -60,6 +134,34 @@ def main():
|
|||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: db-load-dt
|
||||||
|
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
|
||||||
|
disable-model-invocation: true
|
||||||
|
argument-hint: <input.dt> [database]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# /db-load-dt — Загрузка информационной базы из DT-файла
|
||||||
|
|
||||||
|
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
|
||||||
|
|
||||||
|
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
|
||||||
|
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
|
||||||
|
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
|
||||||
|
|
||||||
|
## Когда НЕ использовать
|
||||||
|
|
||||||
|
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
|
||||||
|
в существующую.
|
||||||
|
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/db-load-dt <input.dt> [database]
|
||||||
|
/db-load-dt backup.dt dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порядок действий перед загрузкой
|
||||||
|
|
||||||
|
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
|
||||||
|
отката (восстановиться будет нечем, если не сохранить).
|
||||||
|
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
|
||||||
|
3. Только после подтверждения выполняй загрузку.
|
||||||
|
|
||||||
|
## Параметры подключения
|
||||||
|
|
||||||
|
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||||
|
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||||
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
|
4. Если ветка не совпала — используй `default`
|
||||||
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
|
Если файла нет — предложи `/db-list add`.
|
||||||
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" <параметры>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Параметры скрипта
|
||||||
|
|
||||||
|
| Параметр | Обязательный | Описание |
|
||||||
|
|----------|:------------:|----------|
|
||||||
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
|
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
||||||
|
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
||||||
|
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
||||||
|
|
||||||
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## После выполнения
|
||||||
|
|
||||||
|
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
|
||||||
|
передать `-UnlockCode`; иначе освободи базу и повтори.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Файловая база
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
|
# Серверная база с ускорением загрузки
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связанные навыки
|
||||||
|
|
||||||
|
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
|
||||||
|
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# db-load-dt v1.5 — Load 1C information base from DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Загрузка информационной базы 1С из DT-файла
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
|
||||||
|
ВНИМАНИЕ: операция полностью перезаписывает базу.
|
||||||
|
|
||||||
|
.PARAMETER V8Path
|
||||||
|
Путь к каталогу bin платформы или к 1cv8.exe
|
||||||
|
|
||||||
|
.PARAMETER InfoBasePath
|
||||||
|
Путь к файловой информационной базе
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseServer
|
||||||
|
Сервер 1С (для серверной базы)
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseRef
|
||||||
|
Имя базы на сервере
|
||||||
|
|
||||||
|
.PARAMETER UserName
|
||||||
|
Имя пользователя 1С
|
||||||
|
|
||||||
|
.PARAMETER Password
|
||||||
|
Пароль пользователя
|
||||||
|
|
||||||
|
.PARAMETER InputFile
|
||||||
|
Путь к DT-файлу для загрузки
|
||||||
|
|
||||||
|
.PARAMETER JobsCount
|
||||||
|
Количество фоновых заданий для загрузки (0 = по числу процессоров)
|
||||||
|
|
||||||
|
.PARAMETER UnlockCode
|
||||||
|
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$V8Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseServer,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseRef,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UserName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Password,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$InputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[int]$JobsCount = 0,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UnlockCode
|
||||||
|
)
|
||||||
|
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $V8Path)) {
|
||||||
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if (-not (Test-Path $InputFile)) {
|
||||||
|
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
|
||||||
|
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
|
# --- Build arguments ---
|
||||||
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||||
|
} else {
|
||||||
|
$arguments += "/F", "`"$InfoBasePath`""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
|
||||||
|
|
||||||
|
$arguments += "/RestoreIB", "`"$InputFile`""
|
||||||
|
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
||||||
|
$arguments += "/Out", "`"$outFile`""
|
||||||
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||||
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $outFile) {
|
||||||
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($logContent) {
|
||||||
|
Write-Host "--- Log ---"
|
||||||
|
Write-Host $logContent
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tempDir) {
|
||||||
|
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-load-dt v1.5 — Load 1C information base from DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Load 1C information base from DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-InputFile", required=True)
|
||||||
|
parser.add_argument("-JobsCount", type=int, default=0)
|
||||||
|
parser.add_argument("-UnlockCode", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if not os.path.isfile(args.InputFile):
|
||||||
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
|
||||||
|
arguments.append("--create-database")
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
if args.UnlockCode:
|
||||||
|
arguments.append(f"/UC{args.UnlockCode}")
|
||||||
|
|
||||||
|
arguments.extend(["/RestoreIB", args.InputFile])
|
||||||
|
if args.JobsCount > 0:
|
||||||
|
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-git
|
name: db-load-git
|
||||||
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
||||||
argument-hint: "[database] [source]"
|
argument-hint: "[database] [source]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -64,15 +64,14 @@ powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.p
|
|||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Показать список загруженных файлов и результат из лога
|
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||||
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Все незафиксированные изменения
|
# Все незафиксированные изменения
|
||||||
powershell.exe -NoProfile -File .claude/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
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
# db-load-git v1.11 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -120,27 +121,86 @@ function Get-ObjectXmlFromSubFile {
|
|||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
# --- Resolve V8Path (skip if DryRun) ---
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -167,6 +227,15 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
|
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
||||||
|
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
||||||
|
function Invoke-GitLines {
|
||||||
|
param([string[]]$GitArgs)
|
||||||
|
$out = git -c core.quotePath=false @GitArgs 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $out }
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -176,29 +245,22 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -216,13 +278,16 @@ Write-Host "Git changes detected: $($changedFiles.Count) files"
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
$configFiles = @()
|
$configFiles = @()
|
||||||
|
$supportSkipped = @()
|
||||||
|
|
||||||
foreach ($file in $changedFiles) {
|
foreach ($file in $changedFiles) {
|
||||||
$file = $file.Trim().Replace('\', '/')
|
$file = $file.Trim().Replace('\', '/')
|
||||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||||
|
|
||||||
# Skip service files
|
# Skip service files (not partially loadable). Support-state files are tracked
|
||||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
# to warn the user: support changes apply only via a full load.
|
||||||
|
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
|
||||||
|
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
|
||||||
|
|
||||||
$fullPath = Join-Path $ConfigDir $file
|
$fullPath = Join-Path $ConfigDir $file
|
||||||
|
|
||||||
@@ -265,6 +330,12 @@ foreach ($file in $changedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($supportSkipped.Count -gt 0) {
|
||||||
|
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
|
||||||
|
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
|
||||||
|
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
if ($configFiles.Count -eq 0) {
|
if ($configFiles.Count -eq 0) {
|
||||||
Write-Host "No configuration files found in changes"
|
Write-Host "No configuration files found in changes"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -285,6 +356,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $configFiles
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
$listFile = Join-Path $tempDir "load_list.txt"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
# db-load-git v1.11 — 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
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
@@ -13,26 +15,90 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -44,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git"] + git_args,
|
["git", "-c", "core.quotePath=false"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -93,9 +159,15 @@ def main():
|
|||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
engine = "1cv8"
|
||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -146,14 +218,19 @@ def main():
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
config_files = []
|
config_files = []
|
||||||
|
support_skipped = []
|
||||||
|
|
||||||
for file in changed_files:
|
for file in changed_files:
|
||||||
file = file.strip().replace("\\", "/")
|
file = file.strip().replace("\\", "/")
|
||||||
if not file:
|
if not file:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip service files
|
# Skip service files (not partially loadable). Support-state files are
|
||||||
if file == "ConfigDumpInfo.xml":
|
# tracked to warn: support changes apply only via a full load.
|
||||||
|
if file.endswith("ParentConfigurations.bin"):
|
||||||
|
support_skipped.append(file)
|
||||||
|
continue
|
||||||
|
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
full_path = os.path.join(args.ConfigDir, file)
|
full_path = os.path.join(args.ConfigDir, file)
|
||||||
@@ -186,6 +263,12 @@ def main():
|
|||||||
if rel_path not in config_files:
|
if rel_path not in config_files:
|
||||||
config_files.append(rel_path)
|
config_files.append(rel_path)
|
||||||
|
|
||||||
|
if support_skipped:
|
||||||
|
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
||||||
|
for sf in support_skipped:
|
||||||
|
print(f" - {sf}", file=sys.stderr)
|
||||||
|
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
||||||
|
|
||||||
if len(config_files) == 0:
|
if len(config_files) == 0:
|
||||||
print("No configuration files found in changes")
|
print("No configuration files found in changes")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -205,6 +288,58 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(apply_args)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-xml
|
name: db-load-xml
|
||||||
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
||||||
argument-hint: <configDir> [database]
|
argument-hint: <configDir> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -80,30 +80,22 @@ Documents/Заказ.xml
|
|||||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Прочитай лог и покажи результат
|
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||||
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная загрузка
|
# Полная загрузка
|
||||||
powershell.exe -NoProfile -File .claude/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
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
powershell.exe -NoProfile -File .claude/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"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
# db-load-xml v1.12 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
Загрузка конфигурации 1С из XML-файлов
|
||||||
@@ -98,32 +99,95 @@ param(
|
|||||||
[string]$Format = "Hierarchical",
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$UpdateDB
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[switch]$StrictLog
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -145,6 +209,73 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
$fileList = @()
|
||||||
|
if ($ListFile) {
|
||||||
|
if (-not (Test-Path $ListFile)) {
|
||||||
|
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
} elseif ($Files) {
|
||||||
|
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
}
|
||||||
|
if ($fileList.Count -eq 0) {
|
||||||
|
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $fileList
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -165,25 +296,36 @@ try {
|
|||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
|
|
||||||
# Build list file
|
# Build list file
|
||||||
$generatedListFile = $null
|
$rawList = @()
|
||||||
if ($ListFile) {
|
if ($ListFile) {
|
||||||
# Use provided list file
|
|
||||||
if (-not (Test-Path $ListFile)) {
|
if (-not (Test-Path $ListFile)) {
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$generatedListFile = $ListFile
|
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
} else {
|
} else {
|
||||||
# Generate from -Files parameter
|
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
||||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
|
||||||
|
|
||||||
Write-Host "Files to load: $($fileList.Count)"
|
|
||||||
foreach ($f in $fileList) { Write-Host " $f" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Support-state service files are NOT partially loadable — exclude with a hint.
|
||||||
|
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
|
||||||
|
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
|
||||||
|
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
|
||||||
|
if ($supportFiles.Count -gt 0) {
|
||||||
|
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
|
||||||
|
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
|
||||||
|
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
if ($fileList.Count -eq 0) {
|
||||||
|
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
||||||
|
Write-Host "Files to load: $($fileList.Count)"
|
||||||
|
foreach ($f in $fileList) { Write-Host " $f" }
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$generatedListFile`""
|
$arguments += "-listFile", "`"$generatedListFile`""
|
||||||
$arguments += "-partial"
|
$arguments += "-partial"
|
||||||
$arguments += "-updateConfigDumpInfo"
|
$arguments += "-updateConfigDumpInfo"
|
||||||
@@ -213,20 +355,58 @@ try {
|
|||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
# --- Read log ---
|
||||||
|
$logContent = $null
|
||||||
|
if (Test-Path $outFile) {
|
||||||
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Scan log for silent rejections ---
|
||||||
|
# Platform often writes load-time rejections into /Out but exits with code 0.
|
||||||
|
# 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
|
||||||
|
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
||||||
|
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if ($logContent) {
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
Write-Host "--- Log ---"
|
||||||
if ($logContent) {
|
Write-Host $logContent
|
||||||
Write-Host "--- Log ---"
|
Write-Host "--- End ---"
|
||||||
Write-Host $logContent
|
}
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
if ($silentFailures.Count -gt 0) {
|
||||||
|
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
||||||
|
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
|
||||||
|
Write-Host $msg -ForegroundColor Yellow
|
||||||
|
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
||||||
|
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|||||||
@@ -1,37 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
# db-load-xml v1.12 — 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
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -63,13 +130,24 @@ def main():
|
|||||||
help="File format (default: Hierarchical)",
|
help="File format (default: Hierarchical)",
|
||||||
)
|
)
|
||||||
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")
|
||||||
|
parser.add_argument(
|
||||||
|
"-StrictLog",
|
||||||
|
action="store_true",
|
||||||
|
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -83,6 +161,77 @@ def main():
|
|||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
if args.ListFile:
|
||||||
|
if not os.path.isfile(args.ListFile):
|
||||||
|
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
|
file_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
|
elif args.Files:
|
||||||
|
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
|
||||||
|
else:
|
||||||
|
file_list = []
|
||||||
|
if not file_list:
|
||||||
|
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + file_list
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading configuration from files (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(apply_args)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -109,23 +258,33 @@ def main():
|
|||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
|
|
||||||
# Build list file
|
# Build list file
|
||||||
generated_list_file = None
|
|
||||||
if args.ListFile:
|
if args.ListFile:
|
||||||
# Use provided list file
|
|
||||||
if not os.path.isfile(args.ListFile):
|
if not os.path.isfile(args.ListFile):
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
generated_list_file = args.ListFile
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
|
raw_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
else:
|
else:
|
||||||
# Generate from -Files parameter
|
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||||
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(file_list))
|
|
||||||
|
|
||||||
print(f"Files to load: {len(file_list)}")
|
# Support-state service files are NOT partially loadable — exclude with a hint.
|
||||||
for fl in file_list:
|
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
|
||||||
print(f" {fl}")
|
support_files = [x for x in raw_list if support_re.search(x)]
|
||||||
|
file_list = [x for x in raw_list if not support_re.search(x)]
|
||||||
|
if support_files:
|
||||||
|
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
||||||
|
for sf in support_files:
|
||||||
|
print(f" - {sf}", file=sys.stderr)
|
||||||
|
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
||||||
|
if not file_list:
|
||||||
|
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
|
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write("\n".join(file_list))
|
||||||
|
print(f"Files to load: {len(file_list)}")
|
||||||
|
for fl in file_list:
|
||||||
|
print(f" {fl}")
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
arguments += ["-listFile", generated_list_file]
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
@@ -157,22 +316,60 @@ def main():
|
|||||||
)
|
)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Read log ---
|
||||||
|
log_content = ""
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
except Exception:
|
||||||
|
log_content = ""
|
||||||
|
|
||||||
|
# --- Scan log for silent rejections ---
|
||||||
|
# Platform often writes load-time rejections into /Out but exits with code 0.
|
||||||
|
# These patterns flag cases where metadata was dropped or rejected silently.
|
||||||
|
fatal_log_patterns = [
|
||||||
|
"Неверное свойство объекта метаданных",
|
||||||
|
"не входит в состав объекта метаданных",
|
||||||
|
"Неизвестное имя типа",
|
||||||
|
"Неизвестный объект метаданных",
|
||||||
|
"Ни один из документов не является регистратором для регистра",
|
||||||
|
"Неверное значение перечисления",
|
||||||
|
"не может быть приведен к типу",
|
||||||
|
]
|
||||||
|
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
|
||||||
|
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
||||||
|
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if log_content:
|
||||||
try:
|
print("--- Log ---")
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
print(log_content)
|
||||||
log_content = f.read()
|
print("--- End ---")
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
if silent_failures:
|
||||||
print(log_content)
|
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
||||||
print("--- End ---")
|
print(
|
||||||
except Exception:
|
f"[warning] log contains {len(silent_failures)} rejection(s) — "
|
||||||
pass
|
f"platform loaded config but dropped properties/refs{suffix}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
for f in silent_failures:
|
||||||
|
print(f" {f}", file=sys.stderr)
|
||||||
|
if args.StrictLog and exit_code == 0:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-run
|
name: db-run
|
||||||
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
|
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
|
||||||
argument-hint: "[database]"
|
argument-hint: "[database]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,14 +29,14 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -63,14 +63,14 @@ powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <пар
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Простой запуск
|
# Простой запуск
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Запуск с обработкой
|
# Запуск с обработкой
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||||
|
|
||||||
# Открыть по навигационной ссылке
|
# Открыть по навигационной ссылке
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||||
|
|
||||||
# Серверная база с параметром запуска
|
# Серверная база с параметром запуска
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-run v1.0 — Launch 1C:Enterprise
|
# db-run v1.2 — Launch 1C:Enterprise
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Запуск 1С:Предприятие
|
Запуск 1С:Предприятие
|
||||||
@@ -79,20 +80,45 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,75 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.0 — Launch 1C:Enterprise
|
# db-run v1.2 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def _find_project_v8path():
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
if not v8path:
|
d = os.getcwd()
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
while True:
|
||||||
if found:
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
return found[-1]
|
if os.path.isfile(pf):
|
||||||
else:
|
try:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
sys.exit(1)
|
data = json.load(f)
|
||||||
elif os.path.isdir(v8path):
|
v = data.get("v8path")
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-update
|
name: db-update
|
||||||
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
|
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
|
||||||
argument-hint: "[database]"
|
argument-hint: "[database]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -66,13 +66,6 @@ powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <
|
|||||||
| `-BackgroundSuspend` | Приостановить |
|
| `-BackgroundSuspend` | Приостановить |
|
||||||
| `-BackgroundResume` | Возобновить |
|
| `-BackgroundResume` | Возобновить |
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## Предупреждения
|
## Предупреждения
|
||||||
|
|
||||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||||
@@ -83,11 +76,11 @@ powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обычное обновление (файловая база)
|
# Обычное обновление (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Динамическое обновление (серверная база)
|
# Динамическое обновление (серверная база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||||
|
|
||||||
# Обновление расширения
|
# Обновление расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-update v1.0 — Update 1C database configuration
|
# db-update v1.6 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Обновление конфигурации базы данных 1С
|
Обновление конфигурации базы данных 1С
|
||||||
@@ -89,25 +90,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -117,6 +178,33 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
|
||||||
|
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.0 — Update 1C database configuration
|
# db-update v1.6 — 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
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def _find_project_v8path():
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
if not v8path:
|
d = os.getcwd()
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
while True:
|
||||||
if found:
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
return found[-1]
|
if os.path.isfile(pf):
|
||||||
else:
|
try:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
sys.exit(1)
|
data = json.load(f)
|
||||||
elif os.path.isdir(v8path):
|
v = data.get("v8path")
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -52,11 +121,48 @@ def main():
|
|||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.Dynamic == "+":
|
||||||
|
arguments.append("--dynamic=auto")
|
||||||
|
elif args.Dynamic == "-":
|
||||||
|
arguments.append("--dynamic=disable")
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: epf-add-form
|
|
||||||
description: Добавить управляемую форму к внешней обработке 1С
|
|
||||||
argument-hint: <ProcessorName> <FormName> [Synonym]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /epf-add-form — Добавление формы
|
|
||||||
|
|
||||||
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|---------------|:------------:|--------------|-------------------------------------------|
|
|
||||||
| ProcessorName | да | — | Имя обработки (должна существовать) |
|
|
||||||
| FormName | да | — | Имя формы |
|
|
||||||
| Synonym | нет | = FormName | Синоним формы |
|
|
||||||
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что создаётся
|
|
||||||
|
|
||||||
```
|
|
||||||
<SrcDir>/<ProcessorName>/Forms/
|
|
||||||
├── <FormName>.xml # Метаданные формы (1 UUID)
|
|
||||||
└── <FormName>/
|
|
||||||
└── Ext/
|
|
||||||
├── Form.xml # Описание формы (logform namespace)
|
|
||||||
└── Form/
|
|
||||||
└── Module.bsl # BSL-модуль с 4 регионами
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что модифицируется
|
|
||||||
|
|
||||||
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
|
|
||||||
|
|
||||||
## Детали
|
|
||||||
|
|
||||||
- FormType: Managed
|
|
||||||
- UsePurposes: PlatformApplication, MobilePlatformApplication
|
|
||||||
- AutoCommandBar с id=-1
|
|
||||||
- Реквизит "Объект" с MainAttribute=true
|
|
||||||
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
# epf-add-form v1.0 — Add managed form to 1C processor
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ProcessorName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$Synonym = $FormName,
|
|
||||||
|
|
||||||
[switch]$Main,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ProcessorName
|
|
||||||
$formsDir = Join-Path $processorDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
|
|
||||||
if (Test-Path $formMetaPath) {
|
|
||||||
Write-Error "Форма уже существует: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Создание каталогов ---
|
|
||||||
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
$formExtDir = Join-Path $formDir "Ext"
|
|
||||||
$formModuleDir = Join-Path $formExtDir "Form"
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$encNoBom = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
|
|
||||||
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
|
|
||||||
|
|
||||||
$formUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$formMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject 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">
|
|
||||||
<Form uuid="$formUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$FormName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<FormType>Managed</FormType>
|
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
<UsePurposes>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
|
||||||
</UsePurposes>
|
|
||||||
<ExtendedPresentation/>
|
|
||||||
</Properties>
|
|
||||||
</Form>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
|
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns: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: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">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="Объект" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
|
||||||
|
|
||||||
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
|
||||||
|
|
||||||
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОбработчикиСобытийФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиКомандФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиОповещений
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
|
||||||
|
|
||||||
# --- 4. Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Form> перед первым <Template>, или в конец
|
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$formElem.InnerText = $FormName
|
|
||||||
|
|
||||||
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
|
||||||
if ($firstTemplate) {
|
|
||||||
# Вставить перед Template, добавив перенос строки + табуляцию
|
|
||||||
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
|
|
||||||
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
|
|
||||||
} else {
|
|
||||||
# Добавить в конец ChildObjects
|
|
||||||
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
|
|
||||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
} else {
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
|
|
||||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
|
||||||
$isFirstForm = ($existingForms.Count -eq 1)
|
|
||||||
|
|
||||||
if ($Main -or $isFirstForm) {
|
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
|
||||||
if ($defaultForm) {
|
|
||||||
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false # Preserve original whitespace
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана форма: $FormName"
|
|
||||||
Write-Host " Метаданные: $formMetaPath"
|
|
||||||
Write-Host " Описание: $formXmlPath"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
if ($Main -or $isFirstForm) {
|
|
||||||
Write-Host " DefaultForm обновлён"
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-form v1.0 — Add managed form to 1C external data processor
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-Main", action="store_true")
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
processor_name = args.ProcessorName
|
|
||||||
form_name = args.FormName
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else form_name
|
|
||||||
is_main = args.Main
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, processor_name)
|
|
||||||
forms_dir = os.path.join(processor_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
|
|
||||||
if os.path.exists(form_meta_path):
|
|
||||||
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
form_ext_dir = os.path.join(form_dir, "Ext")
|
|
||||||
form_module_dir = os.path.join(form_ext_dir, "Form")
|
|
||||||
|
|
||||||
os.makedirs(form_module_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 1. Form metadata (Forms/<FormName>.xml) ---
|
|
||||||
|
|
||||||
form_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
form_meta_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<MetaDataObject 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">\n'
|
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
|
||||||
'\t\t<Properties>\n'
|
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
|
||||||
'\t\t\t<Synonym>\n'
|
|
||||||
'\t\t\t\t<v8:item>\n'
|
|
||||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
|
||||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
|
||||||
'\t\t\t\t</v8:item>\n'
|
|
||||||
'\t\t\t</Synonym>\n'
|
|
||||||
'\t\t\t<Comment/>\n'
|
|
||||||
'\t\t\t<FormType>Managed</FormType>\n'
|
|
||||||
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
|
|
||||||
'\t\t\t<UsePurposes>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t</UsePurposes>\n'
|
|
||||||
'\t\t\t<ExtendedPresentation/>\n'
|
|
||||||
'\t\t</Properties>\n'
|
|
||||||
'\t</Form>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
|
||||||
|
|
||||||
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
|
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
||||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
||||||
' xmlns: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: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">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
|
||||||
|
|
||||||
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
|
||||||
|
|
||||||
module_path = os.path.join(form_module_dir, "Module.bsl")
|
|
||||||
|
|
||||||
module_bsl = (
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
# --- 4. Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Form> before first <Template>, or at end
|
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
|
||||||
if first_template is not None:
|
|
||||||
# Insert before Template, adding newline + indent
|
|
||||||
idx = list(child_objects).index(first_template)
|
|
||||||
child_objects.insert(idx, form_elem)
|
|
||||||
# Set whitespace: form_elem gets same tail pattern
|
|
||||||
form_elem.tail = "\n\t\t\t"
|
|
||||||
else:
|
|
||||||
# Add to end of ChildObjects
|
|
||||||
children = list(child_objects)
|
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
|
||||||
# Empty ChildObjects (self-closing)
|
|
||||||
child_objects.text = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
else:
|
|
||||||
if len(children) > 0:
|
|
||||||
last_child = children[-1]
|
|
||||||
old_tail = last_child.tail
|
|
||||||
last_child.tail = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
|
|
||||||
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
|
|
||||||
existing_forms = child_objects.findall("md:Form", NSMAP)
|
|
||||||
is_first_form = len(existing_forms) == 1
|
|
||||||
|
|
||||||
if is_main or is_first_form:
|
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
|
||||||
if default_form is not None:
|
|
||||||
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Создана форма: {form_name}")
|
|
||||||
print(f" Метаданные: {form_meta_path}")
|
|
||||||
print(f" Описание: {form_xml_path}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
if is_main or is_first_form:
|
|
||||||
print(" DefaultForm обновлён")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-bsp-add-command
|
name: epf-bsp-add-command
|
||||||
description: Добавить команду в дополнительную обработку БСП
|
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
|
||||||
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-bsp-init
|
name: epf-bsp-init
|
||||||
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
|
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
|
||||||
argument-hint: <ProcessorName> <Вид>
|
argument-hint: <ProcessorName> <Вид>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
@@ -203,6 +203,6 @@ allowed-tools:
|
|||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|
||||||
- Добавить ещё команду: `/epf-bsp-add-command`
|
- Добавить ещё команду: `/epf-bsp-add-command`
|
||||||
- Добавить форму: `/epf-add-form`
|
- Добавить форму: `/form-add`
|
||||||
- Добавить макет: `/template-add`
|
- Добавить макет: `/template-add`
|
||||||
- Собрать EPF: `/epf-build`
|
- Собрать EPF: `/epf-build`
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка обработки (файловая база)
|
# Сборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||||
@@ -70,20 +71,79 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +181,27 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
$srcDir = Split-Path $SourceFile -Parent
|
||||||
|
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -51,6 +118,10 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
# --- Auto-create stub database if no connection specified ---
|
||||||
auto_created_base = None
|
auto_created_base = None
|
||||||
@@ -84,6 +155,29 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||||
|
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"External data processor/report built successfully: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — 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)]
|
||||||
@@ -1252,6 +1252,57 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($stubEngine -eq "ibcmd") {
|
||||||
|
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
||||||
|
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
||||||
|
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||||
|
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||||
|
$ibArgs += "--data=$ibData"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
|
||||||
|
$ibOut = $__ib.Output
|
||||||
|
$ibRc = $__ib.ExitCode
|
||||||
|
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ibRc -ne 0) {
|
||||||
|
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
||||||
|
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
Write-Host "[OK] Stub database created: $TempBasePath"
|
||||||
|
Write-Host $TempBasePath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — 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
|
||||||
@@ -12,6 +12,27 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -1034,6 +1055,32 @@ def main():
|
|||||||
if register_columns:
|
if register_columns:
|
||||||
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
||||||
|
|
||||||
|
# Stub via ibcmd (one call: create [--import --apply])
|
||||||
|
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if stub_engine == "ibcmd":
|
||||||
|
import shutil
|
||||||
|
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
||||||
|
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
||||||
|
if has_ref_types:
|
||||||
|
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||||
|
ib_args.append(f'--data={ib_data}')
|
||||||
|
result = run_ibcmd(ib_args, warn_no_user=False)
|
||||||
|
shutil.rmtree(ib_data, ignore_errors=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if has_ref_types:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
||||||
|
print(f'[OK] Stub database created: {temp_base}')
|
||||||
|
print(temp_base)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@@ -33,20 +33,20 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка обработки (файловая база)
|
# Разборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||||
@@ -77,20 +78,45 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +127,46 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if (-not (Test-Path $InputFile)) {
|
if (-not (Test-Path $InputFile)) {
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||||
@@ -117,6 +183,26 @@ $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
||||||
|
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,104 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -57,12 +124,20 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate database connection ---
|
# --- Validate database connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
@@ -78,6 +153,28 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
||||||
|
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-init
|
name: epf-init
|
||||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
|
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
|
||||||
argument-hint: <Name> [Synonym]
|
argument-hint: <Name> [Synonym]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,12 +30,12 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|
||||||
- Добавить форму: `/epf-add-form`
|
- Добавить форму: `/form-add`
|
||||||
- Добавить макет: `/template-add`
|
- Добавить макет: `/template-add`
|
||||||
- Добавить справку: `/help-add`
|
- Добавить справку: `/help-add`
|
||||||
- Собрать EPF: `/epf-build`
|
- Собрать EPF: `/epf-build`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-init v1.0 — Init 1C external data processor scaffold
|
# epf-init v1.1 — Init 1C external data processor scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -10,6 +10,8 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-init v1.0 — Init 1C external data processor scaffold
|
# epf-init v1.1 — Init 1C external data processor scaffold
|
||||||
# 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, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# epf-validate v1.1 — Validate 1C external data processor / report structure
|
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[Alias('Path')]
|
||||||
[string]$ObjectPath,
|
[string]$ObjectPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -184,8 +185,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("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 -ne "2.17" -and $version -ne "2.20") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type: ExternalDataProcessor or ExternalReport
|
# Detect type: ExternalDataProcessor or ExternalReport
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.1 — Validate 1C external data processor / report structure
|
# epf-validate v1.2 — 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
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ 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")
|
||||||
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ObjectPath", required=True)
|
parser.add_argument("-ObjectPath", "-Path", required=True)
|
||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
parser.add_argument("-OutFile", default=None)
|
parser.add_argument("-OutFile", default=None)
|
||||||
@@ -165,8 +165,8 @@ def main():
|
|||||||
version = root.get("version", "")
|
version = root.get("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.20"):
|
elif version not in ("2.17", "2.20", "2.21"):
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
child_elements = []
|
child_elements = []
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -42,7 +42,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-build:
|
Используй общий скрипт из epf-build:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка отчёта (файловая база)
|
# Сборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -41,7 +41,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-dump:
|
Используй общий скрипт из epf-dump:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка отчёта (файловая база)
|
# Разборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: erf-init
|
name: erf-init
|
||||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
|
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
|
||||||
argument-hint: <Name> [Synonym] [--with-skd]
|
argument-hint: <Name> [Synonym] [--with-skd]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# erf-init v1.0 — Init 1C external report scaffold
|
# erf-init v1.1 — Init 1C external report scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -12,6 +12,8 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# erf-init v1.0 — Init 1C external report scaffold
|
# erf-init v1.1 — Init 1C external report scaffold
|
||||||
# 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, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
|
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: form-add
|
name: form-add
|
||||||
description: Добавить управляемую форму к объекту конфигурации 1С
|
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
||||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -32,7 +32,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
python "${CLAUDE_SKILL_DIR}/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Purpose — назначение формы
|
## Purpose — назначение формы
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.2 — Add managed form to 1C config object
|
# form-add v1.8 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -15,6 +15,143 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
|
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||||
|
# throws — guard errors degrade to allow.
|
||||||
|
function Get-RootUuid([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $null }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Get-EditMode([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||||
|
if (-not $pj) { return 'deny' }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||||
|
return 'deny'
|
||||||
|
} catch { return 'deny' }
|
||||||
|
}
|
||||||
|
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||||
|
try {
|
||||||
|
$rp = $targetPath
|
||||||
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
$cfgDir = $null; $binPath = $null
|
||||||
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
|
if (-not $cfgDir) {
|
||||||
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||||
|
}
|
||||||
|
if ($elemUuid -and $cfgDir) { break }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
# New object (no element file): fall back to config root uuid.
|
||||||
|
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||||
|
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return }
|
||||||
|
$start = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||||
|
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $hm.Success) { return }
|
||||||
|
$G = [int]$hm.Groups[1].Value
|
||||||
|
$K = [int]$hm.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return }
|
||||||
|
$best = $null
|
||||||
|
if ($elemUuid) {
|
||||||
|
$u = [regex]::Escape($elemUuid.ToLower())
|
||||||
|
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||||
|
$f1 = [int]$m.Groups[1].Value
|
||||||
|
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$blocked = $false; $code = ""; $reason = ""
|
||||||
|
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||||
|
elseif ($require -eq 'removed') {
|
||||||
|
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||||
|
}
|
||||||
|
if (-not $blocked) { return }
|
||||||
|
$mode = Get-EditMode $cfgDir
|
||||||
|
if ($mode -eq 'off') { return }
|
||||||
|
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||||
|
# latter throws and would be swallowed by this function's own catch.
|
||||||
|
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||||
|
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if ($code -eq "capability-off") {
|
||||||
|
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||||
|
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
} elseif ($code -eq "not-removed") {
|
||||||
|
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||||
|
} else {
|
||||||
|
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||||
|
}
|
||||||
|
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||||
|
exit 1
|
||||||
|
} catch { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect XML format version ---
|
||||||
|
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).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"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Фаза 1: Определение типа объекта ---
|
# --- Фаза 1: Определение типа объекта ---
|
||||||
|
|
||||||
@@ -36,6 +173,9 @@ if (-not (Test-Path $ObjectPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
$objectXmlFull = Resolve-Path $ObjectPath
|
||||||
|
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
||||||
|
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
$xmlDoc.PreserveWhitespace = $true
|
||||||
$xmlDoc.Load($objectXmlFull.Path)
|
$xmlDoc.Load($objectXmlFull.Path)
|
||||||
@@ -54,8 +194,8 @@ if (-not $metaDataObject) {
|
|||||||
$supportedTypes = @(
|
$supportedTypes = @(
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task"
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
||||||
)
|
)
|
||||||
|
|
||||||
$objectType = $null
|
$objectType = $null
|
||||||
@@ -159,7 +299,7 @@ if ($objectType -in $processorLikeTypes) {
|
|||||||
|
|
||||||
$formMetaXml = @"
|
$formMetaXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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">
|
<MetaDataObject 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="$($script:formatVersion)">
|
||||||
<Form uuid="$formUuid">
|
<Form uuid="$formUuid">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$FormName</Name>
|
<Name>$FormName</Name>
|
||||||
@@ -196,13 +336,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="2.17">
|
<Form $formNsDecl version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="Список" id="1">
|
<Attribute name="Список" id="1">
|
||||||
@@ -224,13 +361,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="2.17">
|
<Form $formNsDecl version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="$mainAttrName" id="1">
|
<Attribute name="$mainAttrName" id="1">
|
||||||
@@ -261,27 +395,30 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
"BusinessProcess" = "BusinessProcessObject"
|
"BusinessProcess" = "BusinessProcessObject"
|
||||||
"Task" = "TaskObject"
|
"Task" = "TaskObject"
|
||||||
"InformationRegister" = "InformationRegisterRecordManager"
|
"InformationRegister" = "InformationRegisterRecordManager"
|
||||||
|
"AccumulationRegister" = "AccumulationRegisterRecordSet"
|
||||||
}
|
}
|
||||||
|
|
||||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
||||||
|
|
||||||
|
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
||||||
|
$savedDataLine = ""
|
||||||
|
if ($objectType -notin $processorLikeTypes) {
|
||||||
|
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
|
||||||
|
}
|
||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="2.17">
|
<Form $formNsDecl version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="$mainAttrName" id="1">
|
<Attribute name="$mainAttrName" id="1">
|
||||||
<Type>
|
<Type>
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
<v8:Type>cfg:$mainAttrType</v8:Type>
|
||||||
</Type>
|
</Type>
|
||||||
<MainAttribute>true</MainAttribute>
|
<MainAttribute>true</MainAttribute>$savedDataLine
|
||||||
<SavedData>true</SavedData>
|
|
||||||
</Attribute>
|
</Attribute>
|
||||||
</Attributes>
|
</Attributes>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -301,11 +438,6 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
|
|||||||
$moduleBsl = @"
|
$moduleBsl = @"
|
||||||
#Область ОбработчикиСобытийФормы
|
#Область ОбработчикиСобытийФормы
|
||||||
|
|
||||||
&НаСервере
|
|
||||||
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
|
|
||||||
|
|
||||||
КонецПроцедуры
|
|
||||||
|
|
||||||
#КонецОбласти
|
#КонецОбласти
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
#Область ОбработчикиСобытийЭлементовФормы
|
||||||
|
|||||||
@@ -1,20 +1,198 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.2 — Add managed form to 1C config object
|
# form-add v1.8 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
|
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||||
|
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _sg_root_uuid(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str) and child.get("uuid"):
|
||||||
|
return child.get("uuid")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_get_edit_mode(cfg_dir):
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||||
|
if not pj:
|
||||||
|
return "deny"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src:
|
||||||
|
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
if db.get("editingAllowedCheck"):
|
||||||
|
return db["editingAllowedCheck"]
|
||||||
|
if proj.get("editingAllowedCheck"):
|
||||||
|
return proj["editingAllowedCheck"]
|
||||||
|
return "deny"
|
||||||
|
except Exception:
|
||||||
|
return "deny"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_edit_allowed(target_path, require):
|
||||||
|
try:
|
||||||
|
rp = os.path.abspath(target_path)
|
||||||
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
|
cfg_dir = None
|
||||||
|
bin_path = None
|
||||||
|
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||||
|
for _ in range(12):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
if not elem_uuid:
|
||||||
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
|
if not cfg_dir:
|
||||||
|
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||||
|
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||||
|
cfg_dir = d
|
||||||
|
bin_path = cand
|
||||||
|
if elem_uuid and cfg_dir:
|
||||||
|
break
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
if not elem_uuid and cfg_dir:
|
||||||
|
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||||
|
if not bin_path or not os.path.exists(bin_path):
|
||||||
|
return
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return
|
||||||
|
best = None
|
||||||
|
if elem_uuid:
|
||||||
|
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||||
|
f1 = int(m.group(1))
|
||||||
|
if best is None or f1 < best:
|
||||||
|
best = f1
|
||||||
|
blocked = False
|
||||||
|
code = ""
|
||||||
|
reason = ""
|
||||||
|
if g == 1:
|
||||||
|
blocked = True
|
||||||
|
code = "capability-off"
|
||||||
|
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||||
|
elif require == "removed":
|
||||||
|
if best is not None and best != 2:
|
||||||
|
blocked = True
|
||||||
|
code = "not-removed"
|
||||||
|
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||||
|
else:
|
||||||
|
if best is not None and best == 0:
|
||||||
|
blocked = True
|
||||||
|
code = "locked"
|
||||||
|
reason = "объект на замке — редактирование сломает обновления"
|
||||||
|
if not blocked:
|
||||||
|
return
|
||||||
|
mode = _sg_get_edit_mode(cfg_dir)
|
||||||
|
if mode == "off":
|
||||||
|
return
|
||||||
|
if mode == "warn":
|
||||||
|
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||||
|
return
|
||||||
|
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if code == "capability-off":
|
||||||
|
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||||
|
fix = (
|
||||||
|
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||||
|
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||||
|
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||||
|
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
)
|
||||||
|
elif code == "not-removed":
|
||||||
|
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
fix = (
|
||||||
|
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
fix = (
|
||||||
|
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||||
|
)
|
||||||
|
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
NSMAP = {
|
NSMAP = {
|
||||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
"v8": "http://v8.1c.ru/8.1/data/core",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
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 save_xml_with_bom(tree, path):
|
def save_xml_with_bom(tree, path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Save XML tree to file with UTF-8 BOM."""
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
@@ -67,6 +245,9 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
object_xml_full = os.path.abspath(object_path)
|
||||||
|
assert_edit_allowed(object_xml_full, "editable")
|
||||||
|
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(object_xml_full, parser_xml)
|
tree = etree.parse(object_xml_full, parser_xml)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
@@ -74,8 +255,8 @@ def main():
|
|||||||
supported_types = [
|
supported_types = [
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task",
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
||||||
]
|
]
|
||||||
|
|
||||||
object_type = None
|
object_type = None
|
||||||
@@ -171,7 +352,7 @@ def main():
|
|||||||
' 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"'
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
' version="2.17">\n'
|
f' version="{format_version}">\n'
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
f'\t<Form uuid="{form_uuid}">\n'
|
||||||
'\t\t<Properties>\n'
|
'\t\t<Properties>\n'
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||||
@@ -225,13 +406,10 @@ def main():
|
|||||||
|
|
||||||
form_xml = (
|
form_xml = (
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
f'<Form {form_ns_decl} version="2.17">\n'
|
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
'\t<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
||||||
@@ -254,13 +432,10 @@ def main():
|
|||||||
|
|
||||||
form_xml = (
|
form_xml = (
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
f'<Form {form_ns_decl} version="2.17">\n'
|
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
'\t<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||||
@@ -291,19 +466,22 @@ def main():
|
|||||||
"BusinessProcess": "BusinessProcessObject",
|
"BusinessProcess": "BusinessProcessObject",
|
||||||
"Task": "TaskObject",
|
"Task": "TaskObject",
|
||||||
"InformationRegister": "InformationRegisterRecordManager",
|
"InformationRegister": "InformationRegisterRecordManager",
|
||||||
|
"AccumulationRegister": "AccumulationRegisterRecordSet",
|
||||||
}
|
}
|
||||||
|
|
||||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
||||||
|
|
||||||
|
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
||||||
|
saved_data_line = ''
|
||||||
|
if object_type not in processor_like_types:
|
||||||
|
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
|
||||||
|
|
||||||
form_xml = (
|
form_xml = (
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
f'<Form {form_ns_decl} version="2.17">\n'
|
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
'\t<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||||
@@ -311,7 +489,7 @@ def main():
|
|||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||||
'\t\t\t</Type>\n'
|
'\t\t\t</Type>\n'
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||||
'\t\t\t<SavedData>true</SavedData>\n'
|
f'{saved_data_line}'
|
||||||
'\t\t</Attribute>\n'
|
'\t\t</Attribute>\n'
|
||||||
'\t</Attributes>\n'
|
'\t</Attributes>\n'
|
||||||
'</Form>'
|
'</Form>'
|
||||||
@@ -329,11 +507,6 @@ def main():
|
|||||||
module_bsl = (
|
module_bsl = (
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
'\n'
|
'\n'
|
||||||
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
|
|
||||||
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
|
|
||||||
'\n'
|
|
||||||
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
'\n'
|
'\n'
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: form-compile
|
name: form-compile
|
||||||
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов
|
description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
|
||||||
argument-hint: <JsonPath> <OutputPath>
|
argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -9,29 +9,30 @@ allowed-tools:
|
|||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /form-compile — Генерация Form.xml из JSON DSL
|
# /form-compile — Генерация Form.xml
|
||||||
|
|
||||||
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID.
|
Два режима:
|
||||||
|
1. **JSON DSL** — из JSON-определения формы
|
||||||
|
2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
|
||||||
|
|
||||||
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно.
|
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/form-compile <JsonPath> <OutputPath>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|------------|:------------:|-----------------------------------|
|
|------------|:------------:|---------------------------------|
|
||||||
| JsonPath | да | Путь к JSON-определению формы |
|
| JsonPath | режим 1 | Путь к JSON-определению формы |
|
||||||
| OutputPath | да | Путь к выходному файлу Form.xml |
|
| OutputPath | да | Путь к выходному Form.xml |
|
||||||
|
| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>"
|
# Режим JSON DSL
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
|
||||||
|
|
||||||
|
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
|
||||||
|
python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## JSON DSL — справка
|
## JSON DSL — справка
|
||||||
@@ -60,9 +61,11 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
|
|
||||||
| DSL ключ | XML элемент | Значение ключа |
|
| DSL ключ | XML элемент | Значение ключа |
|
||||||
|--------------|-------------------|---------------------------------------------------|
|
|--------------|-------------------|---------------------------------------------------|
|
||||||
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
|
| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
|
||||||
|
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
|
||||||
| `"input"` | InputField | имя элемента |
|
| `"input"` | InputField | имя элемента |
|
||||||
| `"check"` | CheckBoxField | имя |
|
| `"check"` | CheckBoxField | имя |
|
||||||
|
| `"radio"` | RadioButtonField | имя |
|
||||||
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
||||||
| `"labelField"` | LabelField | имя |
|
| `"labelField"` | LabelField | имя |
|
||||||
| `"table"` | Table | имя |
|
| `"table"` | Table | имя |
|
||||||
@@ -73,21 +76,22 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `"picField"` | PictureField | имя |
|
| `"picField"` | PictureField | имя |
|
||||||
| `"calendar"` | CalendarField | имя |
|
| `"calendar"` | CalendarField | имя |
|
||||||
| `"cmdBar"` | CommandBar | имя |
|
| `"cmdBar"` | CommandBar | имя |
|
||||||
|
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
|
||||||
| `"popup"` | Popup | имя |
|
| `"popup"` | Popup | имя |
|
||||||
|
|
||||||
### Общие свойства (все типы элементов)
|
### Общие свойства (все типы элементов)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа) |
|
| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
|
||||||
| `title` | Заголовок элемента |
|
| `title` | Заголовок элемента |
|
||||||
|
| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
|
||||||
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
||||||
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
||||||
| `readOnly: true` | Только чтение |
|
| `readOnly: true` | Только чтение |
|
||||||
| `on: [...]` | События с автоименованием обработчиков |
|
| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
|
||||||
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` |
|
|
||||||
|
|
||||||
### Допустимые имена событий (`on`)
|
### Допустимые имена событий (`events`)
|
||||||
|
|
||||||
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
||||||
|
|
||||||
@@ -95,7 +99,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
|
|
||||||
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
||||||
|
|
||||||
**check**: `OnChange`
|
**check / radio**: `OnChange`
|
||||||
|
|
||||||
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
||||||
|
|
||||||
@@ -112,7 +116,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| Ключ | Описание | Пример |
|
| Ключ | Описание | Пример |
|
||||||
|------|----------|--------|
|
|------|----------|--------|
|
||||||
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
||||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
|
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
|
||||||
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
||||||
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
||||||
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
||||||
@@ -123,7 +127,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
||||||
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
||||||
| `width` / `height` | Размер | числа |
|
| `width` / `height` | Размер | числа |
|
||||||
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей |
|
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
|
||||||
|
| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
|
||||||
| `horizontalStretch: true` | Растягивать по ширине | |
|
| `horizontalStretch: true` | Растягивать по ширине | |
|
||||||
|
|
||||||
### Чекбокс (check)
|
### Чекбокс (check)
|
||||||
@@ -133,6 +138,37 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `path` | DataPath |
|
| `path` | DataPath |
|
||||||
| `titleLocation` | Размещение заголовка |
|
| `titleLocation` | Размещение заголовка |
|
||||||
|
|
||||||
|
### Поле переключателя (radio)
|
||||||
|
|
||||||
|
Радиокнопки или тумблер для выбора одного значения из списка.
|
||||||
|
|
||||||
|
| Ключ | Описание | Пример |
|
||||||
|
|------|----------|--------|
|
||||||
|
| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` |
|
||||||
|
| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` |
|
||||||
|
| `columnsCount` | Число колонок раскладки | `1`, `2`, ... |
|
||||||
|
| `titleLocation` | Размещение заголовка | по умолчанию `"none"` |
|
||||||
|
| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже |
|
||||||
|
|
||||||
|
`choiceList[*]`:
|
||||||
|
|
||||||
|
| Ключ | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` |
|
||||||
|
| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"radio": "СпособКурса",
|
||||||
|
"path": "Объект.СпособУстановкиКурса",
|
||||||
|
"radioButtonType": "Auto",
|
||||||
|
"choiceList": [
|
||||||
|
{ "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } },
|
||||||
|
{ "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Надпись-декорация (label)
|
### Надпись-декорация (label)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
@@ -143,12 +179,14 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
|
|
||||||
### Группа (group)
|
### Группа (group)
|
||||||
|
|
||||||
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`.
|
Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
|
| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
|
||||||
| `showTitle: true` | Показывать заголовок группы |
|
| `showTitle: true` | Показывать заголовок группы |
|
||||||
| `united: false` | Не объединять рамку |
|
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||||
|
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||||
| `children: [...]` | Вложенные элементы |
|
| `children: [...]` | Вложенные элементы |
|
||||||
|
|
||||||
@@ -165,8 +203,52 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `height` | Высота в строках таблицы |
|
| `height` | Высота в строках таблицы |
|
||||||
| `header: false` | Скрыть шапку |
|
| `header: false` | Скрыть шапку |
|
||||||
| `footer: true` | Показать подвал |
|
| `footer: true` | Показать подвал |
|
||||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
|
| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` |
|
||||||
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
|
| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` |
|
||||||
|
| `choiceMode: true` | Режим выбора (для форм выбора) |
|
||||||
|
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
|
||||||
|
| `enableDrag: true` | Разрешить перетаскивание |
|
||||||
|
| `enableStartDrag: true` | Разрешить начало перетаскивания |
|
||||||
|
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
|
||||||
|
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
|
||||||
|
|
||||||
|
Колонки можно группировать через `columnGroup` (см. ниже).
|
||||||
|
|
||||||
|
### Группа колонок (columnGroup)
|
||||||
|
|
||||||
|
Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`.
|
||||||
|
|
||||||
|
| Ключ | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `name` | Имя элемента (рекомендуется задавать явно) |
|
||||||
|
| `title` | Заголовок группы |
|
||||||
|
| `showTitle: false` | Скрыть заголовок |
|
||||||
|
| `showInHeader: true/false` | Показывать ли группу в шапке таблицы |
|
||||||
|
| `width` | Ширина |
|
||||||
|
| `horizontalStretch: false` | Растягивание |
|
||||||
|
| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список", "columns": [
|
||||||
|
{ "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [
|
||||||
|
{ "input": "СрокИсполнения", "path": "Список.СрокИсполнения" },
|
||||||
|
{ "labelField": "Просрочено", "path": "Список.Просрочено" }
|
||||||
|
]},
|
||||||
|
{ "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [
|
||||||
|
{ "input": "Исполнитель", "path": "Список.Исполнитель" }
|
||||||
|
]},
|
||||||
|
{ "input": "Комментарий", "path": "Список.Комментарий" }
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Картинка-поле (picField)
|
||||||
|
|
||||||
|
PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`:
|
||||||
|
|
||||||
|
| Ключ | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` |
|
||||||
|
| `loadTransparent: true` | Скрыть кадр «нет значения» |
|
||||||
|
|
||||||
### Страницы (pages + page)
|
### Страницы (pages + page)
|
||||||
|
|
||||||
@@ -188,18 +270,39 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `command` | Имя команды формы → `Form.Command.Имя` |
|
| `command` | Имя команды формы → `Form.Command.Имя` |
|
||||||
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
||||||
| `defaultButton: true` | Кнопка по умолчанию |
|
| `defaultButton: true` | Кнопка по умолчанию |
|
||||||
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` |
|
| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
|
||||||
| `picture` | Картинка кнопки |
|
| `picture` | Картинка кнопки |
|
||||||
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
||||||
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
||||||
|
|
||||||
### Командная панель (cmdBar)
|
### Командная панель (cmdBar)
|
||||||
|
|
||||||
|
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `autofill: true` | Автозаполнение стандартными командами |
|
| `autofill: true` | Автозаполнение стандартными командами |
|
||||||
| `children: [...]` | Кнопки панели |
|
| `children: [...]` | Кнопки панели |
|
||||||
|
|
||||||
|
### Главная автокомандная панель формы (autoCmdBar)
|
||||||
|
|
||||||
|
Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением.
|
||||||
|
|
||||||
|
| Ключ | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `autofill: true/false` | Автозаполнение стандартными командами |
|
||||||
|
| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` |
|
||||||
|
| `children: [...]` | Кнопки/popup |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
|
||||||
|
{ "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
|
||||||
|
"locationInCommandBar": "InAdditionalSubmenu" }
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе.
|
||||||
|
|
||||||
### Выпадающее меню (popup)
|
### Выпадающее меню (popup)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
@@ -221,6 +324,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
|
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
|
||||||
|
{ "name": "Список", "type": "DynamicList", "main": true, "settings": {
|
||||||
|
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
|
||||||
|
}}
|
||||||
{ "name": "Итого", "type": "decimal(15,2)" }
|
{ "name": "Итого", "type": "decimal(15,2)" }
|
||||||
{ "name": "Таблица", "type": "ValueTable", "columns": [
|
{ "name": "Таблица", "type": "ValueTable", "columns": [
|
||||||
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
|
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
|
||||||
@@ -229,6 +335,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `savedData: true` — сохраняемые данные
|
- `savedData: true` — сохраняемые данные
|
||||||
|
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
|
||||||
|
|
||||||
### Команды (commands)
|
### Команды (commands)
|
||||||
|
|
||||||
@@ -241,6 +348,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
|
|
||||||
### Система типов
|
### Система типов
|
||||||
|
|
||||||
|
**Примитивные:**
|
||||||
|
|
||||||
| DSL | XML |
|
| DSL | XML |
|
||||||
|------------------------|----------------------------------------|
|
|------------------------|----------------------------------------|
|
||||||
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
|
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
|
||||||
@@ -248,11 +357,38 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
|
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
|
||||||
| `"boolean"` | `xs:boolean` |
|
| `"boolean"` | `xs:boolean` |
|
||||||
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
|
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
|
||||||
| `"CatalogRef.XXX"` | `cfg:CatalogRef.XXX` |
|
|
||||||
| `"DocumentRef.XXX"` | `cfg:DocumentRef.XXX` |
|
**Ссылочные и объектные (`cfg:Prefix.Name`):**
|
||||||
| `"ValueTable"` | `v8:ValueTable` |
|
|
||||||
| `"ValueList"` | `v8:ValueListType` |
|
| DSL | Описание |
|
||||||
| `"Type1 \| Type2"` | составной тип |
|
|-----|----------|
|
||||||
|
| `"CatalogRef.XXX"` / `"CatalogObject.XXX"` | Справочник |
|
||||||
|
| `"DocumentRef.XXX"` / `"DocumentObject.XXX"` | Документ |
|
||||||
|
| `"EnumRef.XXX"` | Перечисление |
|
||||||
|
| `"DataProcessorObject.XXX"` / `"ReportObject.XXX"` | Обработка / Отчёт |
|
||||||
|
| `"InformationRegisterRecordSet.XXX"` | Набор записей регистра сведений |
|
||||||
|
| `"AccumulationRegisterRecordSet.XXX"` | Набор записей регистра накопления |
|
||||||
|
| `"DynamicList"` | Динамический список |
|
||||||
|
|
||||||
|
Также допустимы: `ChartOfAccountsRef/Object`, `ChartOfCharacteristicTypesRef/Object`, `ChartOfCalculationTypesRef/Object`, `ExchangePlanRef/Object`, `BusinessProcessRef/Object`, `TaskRef/Object`, `AccountingRegisterRecordSet`, `InformationRegisterRecordManager`, `ConstantsSet`.
|
||||||
|
|
||||||
|
**Платформенные:**
|
||||||
|
|
||||||
|
| DSL | XML |
|
||||||
|
|-----|-----|
|
||||||
|
| `"ValueTable"` | `v8:ValueTable` |
|
||||||
|
| `"ValueTree"` | `v8:ValueTree` |
|
||||||
|
| `"ValueList"` | `v8:ValueListType` |
|
||||||
|
| `"TypeDescription"` | `v8:TypeDescription` |
|
||||||
|
| `"UUID"` | `v8:UUID` |
|
||||||
|
| `"FormattedString"` | `v8ui:FormattedString` |
|
||||||
|
| `"Picture"` / `"Color"` / `"Font"` | `v8ui:*` |
|
||||||
|
| `"DataCompositionSettings"` | `dcsset:DataCompositionSettings` |
|
||||||
|
| `"Type1 \| Type2"` | составной тип (несколько `<v8:Type>`) |
|
||||||
|
|
||||||
|
**Недопустимые типы (XDTO-ошибка при загрузке):**
|
||||||
|
|
||||||
|
> `FormDataStructure`, `FormDataCollection`, `FormDataTree` — runtime-типы 1С, не существуют в XML-схеме. Вместо них используйте `CatalogObject.XXX`, `DocumentObject.XXX`, `DataProcessorObject.XXX`, `ValueTable`, `ValueTree`.
|
||||||
|
|
||||||
## Связки: элемент + реквизит
|
## Связки: элемент + реквизит
|
||||||
|
|
||||||
@@ -305,13 +441,13 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
||||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] },
|
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
|
||||||
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
||||||
]},
|
]},
|
||||||
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
||||||
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
|
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
|
||||||
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
||||||
{ "button": "Закрыть", "stdCommand": "Close" }
|
{ "button": "Закрыть", "stdCommand": "Close" }
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
"attributes": [
|
"attributes": [
|
||||||
@@ -365,8 +501,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
"title": "Просмотр данных",
|
"title": "Просмотр данных",
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "group": "horizontal", "name": "Фильтр", "children": [
|
{ "group": "horizontal", "name": "Фильтр", "children": [
|
||||||
{ "input": "Период", "path": "Период", "on": ["OnChange"] },
|
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
|
||||||
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] }
|
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
|
||||||
]},
|
]},
|
||||||
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
||||||
{ "input": "Дата", "path": "Данные.Дата" },
|
{ "input": "Дата", "path": "Данные.Дата" },
|
||||||
@@ -387,10 +523,26 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Продвинутые конструкции (по необходимости)
|
||||||
|
|
||||||
|
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`:
|
||||||
|
|
||||||
|
- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса
|
||||||
|
- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки)
|
||||||
|
- `choice-params.md` — параметры и связи выбора у полей ввода
|
||||||
|
- `command-interface.md` — командный интерфейс формы
|
||||||
|
- `roles-access.md` — пользовательская видимость и доступ по ролям
|
||||||
|
- `companion-panels.md` — контент расширенной подсказки и контекстного меню
|
||||||
|
- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок)
|
||||||
|
- `charts.md` — диаграммы и планировщик
|
||||||
|
- `report-form.md` — свойства формы отчёта
|
||||||
|
- `type-system-advanced.md` — наборы и составные типы
|
||||||
|
- `table-advanced.md` — расширенные свойства таблиц
|
||||||
|
- `layout-advanced.md` — тонкая компоновка и геометрия
|
||||||
|
|
||||||
## Автогенерация
|
## Автогенерация
|
||||||
|
|
||||||
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
||||||
- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении`
|
|
||||||
- **Namespace**: все 17 namespace-деклараций
|
- **Namespace**: все 17 namespace-деклараций
|
||||||
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
||||||
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Form Presets
|
||||||
|
|
||||||
|
Пресеты управляют раскладкой форм, генерируемых в режиме `--from-object`.
|
||||||
|
|
||||||
|
## Как работает
|
||||||
|
|
||||||
|
Цепочка merge (каждый следующий уровень перезаписывает предыдущий через deep merge):
|
||||||
|
|
||||||
|
1. **Hardcoded defaults** -- встроены в скрипт, ориентированы на ERP
|
||||||
|
2. **Built-in preset** -- файл из этой папки (`erp-standard.json` по умолчанию)
|
||||||
|
3. **Project-level preset** -- файл `presets/skills/form/<name>.json`, поиск вверх от OutputPath
|
||||||
|
|
||||||
|
Имя пресета задаётся параметром `--preset` (по умолчанию `erp-standard`).
|
||||||
|
|
||||||
|
## Project-level пресет
|
||||||
|
|
||||||
|
Чтобы переопределить стандартный пресет в своём проекте, создайте файл:
|
||||||
|
|
||||||
|
```
|
||||||
|
<project-root>/presets/skills/form/erp-standard.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт ищет этот файл, поднимаясь от OutputPath к корню. Первый найденный файл применяется поверх built-in через deep merge -- не нужно копировать весь пресет, достаточно указать только переопределяемые ключи.
|
||||||
|
|
||||||
|
## Секции
|
||||||
|
|
||||||
|
Ключи верхнего уровня в JSON -- секции вида `{тип}.{назначение}`:
|
||||||
|
|
||||||
|
| Секция | Тип объекта | Назначение формы |
|
||||||
|
|--------|-------------|------------------|
|
||||||
|
| `document.item` | Document | Форма документа |
|
||||||
|
| `document.list` | Document | Форма списка |
|
||||||
|
| `document.choice` | Document | Форма выбора |
|
||||||
|
| `catalog.item` | Catalog | Форма элемента |
|
||||||
|
| `catalog.folder` | Catalog | Форма группы |
|
||||||
|
| `catalog.list` | Catalog | Форма списка |
|
||||||
|
| `catalog.choice` | Catalog | Форма выбора |
|
||||||
|
| `informationRegister.record` | InformationRegister | Форма записи |
|
||||||
|
| `informationRegister.list` | InformationRegister | Форма списка |
|
||||||
|
| `accumulationRegister.list` | AccumulationRegister | Форма списка |
|
||||||
|
| `chartOfCharacteristicTypes.*` | ChartOfCharacteristicTypes | item/folder/list/choice |
|
||||||
|
| `exchangePlan.*` | ExchangePlan | item/list/choice |
|
||||||
|
| `chartOfAccounts.*` | ChartOfAccounts | item/folder/list/choice |
|
||||||
|
|
||||||
|
### basedOn
|
||||||
|
|
||||||
|
Секция может наследовать от другой:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document.choice": {
|
||||||
|
"basedOn": "document.list",
|
||||||
|
"properties": { "windowOpeningMode": "LockOwnerWindow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ключи секций
|
||||||
|
|
||||||
|
### Форма объекта (Item/Record)
|
||||||
|
|
||||||
|
| Ключ | Описание | Допустимые значения |
|
||||||
|
|------|----------|---------------------|
|
||||||
|
| `header.position` | Где размещать шапку | `"insidePage"` -- на первой странице, `"abovePages"` -- над страницами |
|
||||||
|
| `header.layout` | Колонки шапки | `"1col"`, `"2col"` |
|
||||||
|
| `header.distribute` | Распределение в 2 колонках | `"even"`, `"left"`, `"right"` |
|
||||||
|
| `header.dateTitle` | Заголовок даты (Document) | строка, напр. `"от"` |
|
||||||
|
| `footer.fields` | Поля в подвале | массив имён реквизитов, напр. `["Комментарий"]` |
|
||||||
|
| `footer.position` | Где размещать подвал | `"insidePage"`, `"belowPages"`, `"none"` |
|
||||||
|
| `tabularSections.container` | Контейнер табчастей | `"pages"` -- на вкладках, `"inline"` -- в корне, `"single-no-pages"` -- одна ТЧ без страниц |
|
||||||
|
| `tabularSections.exclude` | Исключить табчасти | массив имён, напр. `["ДополнительныеРеквизиты"]` |
|
||||||
|
| `tabularSections.lineNumber` | Колонка НомерСтроки | `true` / `false` |
|
||||||
|
| `additional.position` | Блок доп. реквизитов | `"page"` -- отдельная вкладка, `"below"` -- под табчастями, `"none"` -- не создавать |
|
||||||
|
| `additional.layout` | Колонки доп. блока | `"1col"`, `"2col"` |
|
||||||
|
| `additional.bspGroup` | Группа ДополнительныеРеквизиты | `true` / `false` |
|
||||||
|
| `codeDescription.layout` | Код + Наименование | `"horizontal"`, `"vertical"` |
|
||||||
|
| `codeDescription.order` | Порядок Код/Наименование | `"descriptionFirst"`, `"codeFirst"` |
|
||||||
|
| `parent.title` | Заголовок поля Родитель | строка, напр. `"Входит в группу"` |
|
||||||
|
| `parent.position` | Позиция поля Родитель | `"beforeCodeDescription"`, `"afterCodeDescription"`, `"inHeader"` |
|
||||||
|
| `owner.readOnly` | Владелец только для чтения | `true` / `false` |
|
||||||
|
| `owner.position` | Позиция поля Владелец | `"first"` |
|
||||||
|
| `fieldDefaults.ref.choiceButton` | Кнопка выбора для ссылок | `true` / `false` |
|
||||||
|
| `fieldDefaults.boolean.element` | Элемент для Boolean | `"check"` (флажок) |
|
||||||
|
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
|
||||||
|
| `properties` | Свойства формы | объект: `autoTitle`, `windowOpeningMode` и др. |
|
||||||
|
|
||||||
|
### Форма списка (List/Choice)
|
||||||
|
|
||||||
|
| Ключ | Описание | Допустимые значения |
|
||||||
|
|------|----------|---------------------|
|
||||||
|
| `columns` | Какие колонки показывать | `"all"` -- все реквизиты, или массив имён |
|
||||||
|
| `columnType` | Тип элемента колонки | `"labelField"`, `"input"` |
|
||||||
|
| `hiddenRef` | Скрытая колонка Ref | `true` / `false` |
|
||||||
|
| `tableCommandBar` | Командная панель таблицы | `"auto"`, `"none"` |
|
||||||
|
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
|
||||||
|
| `choiceMode` | Режим выбора (ChoiceForm) | `true` / `false` |
|
||||||
|
| `properties` | Свойства формы | объект: `windowOpeningMode` и др. |
|
||||||
|
|
||||||
|
## Пример project-level пресета
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-project",
|
||||||
|
"description": "Стиль форм нашего проекта",
|
||||||
|
|
||||||
|
"document.item": {
|
||||||
|
"header": {
|
||||||
|
"layout": "1col"
|
||||||
|
},
|
||||||
|
"tabularSections": {
|
||||||
|
"exclude": ["ДополнительныеРеквизиты", "СведенияОСертификатах"]
|
||||||
|
},
|
||||||
|
"additional": {
|
||||||
|
"position": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"catalog.item": {
|
||||||
|
"codeDescription": {
|
||||||
|
"order": "codeFirst"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот файл переопределяет только указанные ключи -- остальное наследуется из built-in пресета.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "erp-standard",
|
||||||
|
"description": "ERP 8.3.24 standard form layout",
|
||||||
|
|
||||||
|
"document.item": {
|
||||||
|
"header": {
|
||||||
|
"position": "insidePage",
|
||||||
|
"layout": "2col",
|
||||||
|
"distribute": "even",
|
||||||
|
"dateTitle": "от"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"fields": ["Комментарий"],
|
||||||
|
"position": "insidePage"
|
||||||
|
},
|
||||||
|
"tabularSections": {
|
||||||
|
"container": "pages",
|
||||||
|
"exclude": ["ДополнительныеРеквизиты"],
|
||||||
|
"lineNumber": true
|
||||||
|
},
|
||||||
|
"additional": {
|
||||||
|
"position": "page",
|
||||||
|
"layout": "2col",
|
||||||
|
"bspGroup": true
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"autoTitle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"catalog.item": {
|
||||||
|
"codeDescription": {
|
||||||
|
"layout": "horizontal",
|
||||||
|
"order": "descriptionFirst"
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"title": "Входит в группу",
|
||||||
|
"position": "afterCodeDescription"
|
||||||
|
},
|
||||||
|
"tabularSections": {
|
||||||
|
"exclude": ["ДополнительныеРеквизиты", "Представления"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"informationRegister.record": {
|
||||||
|
"properties": {
|
||||||
|
"windowOpeningMode": "LockOwnerWindow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"informationRegister.list": {},
|
||||||
|
|
||||||
|
"accumulationRegister.list": {},
|
||||||
|
|
||||||
|
"chartOfCharacteristicTypes.item": {
|
||||||
|
"basedOn": "catalog.item"
|
||||||
|
},
|
||||||
|
|
||||||
|
"exchangePlan.item": {
|
||||||
|
"basedOn": "catalog.item"
|
||||||
|
},
|
||||||
|
|
||||||
|
"chartOfAccounts.item": {
|
||||||
|
"parent": {
|
||||||
|
"title": "Подчинен счету"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Оформление
|
||||||
|
|
||||||
|
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
|
||||||
|
|
||||||
|
## Оформление элемента (цвета / шрифты / граница)
|
||||||
|
|
||||||
|
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
|
||||||
|
|
||||||
|
| Ключ | Что задаёт |
|
||||||
|
|------|-----------|
|
||||||
|
| `textColor` | Цвет текста |
|
||||||
|
| `backColor` | Цвет фона |
|
||||||
|
| `borderColor` | Цвет рамки |
|
||||||
|
| `font` | Шрифт |
|
||||||
|
| `border` | Граница |
|
||||||
|
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup` — `titleTextColor`/`titleFont` заголовка страницы/попапа |
|
||||||
|
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
|
||||||
|
|
||||||
|
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
|
||||||
|
|
||||||
|
### Цвет
|
||||||
|
|
||||||
|
Строка в одной из форм:
|
||||||
|
|
||||||
|
| Форма | Значение |
|
||||||
|
|-------|----------|
|
||||||
|
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
|
||||||
|
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
|
||||||
|
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
|
||||||
|
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
|
||||||
|
|
||||||
|
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
|
||||||
|
|
||||||
|
### Шрифт (`font` / `titleFont` / `footerFont`)
|
||||||
|
|
||||||
|
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
|
||||||
|
- Объект — задаются только нужные атрибуты:
|
||||||
|
|
||||||
|
| Ключ | Назначение |
|
||||||
|
|------|-----------|
|
||||||
|
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
|
||||||
|
| `faceName` | Имя гарнитуры (для собственного шрифта) |
|
||||||
|
| `height` | Размер |
|
||||||
|
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
|
||||||
|
| `scale` | Масштаб, % |
|
||||||
|
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "label": "Внимание!", "textColor": "web:FireBrick",
|
||||||
|
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Граница (`border`)
|
||||||
|
|
||||||
|
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
|
||||||
|
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
|
||||||
|
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
|
||||||
|
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Условное оформление формы (`conditionalAppearance`)
|
||||||
|
|
||||||
|
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"conditionalAppearance": [
|
||||||
|
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
|
||||||
|
"appearance": { "ЦветФона": "style:FormBackColor" },
|
||||||
|
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `selection` | array | Имена форматируемых полей формы |
|
||||||
|
| `filter` | array | Условие применения (грамматика — ниже) |
|
||||||
|
| `appearance` | object | Словарь «параметр платформы: значение» |
|
||||||
|
| `presentation` | string / object | Подпись правила в списке настроек |
|
||||||
|
| `use` | bool | `false` — правило отключено |
|
||||||
|
| `viewMode` | string | Режим отображения настройки |
|
||||||
|
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
|
||||||
|
|
||||||
|
### filter
|
||||||
|
|
||||||
|
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"filter": [
|
||||||
|
"Статус = 3",
|
||||||
|
{ "field": "Сумма", "op": ">=", "value": 1000 },
|
||||||
|
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
||||||
|
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
|
||||||
|
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
|
||||||
|
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
||||||
|
|
||||||
|
### appearance
|
||||||
|
|
||||||
|
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
|
||||||
|
|
||||||
|
Значения:
|
||||||
|
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
|
||||||
|
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
|
||||||
|
- голая строка → нелокализованный литерал (`""` → пустое значение);
|
||||||
|
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
|
||||||
|
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"conditionalAppearance": [
|
||||||
|
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
|
||||||
|
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
|
||||||
|
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
|
||||||
|
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# Диаграммы, диаграмма Ганта, планировщик
|
||||||
|
|
||||||
|
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
|
||||||
|
|
||||||
|
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
|
||||||
|
2. **Элемент** формы, привязанный к реквизиту через `path`.
|
||||||
|
|
||||||
|
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
|
||||||
|
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
|
||||||
|
|
||||||
|
## Типы реквизита и элемента
|
||||||
|
|
||||||
|
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|
||||||
|
|---------|-----------|---------------|---------------------------------|
|
||||||
|
| Диаграмма | `chart` | `d5p1:Chart` | — |
|
||||||
|
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
|
||||||
|
| Планировщик | `planner` | `pl:Planner` | — |
|
||||||
|
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
|
||||||
|
| Период | `periodField` | `v8:StandardPeriod` | — |
|
||||||
|
| Дендрограмма | `dendrogram` | — | — |
|
||||||
|
|
||||||
|
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
|
||||||
|
|
||||||
|
### Элемент диаграммы Ганта (`ganttTable`)
|
||||||
|
|
||||||
|
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "ganttChart": "Ганта", "path": "Ганта",
|
||||||
|
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design-time конфиг диаграммы (`chart`)
|
||||||
|
|
||||||
|
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
|
||||||
|
|
||||||
|
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
|
||||||
|
"chartType": "Line",
|
||||||
|
"isSeriesDesign": true, "realSeriesCount": "2",
|
||||||
|
"realSeriesData": [
|
||||||
|
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
|
||||||
|
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
|
||||||
|
"isIndicator": false, "colorPriority": false }
|
||||||
|
],
|
||||||
|
"isShowTitle": true, "title": "Продажи",
|
||||||
|
"isShowLegend": true, "legendPlacement": "Bottom",
|
||||||
|
"paletteKind": "Auto"
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
Смысловое ядро для правки:
|
||||||
|
|
||||||
|
| Ключ | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
|
||||||
|
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
|
||||||
|
| `isShowTitle` + `title` | Показ и текст заголовка |
|
||||||
|
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
|
||||||
|
| `paletteKind` | Палитра (`Auto` / …) |
|
||||||
|
| `bkgColor` / `labelsColor` / … | Базовые цвета |
|
||||||
|
|
||||||
|
Формы значений внутри `chart`:
|
||||||
|
|
||||||
|
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
|
||||||
|
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
|
||||||
|
- **`border`** — `{ width, style }`.
|
||||||
|
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
|
||||||
|
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
|
||||||
|
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
|
||||||
|
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
|
||||||
|
|
||||||
|
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
|
||||||
|
|
||||||
|
### Диаграмма Ганта (`d5p1:GanttChart`)
|
||||||
|
|
||||||
|
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
|
||||||
|
|
||||||
|
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
|
||||||
|
|
||||||
|
## Design-time конфиг планировщика (`planner`)
|
||||||
|
|
||||||
|
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
|
||||||
|
"items": [
|
||||||
|
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
|
||||||
|
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
|
||||||
|
],
|
||||||
|
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
|
||||||
|
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
|
||||||
|
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимум — один `item`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ `planner` | Тип | Назначение |
|
||||||
|
|----------------|-----|------------|
|
||||||
|
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
|
||||||
|
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
|
||||||
|
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
|
||||||
|
| `timeScale` | object | Шкала времени (см. ниже) |
|
||||||
|
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
|
||||||
|
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
|
||||||
|
| `border` | border | Рамка `{ width, style }` |
|
||||||
|
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
|
||||||
|
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
|
||||||
|
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
|
||||||
|
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
|
||||||
|
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
|
||||||
|
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
|
||||||
|
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
|
||||||
|
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
|
||||||
|
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
|
||||||
|
|
||||||
|
Шкала времени (`timeScale`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"timeScale": {
|
||||||
|
"placement": "Left",
|
||||||
|
"levels": [ { "measure": "Hour", "interval": 1 } ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
|
||||||
|
|
||||||
|
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
|
||||||
|
|
||||||
|
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Параметры выбора и связь по типу
|
||||||
|
|
||||||
|
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Контрагент", "path": "Объект.Контрагент",
|
||||||
|
"choiceParameters": [
|
||||||
|
{ "name": "Отбор.Активный", "value": true },
|
||||||
|
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
|
||||||
|
],
|
||||||
|
"choiceParameterLinks": [
|
||||||
|
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
|
||||||
|
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
|
||||||
|
],
|
||||||
|
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параметры выбора (`choiceParameters`)
|
||||||
|
|
||||||
|
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
|
||||||
|
|
||||||
|
- `name` — имя параметра (`"Отбор.Активный"`).
|
||||||
|
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
|
||||||
|
|
||||||
|
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameters": [
|
||||||
|
"Отбор.Активный=true",
|
||||||
|
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связи параметров выбора (`choiceParameterLinks`)
|
||||||
|
|
||||||
|
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
|
||||||
|
|
||||||
|
- `name` — имя параметра выбора.
|
||||||
|
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
|
||||||
|
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Договор", "path": "Объект.Договор",
|
||||||
|
"choiceParameterLinks": [
|
||||||
|
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связь по типу (`typeLink`)
|
||||||
|
|
||||||
|
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
|
||||||
|
|
||||||
|
- `dataPath` — путь к полю, задающему тип.
|
||||||
|
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"typeLink": "Объект.ВидСубконто"
|
||||||
|
"typeLink": "Объект.ВидСубконто#1"
|
||||||
|
```
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Командный интерфейс формы
|
||||||
|
|
||||||
|
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
|
||||||
|
|
||||||
|
- `commandBar` — командная панель формы;
|
||||||
|
- `navigationPanel` — панель навигации.
|
||||||
|
|
||||||
|
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"commandInterface": {
|
||||||
|
"commandBar": [
|
||||||
|
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
|
||||||
|
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
|
||||||
|
"CommonCommand.История"
|
||||||
|
],
|
||||||
|
"navigationPanel": {
|
||||||
|
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
|
||||||
|
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Элемент-команда
|
||||||
|
|
||||||
|
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"CommonCommand.История"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
|
||||||
|
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
|
||||||
|
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
|
||||||
|
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
|
||||||
|
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
|
||||||
|
| `index` | int | Порядок команды внутри группы |
|
||||||
|
| `attribute` | string | Путь реквизита для элемента панели навигации |
|
||||||
|
|
||||||
|
## Две формы записи панели
|
||||||
|
|
||||||
|
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
|
||||||
|
|
||||||
|
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"commandBar": [
|
||||||
|
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
|
||||||
|
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"navigationPanel": {
|
||||||
|
"important": [ "CommonCommand.СвязанныеДокументы" ],
|
||||||
|
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
|
||||||
|
"seeAlso": [ "CommonCommand.Заметки" ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи-группы дерева зависят от панели:
|
||||||
|
|
||||||
|
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
|
||||||
|
- `commandBar`: `important`, `createBasedOn`;
|
||||||
|
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
|
||||||
|
|
||||||
|
## Скрыть видимую команду
|
||||||
|
|
||||||
|
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"commandBar": [
|
||||||
|
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Показать команду только некоторым ролям:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "command": "Form.Command.Печать", "defaultVisible": false,
|
||||||
|
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
|
||||||
|
```
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Companion-панели и расширенная подсказка элемента
|
||||||
|
|
||||||
|
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "table": "Список", "path": "Список",
|
||||||
|
"commandBar": { "children": [ … ] },
|
||||||
|
"contextMenu": { "children": [ … ] },
|
||||||
|
"extendedTooltip": "Двойной клик открывает карточку" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Расширенная подсказка (`extendedTooltip`)
|
||||||
|
|
||||||
|
Подсказка-надпись рядом с элементом. Две формы записи.
|
||||||
|
|
||||||
|
**Текст-форма** — просто текст подсказки:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"extendedTooltip": "Укажите ИНН контрагента"
|
||||||
|
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
|
||||||
|
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
- строка — ru-текст;
|
||||||
|
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
|
||||||
|
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
|
||||||
|
|
||||||
|
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"extendedTooltip": {
|
||||||
|
"text": "Перейти к инструкции",
|
||||||
|
"hyperlink": true,
|
||||||
|
"textColor": "web:Blue",
|
||||||
|
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи own-content объекта (все необязательны):
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
|
||||||
|
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
|
||||||
|
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
|
||||||
|
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
|
||||||
|
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
|
||||||
|
| `width` / `height` | number | Размеры |
|
||||||
|
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
|
||||||
|
| `titleHeight` | number | Высота заголовка |
|
||||||
|
| `horizontalStretch` | bool | Горизонтальное растяжение |
|
||||||
|
| `verticalAlign` | string | Вертикальное выравнивание |
|
||||||
|
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
|
||||||
|
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
|
||||||
|
|
||||||
|
## Командная панель (`commandBar`)
|
||||||
|
|
||||||
|
Собственная командная панель элемента (обычно таблицы или группы).
|
||||||
|
|
||||||
|
**Значение** — массив или объект:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
|
||||||
|
|
||||||
|
"commandBar": {
|
||||||
|
"autofill": false,
|
||||||
|
"horizontalAlign": "Right",
|
||||||
|
"children": [
|
||||||
|
{ "button": "Создать", "command": "СоздатьЭлемент" },
|
||||||
|
{ "buttonGroup": "Печать", "children": [ … ] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
|
||||||
|
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
|
||||||
|
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
|
||||||
|
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
|
||||||
|
|
||||||
|
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
|
||||||
|
|
||||||
|
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
|
||||||
|
|
||||||
|
## Контекстное меню (`contextMenu`)
|
||||||
|
|
||||||
|
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
|
||||||
|
|
||||||
|
"contextMenu": {
|
||||||
|
"autofill": false,
|
||||||
|
"children": [
|
||||||
|
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `children` | array | Пункты меню — обычная грамматика кнопок |
|
||||||
|
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
|
||||||
|
|
||||||
|
## Пример: таблица со своим меню и инфо-баннером
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "table": "Заказы", "path": "Объект.Заказы",
|
||||||
|
"extendedTooltip": {
|
||||||
|
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
|
||||||
|
"formatted": true
|
||||||
|
},
|
||||||
|
"commandBar": {
|
||||||
|
"autofill": false,
|
||||||
|
"horizontalAlign": "Right",
|
||||||
|
"children": [
|
||||||
|
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
|
||||||
|
{ "button": "Удалить", "command": "УдалитьЗаказ" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contextMenu": {
|
||||||
|
"children": [
|
||||||
|
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
|
||||||
|
{ "buttonGroup": "Экспорт", "children": [
|
||||||
|
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
|
||||||
|
]
|
||||||
|
} }
|
||||||
|
```
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Динамический список
|
||||||
|
|
||||||
|
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Список", "type": "DynamicList", "main": true,
|
||||||
|
"settings": { "mainTable": "Catalog.Контрагенты" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
|
||||||
|
|
||||||
|
## Источник данных
|
||||||
|
|
||||||
|
Два взаимоисключающих режима:
|
||||||
|
|
||||||
|
**Таблично-ориентированный** — основная таблица метаданных:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"settings": { "mainTable": "Catalog.Контрагенты" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Запросный** — произвольный запрос:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"settings": {
|
||||||
|
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
|
||||||
|
"mainTable": "Document.Заказ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
|
||||||
|
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
|
||||||
|
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
|
||||||
|
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
|
||||||
|
|
||||||
|
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
|
||||||
|
|
||||||
|
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
|
||||||
|
|
||||||
|
## Параметры запроса (`parameters`)
|
||||||
|
|
||||||
|
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"settings": {
|
||||||
|
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
|
||||||
|
"parameters": [
|
||||||
|
"Артикул",
|
||||||
|
"Маска: string = %",
|
||||||
|
{ "name": "ВидЦен", "valueListAllowed": true },
|
||||||
|
{ "name": "Период", "type": "dateTime" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
|
||||||
|
|
||||||
|
## Значения параметров в настройках (`dataParameters`)
|
||||||
|
|
||||||
|
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dataParameters": [ "Организация = _", "ВидЦен" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поля набора (`fields`)
|
||||||
|
|
||||||
|
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"fields": [
|
||||||
|
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
|
||||||
|
{ "field": "Остаток", "valueType": "number(15,2)" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
|
||||||
|
|
||||||
|
## Вычисляемые поля (`calculatedFields`)
|
||||||
|
|
||||||
|
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"calculatedFields": [
|
||||||
|
"Метка = Code + \" \" + Description",
|
||||||
|
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Объектная форма — для `presentationExpression` / `orderExpression`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
|
||||||
|
"valueType": "string(10)", "presentationExpression": "Code" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Отбор (`filter`)
|
||||||
|
|
||||||
|
Shorthand `"Поле оператор значение @флаги"` или объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"filter": [
|
||||||
|
"Организация = _ @off @user",
|
||||||
|
"Сумма > 1000",
|
||||||
|
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
|
||||||
|
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
||||||
|
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
|
||||||
|
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
|
||||||
|
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
||||||
|
|
||||||
|
## Сортировка (`order`)
|
||||||
|
|
||||||
|
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"order": [ "Дата desc", "Наименование", "Auto" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Группировка строк (`grouping`)
|
||||||
|
|
||||||
|
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"grouping": "Контрагент > Договор"
|
||||||
|
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
|
||||||
|
|
||||||
|
## Условное оформление (`conditionalAppearance`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"conditionalAppearance": [
|
||||||
|
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Продвинутая раскладка
|
||||||
|
|
||||||
|
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
|
||||||
|
|
||||||
|
## Выравнивание внутри родителя
|
||||||
|
|
||||||
|
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
|
||||||
|
|
||||||
|
| Ключ | Значения | Что выравнивает |
|
||||||
|
|------|----------|-----------------|
|
||||||
|
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
|
||||||
|
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
|
||||||
|
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
|
||||||
|
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
|
||||||
|
|
||||||
|
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "button": "ОК", "groupHorizontalAlign": "Right" }
|
||||||
|
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
|
||||||
|
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ограничение максимального размера
|
||||||
|
|
||||||
|
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `maxWidth` | число | Жёсткий максимум ширины элемента |
|
||||||
|
| `maxHeight` | число | Жёсткий максимум высоты элемента |
|
||||||
|
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
|
||||||
|
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
|
||||||
|
|
||||||
|
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
|
||||||
|
"horizontalStretch": true, "autoMaxWidth": false }
|
||||||
|
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поведение при вводе и активации
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
|
||||||
|
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
|
||||||
|
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
|
||||||
|
|
||||||
|
## Перетаскивание
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
|
||||||
|
|
||||||
|
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
|
||||||
|
|
||||||
|
## Закрепление колонки в таблице (`fixingInTable`)
|
||||||
|
|
||||||
|
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
|
||||||
|
|
||||||
|
| Значения |
|
||||||
|
|----------|
|
||||||
|
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||||
|
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
||||||
|
{ "input": "Количество", "path": "Объект.Товары.Количество" },
|
||||||
|
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
|
||||||
|
|
||||||
|
## Ячейки колонок: шапка и подвал
|
||||||
|
|
||||||
|
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
|
||||||
|
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
|
||||||
|
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
|
||||||
|
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
|
||||||
|
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
|
||||||
|
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
|
||||||
|
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
|
||||||
|
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Адаптивная важность (`displayImportance`)
|
||||||
|
|
||||||
|
| Значения |
|
||||||
|
|----------|
|
||||||
|
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
|
||||||
|
|
||||||
|
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
|
||||||
|
```
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Форма отчёта
|
||||||
|
|
||||||
|
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"properties": {
|
||||||
|
"reportFormType": "Main",
|
||||||
|
"reportResult": "РезультатОтчета",
|
||||||
|
"detailsData": "ДанныеРасшифровки"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
|
||||||
|
|
||||||
|
## Тип формы отчёта (`reportFormType`)
|
||||||
|
|
||||||
|
Роль формы в составе отчёта:
|
||||||
|
|
||||||
|
| Значение | Назначение |
|
||||||
|
|----------|-----------|
|
||||||
|
| `Main` | Основная форма отчёта (результат + настройки) |
|
||||||
|
| `Settings` | Форма настроек |
|
||||||
|
| `Variant` | Форма варианта |
|
||||||
|
|
||||||
|
```json
|
||||||
|
"reportFormType": "Main"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Привязка к компоновке
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
|
||||||
|
| `detailsData` | string | Имя реквизита данных расшифровки |
|
||||||
|
| `variantAppearance` | string | Имя реквизита оформления варианта |
|
||||||
|
|
||||||
|
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
|
||||||
|
|
||||||
|
## Группа пользовательских настроек (`customSettingsFolder`)
|
||||||
|
|
||||||
|
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Прочие свойства компоновки
|
||||||
|
|
||||||
|
Редкие, задавайте только при явной необходимости:
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
|
||||||
|
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
|
||||||
|
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
|
||||||
|
|
||||||
|
## Реалистичный пример
|
||||||
|
|
||||||
|
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"reportFormType": "Main",
|
||||||
|
"reportResult": "РезультатОтчета",
|
||||||
|
"detailsData": "ДанныеРасшифровки",
|
||||||
|
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
||||||
|
},
|
||||||
|
"attributes": [
|
||||||
|
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
|
||||||
|
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
|
||||||
|
],
|
||||||
|
"elements": [
|
||||||
|
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
|
||||||
|
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
|
||||||
|
"titleLocation": "none" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Доступ по ролям
|
||||||
|
|
||||||
|
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
|
||||||
|
|
||||||
|
| Ключ | Владелец | Смысл |
|
||||||
|
|------|----------|-------|
|
||||||
|
| `userVisible` | элемент формы | пользовательская видимость элемента |
|
||||||
|
| `view` | реквизит формы | право просмотра |
|
||||||
|
| `edit` | реквизит формы | право редактирования |
|
||||||
|
| `use` | команда формы | доступность команды |
|
||||||
|
|
||||||
|
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
|
||||||
|
|
||||||
|
## Значение
|
||||||
|
|
||||||
|
Две формы (одинаковы для всех четырёх ключей):
|
||||||
|
|
||||||
|
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Поле", "userVisible": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Реквизит",
|
||||||
|
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
|
||||||
|
|
||||||
|
## Имя роли
|
||||||
|
|
||||||
|
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
Элемент скрыт у всех пользователей:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "input": "Комментарий", "userVisible": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Реквизит не виден никому и редактируется только одной ролью:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "СуммаБонуса",
|
||||||
|
"view": false,
|
||||||
|
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле доступно для просмотра всем, но редактируемо только администратору:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Статус",
|
||||||
|
"view": true,
|
||||||
|
"edit": { "common": false, "roles": { "Администратор": true } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда недоступна по умолчанию, разрешена только бухгалтеру:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "ПровестиЗакрытие",
|
||||||
|
"use": { "common": false, "roles": { "Бухгалтер": true } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Обратный случай — доступно всем, кроме одной роли:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "РедактироватьЦену",
|
||||||
|
"edit": { "common": true, "roles": { "Кладовщик": false } } }
|
||||||
|
```
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Спец-поля «документ/датчик»
|
||||||
|
|
||||||
|
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
|
||||||
|
|
||||||
|
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
|
||||||
|
|
||||||
|
| Ключ типа | Тип реквизита |
|
||||||
|
|-----------|---------------|
|
||||||
|
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
|
||||||
|
| `html` | `string` |
|
||||||
|
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
|
||||||
|
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
|
||||||
|
| `progressBar` | число |
|
||||||
|
| `trackBar` | число |
|
||||||
|
|
||||||
|
## spreadsheet — поле табличного документа
|
||||||
|
|
||||||
|
Просмотр/редактирование табличного документа (отчёт, печатная форма).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
|
||||||
|
"titleLocation": "none", "readOnly": true,
|
||||||
|
"output": "Disable", "protection": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
||||||
|
| `protection` | bool | Защита от изменений |
|
||||||
|
| `edit` | bool | Разрешить редактирование |
|
||||||
|
| `showGrid` | bool | Показывать сетку |
|
||||||
|
| `showHeaders` | bool | Показывать заголовки строк/колонок |
|
||||||
|
| `showGroups` | bool | Показывать группировки |
|
||||||
|
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
|
||||||
|
| `showCellNames` | bool | Показывать имена ячеек |
|
||||||
|
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
|
||||||
|
| `viewScalingMode` | string | Режим масштабирования просмотра |
|
||||||
|
| `selectionShowMode` | string | Режим отображения выделения |
|
||||||
|
| `pointerType` | string | Тип указателя |
|
||||||
|
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
|
||||||
|
|
||||||
|
## html — поле HTML-документа
|
||||||
|
|
||||||
|
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
|
||||||
|
"output": "Enable", "warningOnEditRepresentation": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
||||||
|
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
|
||||||
|
|
||||||
|
## textDoc — поле текстового документа
|
||||||
|
|
||||||
|
Просмотр/редактирование текстового документа.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
||||||
|
|
||||||
|
## formattedDoc — поле форматированного документа
|
||||||
|
|
||||||
|
Просмотр/редактирование форматированного документа.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
||||||
|
|
||||||
|
## progressBar — поле индикатора
|
||||||
|
|
||||||
|
Индикатор прогресса. Реквизит — числовой.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "progressBar": "Прогресс", "path": "Прогресс",
|
||||||
|
"minValue": 0, "maxValue": 100, "showPercent": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
||||||
|
| `showPercent` | bool | Показывать проценты |
|
||||||
|
|
||||||
|
## trackBar — поле ползунка
|
||||||
|
|
||||||
|
Регулятор-ползунок. Реквизит — числовой.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "trackBar": "Масштаб", "path": "Масштаб",
|
||||||
|
"minValue": 20, "maxValue": 400, "markingStep": 20 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
||||||
|
| `step` | число | Шаг изменения |
|
||||||
|
| `largeStep` | число | Крупный шаг |
|
||||||
|
| `markingStep` | число | Шаг разметки |
|
||||||
|
| `markingAppearance` | string | Оформление разметки |
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Таблица — продвинутые возможности
|
||||||
|
|
||||||
|
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
|
||||||
|
|
||||||
|
## Представление (`representation`)
|
||||||
|
|
||||||
|
Как таблица рисует строки:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список", "representation": "Tree" }
|
||||||
|
```
|
||||||
|
|
||||||
|
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
|
||||||
|
|
||||||
|
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
|
||||||
|
|
||||||
|
## Выделение и текущая строка
|
||||||
|
|
||||||
|
| Ключ | Значения | Назначение |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
|
||||||
|
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
|
||||||
|
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поиск при вводе (`searchOnInput`)
|
||||||
|
|
||||||
|
Поведение встроенного поиска при наборе текста в таблице:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
|
||||||
|
```
|
||||||
|
|
||||||
|
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
|
||||||
|
|
||||||
|
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
|
||||||
|
|
||||||
|
## Прочие свойства таблицы
|
||||||
|
|
||||||
|
| Ключ | Тип | Назначение |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| `useAlternationRowColor` | bool | Чередование цвета строк |
|
||||||
|
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
|
||||||
|
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
|
||||||
|
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
|
||||||
|
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
|
||||||
|
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
|
||||||
|
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
|
||||||
|
|
||||||
|
## Фиксация колонки (`fixingInTable`)
|
||||||
|
|
||||||
|
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||||
|
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
||||||
|
{ "input": "Количество", "path": "Объект.Товары.Количество" }
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Left` / `Right` / `None`.
|
||||||
|
|
||||||
|
## Исключённые команды (`excludedCommands`)
|
||||||
|
|
||||||
|
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Товары", "path": "Объект.Товары",
|
||||||
|
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
|
||||||
|
|
||||||
|
## Дополнения командной панели (`additions`)
|
||||||
|
|
||||||
|
Дополнения — это «представления» встроенного поиска таблицы:
|
||||||
|
|
||||||
|
- `searchString` — отображение строки поиска,
|
||||||
|
- `viewStatus` — состояние просмотра,
|
||||||
|
- `searchControl` — управление поиском.
|
||||||
|
|
||||||
|
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
|
||||||
|
|
||||||
|
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список",
|
||||||
|
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список", "commandBar": [
|
||||||
|
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
|
||||||
|
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
|
||||||
|
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
|
||||||
|
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
|
||||||
|
|
||||||
|
## Таблица динамического списка
|
||||||
|
|
||||||
|
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
|
||||||
|
|
||||||
|
| Ключ | Тип | Умолчание | Назначение |
|
||||||
|
|------|-----|-----------|-----------|
|
||||||
|
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
|
||||||
|
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
|
||||||
|
| `autoRefresh` | bool | `false` | Автообновление списка |
|
||||||
|
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
|
||||||
|
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
|
||||||
|
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
|
||||||
|
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
|
||||||
|
| `showRoot` | bool | `true` | Показывать корень |
|
||||||
|
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
|
||||||
|
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
|
||||||
|
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "table": "Список", "path": "Список",
|
||||||
|
"representation": "Tree",
|
||||||
|
"rowPictureDataPath": "Список.DefaultPicture",
|
||||||
|
"choiceFoldersAndItems": "FoldersAndItems",
|
||||||
|
"allowRootChoice": true,
|
||||||
|
"updateOnDataChange": "DontUpdate" }
|
||||||
|
```
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Продвинутые конструкции типов
|
||||||
|
|
||||||
|
Примитивы (`string(n)`, `number(p,s)`, `boolean`, `date`/`dateTime`, …) и одиночные ссылки (`CatalogRef.Контрагенты`, `DocumentRef.Заказ`, `EnumRef.X`, …) описаны в основной инструкции. Здесь — типы, которые нельзя выразить одним именем: составные типы, наборы типов и платформенные наборы ссылок.
|
||||||
|
|
||||||
|
Любая из этих конструкций пишется в поле `type` реквизита, реквизита-параметра или поля.
|
||||||
|
|
||||||
|
## Составные типы
|
||||||
|
|
||||||
|
Несколько типов на одном реквизите — части перечисляются через разделитель `" | "` (можно `+`). Реквизит сможет принимать значение любого из перечисленных типов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Плательщик",
|
||||||
|
"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Смешивать можно типы из разных категорий — ссылки, примитивы, наборы типов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Источник",
|
||||||
|
"type": "CatalogRef.Контрагенты | DocumentRef.Заказ | string(150)" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждая часть — самостоятельный токен из этого файла или из основной инструкции. Порядок частей произвольный.
|
||||||
|
|
||||||
|
## Наборы типов (TypeSet)
|
||||||
|
|
||||||
|
«Набор типов» подставляется вместо конкретного типа — это один токен, а не перечисление. Применимо и в составном типе как одна из частей.
|
||||||
|
|
||||||
|
| Токен `type` | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `"DefinedType.ИмяТипа"` | определяемый тип конфигурации |
|
||||||
|
| `"Characteristic.ИмяПлана"` | тип значения характеристики (по плану видов характеристик) |
|
||||||
|
| `"AnyRef"` | любая ссылка |
|
||||||
|
| `"AnyIBRef"` | любая ссылка информационной базы |
|
||||||
|
|
||||||
|
Определяемый тип — реквизит принимает то, что задано в определяемом типе конфигурации (например `DefinedType.ДенежнаяСумма`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Сумма", "type": "DefinedType.ДенежнаяСумма" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Характеристика — тип значения берётся из плана видов характеристик:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Значение", "type": "Characteristic.ДополнительныеРеквизиты" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Платформенные наборы ссылок
|
||||||
|
|
||||||
|
«Голый» ссылочный токен **без `.Имя`** означает «любая ссылка этой категории объектов»:
|
||||||
|
|
||||||
|
| Токен `type` | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `"CatalogRef"` | любая ссылка справочника |
|
||||||
|
| `"DocumentRef"` | любая ссылка документа |
|
||||||
|
| `"EnumRef"` | любая ссылка перечисления |
|
||||||
|
| `"ExchangePlanRef"` | любая ссылка плана обмена |
|
||||||
|
| `"TaskRef"` | любая ссылка задачи |
|
||||||
|
| `"BusinessProcessRef"` | любая ссылка бизнес-процесса |
|
||||||
|
| `"ChartOfCharacteristicTypesRef"` | любая ссылка плана видов характеристик |
|
||||||
|
| `"ChartOfAccountsRef"` | любая ссылка плана счетов |
|
||||||
|
| `"ChartOfCalculationTypesRef"` | любая ссылка плана видов расчёта |
|
||||||
|
|
||||||
|
Различие с одиночной ссылкой — только в наличии `.Имя`:
|
||||||
|
|
||||||
|
- `"CatalogRef.Валюты"` — конкретный справочник «Валюты»;
|
||||||
|
- `"CatalogRef"` — любой справочник.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "ЛюбойСправочник", "type": "CatalogRef" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Эти наборы тоже комбинируются в составном типе:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Объект", "type": "CatalogRef | DocumentRef" }
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user