mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-06 03:08:57 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b385836a |
@@ -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 +1 @@
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
|
||||
@@ -1,58 +1,60 @@
|
||||
---
|
||||
name: cf-edit
|
||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-edit — редактирование конфигурации 1С
|
||||
|
||||
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Operation` | Операция (см. таблицу) |
|
||||
| `Value` | Значение для операции (batch через `;;`) |
|
||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Формат Value | Описание |
|
||||
|----------|-------------|----------|
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||
|
||||
Подробнее: `reference.md` в каталоге навыка.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Изменить версию и поставщика
|
||||
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||
|
||||
# Добавить объекты
|
||||
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||
|
||||
# Удалить объект
|
||||
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||
|
||||
# Роли по умолчанию
|
||||
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
||||
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||
```
|
||||
---
|
||||
name: cf-edit
|
||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-edit — редактирование конфигурации 1С
|
||||
|
||||
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Operation` | Операция (см. таблицу) |
|
||||
| `Value` | Значение для операции (batch через `;;`) |
|
||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Формат Value | Описание |
|
||||
|----------|-------------|----------|
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `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)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Изменить версию и поставщика
|
||||
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||
|
||||
# Добавить объекты
|
||||
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||
|
||||
# Удалить объект
|
||||
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||
|
||||
# Роли по умолчанию
|
||||
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
|
||||
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||
```
|
||||
|
||||
@@ -1,63 +1,150 @@
|
||||
# cf-edit — справочник операций
|
||||
|
||||
## modify-property
|
||||
|
||||
Свойства для редактирования:
|
||||
|
||||
### Скалярные
|
||||
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
|
||||
|
||||
### LocalString (многоязычные)
|
||||
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
|
||||
|
||||
### Enum
|
||||
| Свойство | Допустимые значения |
|
||||
|----------|---------------------|
|
||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
|
||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||
| `ScriptVariant` | `Russian`, `English` |
|
||||
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
|
||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
|
||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||
|
||||
### Ref
|
||||
`DefaultLanguage` — значение вида `Language.Русский`
|
||||
|
||||
### Формат batch
|
||||
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
|
||||
|
||||
## add-childObject / remove-childObject
|
||||
|
||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||
|
||||
При добавлении объект вставляется в каноническую позицию:
|
||||
1. Находит последний элемент того же типа → вставляет после
|
||||
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
|
||||
3. Внутри одного типа — алфавитный порядок
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
`set-defaultRoles` полностью заменяет список ролей.
|
||||
|
||||
## DefinitionFile (JSON)
|
||||
|
||||
```json
|
||||
[
|
||||
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
|
||||
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
|
||||
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
|
||||
]
|
||||
```
|
||||
|
||||
## Авто-валидация
|
||||
|
||||
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
|
||||
# cf-edit — справочник операций
|
||||
|
||||
## modify-property
|
||||
|
||||
Свойства для редактирования:
|
||||
|
||||
### Скалярные
|
||||
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
|
||||
|
||||
### LocalString (многоязычные)
|
||||
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
|
||||
|
||||
### Enum
|
||||
| Свойство | Допустимые значения |
|
||||
|----------|---------------------|
|
||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
|
||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||
| `ScriptVariant` | `Russian`, `English` |
|
||||
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
|
||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
|
||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||
|
||||
### Ref
|
||||
`DefaultLanguage` — значение вида `Language.Русский`
|
||||
|
||||
### Формат batch
|
||||
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
|
||||
|
||||
## add-childObject / remove-childObject
|
||||
|
||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||
|
||||
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
`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)
|
||||
|
||||
```json
|
||||
[
|
||||
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
|
||||
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
|
||||
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid as _uuid
|
||||
from html import escape as html_escape
|
||||
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"
|
||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
@@ -32,6 +194,22 @@ TYPE_ORDER = [
|
||||
"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", "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"]
|
||||
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
||||
REF_PROPS = ["DefaultLanguage"]
|
||||
@@ -131,7 +309,9 @@ def parse_batch_value(val):
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
|
||||
@@ -141,9 +321,9 @@ def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
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("-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("-NoValidate", action="store_true")
|
||||
args = parser.parse_args()
|
||||
@@ -169,6 +349,9 @@ def main():
|
||||
print(f"File not found: {config_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
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)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
@@ -283,6 +466,25 @@ def main():
|
||||
sys.exit(1)
|
||||
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
|
||||
exists = False
|
||||
for child in child_objs_el:
|
||||
@@ -455,6 +657,269 @@ def main():
|
||||
modify_count += 1
|
||||
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",
|
||||
"задача": "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 ---
|
||||
operations = []
|
||||
if args.DefinitionFile:
|
||||
@@ -475,17 +940,21 @@ def main():
|
||||
op_value = op.get("value", args.Value or "")
|
||||
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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:
|
||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
---
|
||||
name: cf-info
|
||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-info — Структура конфигурации 1С
|
||||
|
||||
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
| Режим | Что показывает |
|
||||
|---|---|
|
||||
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
||||
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
||||
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор пустой конфигурации
|
||||
... -ConfigPath upload/cfempty
|
||||
|
||||
# Краткая сводка реальной конфигурации
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
||||
|
||||
# Полная информация
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode full
|
||||
|
||||
# С пагинацией
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
||||
```
|
||||
---
|
||||
name: cf-info
|
||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-info — Структура конфигурации 1С
|
||||
|
||||
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
| Режим | Что показывает |
|
||||
|---|---|
|
||||
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
||||
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
||||
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор пустой конфигурации
|
||||
... -ConfigPath src
|
||||
|
||||
# Краткая сводка реальной конфигурации
|
||||
... -ConfigPath src -Mode brief
|
||||
|
||||
# Полная информация
|
||||
... -ConfigPath src -Mode full
|
||||
|
||||
# С пагинацией
|
||||
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
||||
|
||||
# Drill-down: только начальная страница (раскладка форм с ролями)
|
||||
... -ConfigPath src -Section home-page
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from lxml import etree
|
||||
@@ -13,8 +14,9 @@ sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# --- Argument parsing ---
|
||||
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("-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("-Offset", type=int, default=0, help="Lines to skip")
|
||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||
@@ -125,6 +127,173 @@ type_ru_names = {
|
||||
"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 ---
|
||||
object_counts = OrderedDict()
|
||||
total_objects = 0
|
||||
@@ -146,6 +315,7 @@ cfg_version = get_prop_text("Version")
|
||||
cfg_vendor = get_prop_text("Vendor")
|
||||
cfg_compat = get_prop_text("CompatibilityMode")
|
||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||
cfg_script = get_prop_text("ScriptVariant")
|
||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||
@@ -159,14 +329,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||
|
||||
# --- BRIEF mode ---
|
||||
if args.Mode == "brief":
|
||||
if args.Mode == "brief" and not args.Section:
|
||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||
|
||||
# --- OVERVIEW mode ---
|
||||
if args.Mode == "overview":
|
||||
if args.Mode == "overview" and not args.Section:
|
||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||
@@ -178,6 +348,8 @@ if args.Mode == "overview":
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
out(f"Совместимость: {cfg_compat}")
|
||||
out(f"Режим запуска: {cfg_default_run}")
|
||||
out(f"Язык скриптов: {cfg_script}")
|
||||
@@ -187,6 +359,20 @@ if args.Mode == "overview":
|
||||
out(f"Интерфейс: {cfg_intf_compat}")
|
||||
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
|
||||
out(f"--- Состав ({total_objects} объектов) ---")
|
||||
out()
|
||||
@@ -207,7 +393,30 @@ if args.Mode == "overview":
|
||||
out(f" {padded} {count}")
|
||||
|
||||
# --- 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 ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||
@@ -229,6 +438,8 @@ if args.Mode == "full":
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||
if cfg_update_addr:
|
||||
out(f"Каталог обн.: {cfg_update_addr}")
|
||||
@@ -283,6 +494,26 @@ if args.Mode == "full":
|
||||
out(f"Обычн.формы в управл.: {use_of}")
|
||||
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 ---
|
||||
out("--- Хранилища и формы по умолчанию ---")
|
||||
storage_props = [
|
||||
|
||||
@@ -1,58 +1,49 @@
|
||||
---
|
||||
name: cf-init
|
||||
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
||||
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-init — Создание пустой конфигурации 1С
|
||||
|
||||
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `Name` | Имя конфигурации (обязат.) |
|
||||
| `Synonym` | Синоним (= Name если не указан) |
|
||||
| `OutputDir` | Каталог для создания (default: `src`) |
|
||||
| `Version` | Версия конфигурации |
|
||||
| `Vendor` | Поставщик |
|
||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<OutputDir>/
|
||||
├── Configuration.xml # Корневой файл — все свойства
|
||||
└── Languages/
|
||||
└── Русский.xml # Язык по умолчанию
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Базовая конфигурация
|
||||
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||
|
||||
# С версией и поставщиком
|
||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||
|
||||
# Другой режим совместимости
|
||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cf-init TestConfig -OutputDir test-tmp/cf
|
||||
/cf-info test-tmp/cf — проверить созданное
|
||||
/cf-validate test-tmp/cf — валидировать
|
||||
```
|
||||
---
|
||||
name: cf-init
|
||||
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
||||
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-init — Создание пустой конфигурации 1С
|
||||
|
||||
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `Name` | Имя конфигурации (обязат.) |
|
||||
| `Synonym` | Синоним (= Name если не указан) |
|
||||
| `OutputDir` | Каталог для создания (default: `src`) |
|
||||
| `Version` | Версия конфигурации |
|
||||
| `Vendor` | Поставщик |
|
||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Базовая конфигурация
|
||||
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||
|
||||
# С версией и поставщиком
|
||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||
|
||||
# Другой режим совместимости
|
||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cf-init TestConfig -OutputDir test-tmp/cf
|
||||
/cf-info test-tmp/cf — проверить созданное
|
||||
/cf-validate test-tmp/cf — валидировать
|
||||
```
|
||||
|
||||
@@ -1,215 +1,249 @@
|
||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$OutputDir = "src",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Mobile functionalities ---
|
||||
$mobileFuncs = @(
|
||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
||||
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
||||
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
||||
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
||||
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
||||
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
||||
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
||||
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
||||
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
||||
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||
)
|
||||
|
||||
$mobileXml = ""
|
||||
foreach ($mf in $mobileFuncs) {
|
||||
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
||||
}
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?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">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<NamePrefix/>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles/>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<UpdateCatalogAddress/>
|
||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||
<AdditionalFullTextSearchDictionaries/>
|
||||
<CommonSettingsStorage/>
|
||||
<ReportsUserSettingsStorage/>
|
||||
<ReportsVariantsStorage/>
|
||||
<FormDataSettingsStorage/>
|
||||
<DynamicListsUserSettingsStorage/>
|
||||
<URLExternalDataStorage/>
|
||||
<Content/>
|
||||
<DefaultReportForm/>
|
||||
<DefaultReportVariantForm/>
|
||||
<DefaultReportSettingsForm/>
|
||||
<DefaultReportAppearanceTemplate/>
|
||||
<DefaultDynamicListSettingsForm/>
|
||||
<DefaultSearchForm/>
|
||||
<DefaultDataHistoryChangeHistoryForm/>
|
||||
<DefaultDataHistoryVersionDataForm/>
|
||||
<DefaultDataHistoryVersionDifferencesForm/>
|
||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
||||
<RequiredMobileApplicationPermissions/>
|
||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||
</UsedMobileApplicationFunctionalities>
|
||||
<StandaloneConfigurationRestrictionRoles/>
|
||||
<MobileApplicationURLs/>
|
||||
<AllowedIncomingShareRequestTypes/>
|
||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
||||
<DefaultInterface/>
|
||||
<DefaultStyle/>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<DataLockControlMode>Managed</DataLockControlMode>
|
||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||
<DefaultConstantsForm/>
|
||||
</Properties>
|
||||
<ChildObjects>
|
||||
<Language>Русский</Language>
|
||||
</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
$langXml = @"
|
||||
<?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">
|
||||
<Language uuid="$uuidLang">
|
||||
<Properties>
|
||||
<Name>Русский</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Русский</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создана конфигурация: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$OutputDir = "src",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Mobile functionalities ---
|
||||
$mobileFuncs = @(
|
||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
||||
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
||||
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
||||
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
||||
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
||||
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
||||
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
||||
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
||||
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
||||
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||
)
|
||||
|
||||
$mobileXml = ""
|
||||
foreach ($mf in $mobileFuncs) {
|
||||
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
||||
}
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?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">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<NamePrefix/>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles/>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<UpdateCatalogAddress/>
|
||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||
<AdditionalFullTextSearchDictionaries/>
|
||||
<CommonSettingsStorage/>
|
||||
<ReportsUserSettingsStorage/>
|
||||
<ReportsVariantsStorage/>
|
||||
<FormDataSettingsStorage/>
|
||||
<DynamicListsUserSettingsStorage/>
|
||||
<URLExternalDataStorage/>
|
||||
<Content/>
|
||||
<DefaultReportForm/>
|
||||
<DefaultReportVariantForm/>
|
||||
<DefaultReportSettingsForm/>
|
||||
<DefaultReportAppearanceTemplate/>
|
||||
<DefaultDynamicListSettingsForm/>
|
||||
<DefaultSearchForm/>
|
||||
<DefaultDataHistoryChangeHistoryForm/>
|
||||
<DefaultDataHistoryVersionDataForm/>
|
||||
<DefaultDataHistoryVersionDifferencesForm/>
|
||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
||||
<RequiredMobileApplicationPermissions/>
|
||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||
</UsedMobileApplicationFunctionalities>
|
||||
<StandaloneConfigurationRestrictionRoles/>
|
||||
<MobileApplicationURLs/>
|
||||
<AllowedIncomingShareRequestTypes/>
|
||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
||||
<DefaultInterface/>
|
||||
<DefaultStyle/>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<DataLockControlMode>Managed</DataLockControlMode>
|
||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||
<DefaultConstantsForm/>
|
||||
</Properties>
|
||||
<ChildObjects>
|
||||
<Language>Русский</Language>
|
||||
</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
$langXml = @"
|
||||
<?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">
|
||||
<Language uuid="$uuidLang">
|
||||
<Properties>
|
||||
<Name>Русский</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Русский</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</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 ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
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 ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создана конфигурация: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
Write-Host " Ext/CAI: $caiFile"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration."""
|
||||
import sys, os, argparse, uuid
|
||||
@@ -155,7 +155,7 @@ def main():
|
||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||
\t\t\t<DefaultConstantsForm/>
|
||||
@@ -184,20 +184,50 @@ def main():
|
||||
\t</Language>
|
||||
</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 ---
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
lang_dir = os.path.join(output_dir, "Languages")
|
||||
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_utf8_bom(cfg_file, cfg_xml)
|
||||
lang_file = os.path.join(lang_dir, "Русский.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" Каталог: {output_dir}")
|
||||
print(f" Configuration.xml: {cfg_file}")
|
||||
print(f" Languages: {lang_file}")
|
||||
print(f" Ext/CAI: {cai_file}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,44 +1,29 @@
|
||||
---
|
||||
name: cf-validate
|
||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-validate — валидация конфигурации 1С
|
||||
|
||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|----------|-------------|
|
||||
| 1 | XML well-formedness, MetaDataObject/Configuration, version 2.17/2.20 | ERROR |
|
||||
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId, уникальность | ERROR |
|
||||
| 3 | Properties: Name непустой, Synonym, DefaultLanguage, DefaultRunMode | ERROR/WARN |
|
||||
| 4 | Properties: enum-значения (11 свойств) | ERROR |
|
||||
| 5 | ChildObjects: валидные имена типов (44 типа), нет дубликатов, порядок типов | ERROR/WARN |
|
||||
| 6 | DefaultLanguage ссылается на существующий Language в ChildObjects | ERROR |
|
||||
| 7 | Файлы языков Languages/<name>.xml существуют | WARN |
|
||||
| 8 | Каталоги объектов из ChildObjects существуют (spot-check) | WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
---
|
||||
name: cf-validate
|
||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-validate — валидация конфигурации 1С
|
||||
|
||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-validate v1.1 — Validate 1C configuration XML structure
|
||||
# cf-validate v1.3 — Validate 1C configuration XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||
import sys, os, argparse, re
|
||||
@@ -82,7 +82,7 @@ VALID_ENUM_VALUES = {
|
||||
'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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||
'ScriptVariant': ['Russian', 'English'],
|
||||
@@ -90,7 +90,10 @@ VALID_ENUM_VALUES = {
|
||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||
'ModalityUseMode': ['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'],
|
||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||
'CompatibilityMode': [
|
||||
@@ -100,7 +103,7 @@ VALID_ENUM_VALUES = {
|
||||
'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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -162,7 +165,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
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('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
@@ -228,8 +231,8 @@ def main():
|
||||
version = root.get('version', '')
|
||||
if not version:
|
||||
r.warn('1. Missing version attribute on MetaDataObject')
|
||||
elif version not in ('2.17', '2.20'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||
elif version not in ('2.17', '2.20', '2.21'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||
|
||||
# Must have Configuration child
|
||||
cfg_node = None
|
||||
@@ -534,6 +537,60 @@ def main():
|
||||
else:
|
||||
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 ---
|
||||
r.finalize(out_file)
|
||||
sys.exit(1 if r.errors > 0 else 0)
|
||||
|
||||
+101
-101
@@ -1,101 +1,101 @@
|
||||
---
|
||||
name: cfe-borrow
|
||||
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-borrow — Заимствование объектов из конфигурации
|
||||
|
||||
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||
| `BorrowMainAttribute` | Используй при добавлении нового реквизита на заимствованную форму. `Form` (по умолч.) — реквизиты с формы, `All` — все реквизиты объекта |
|
||||
|
||||
## Формат -Object
|
||||
|
||||
- `Catalog.Контрагенты` — справочник
|
||||
- `CommonModule.РаботаСФайлами` — общий модуль
|
||||
- `Document.РеализацияТоваров` — документ
|
||||
- `Enum.ВидыОплат` — перечисление
|
||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||
Поддерживаются все 44 типа объектов конфигурации.
|
||||
|
||||
### Заимствование форм
|
||||
|
||||
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
|
||||
|
||||
Создаётся:
|
||||
1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
|
||||
2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
|
||||
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
|
||||
4. **Регистрация** — `<Form>` в ChildObjects родительского объекта
|
||||
|
||||
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
||||
|
||||
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
||||
|
||||
**Два режима**:
|
||||
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
||||
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
||||
|
||||
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
||||
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||
|
||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Заимствовать один объект
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
|
||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||
|
||||
# Несколько объектов за раз
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||
|
||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
|
||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
|
||||
---
|
||||
name: cfe-borrow
|
||||
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-borrow — Заимствование объектов из конфигурации
|
||||
|
||||
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||
|
||||
## Формат -Object
|
||||
|
||||
- `Catalog.Контрагенты` — справочник
|
||||
- `CommonModule.РаботаСФайлами` — общий модуль
|
||||
- `Document.РеализацияТоваров` — документ
|
||||
- `Enum.ВидыОплат` — перечисление
|
||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||
Поддерживаются все 44 типа объектов конфигурации.
|
||||
|
||||
### Заимствование форм
|
||||
|
||||
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
|
||||
|
||||
Создаётся:
|
||||
1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
|
||||
2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
|
||||
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
|
||||
4. **Регистрация** — `<Form>` в ChildObjects родительского объекта
|
||||
|
||||
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
||||
|
||||
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
||||
|
||||
**Два режима**:
|
||||
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
||||
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
||||
|
||||
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
||||
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||
|
||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Заимствовать один объект
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
|
||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||
|
||||
# Несколько объектов за раз
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||
|
||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
|
||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
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"
|
||||
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):
|
||||
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):
|
||||
if container.text and "\n" in container.text:
|
||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||
@@ -305,7 +351,9 @@ def expand_self_closing(container, parent_indent):
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'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)
|
||||
@@ -363,6 +411,8 @@ def main():
|
||||
cfg_resolved = os.path.abspath(cfg_path)
|
||||
cfg_dir = os.path.dirname(cfg_resolved)
|
||||
|
||||
format_version = detect_format_version(ext_dir)
|
||||
|
||||
# --- 2. Load extension Configuration.xml ---
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(ext_resolved, xml_parser)
|
||||
@@ -442,6 +492,13 @@ def main():
|
||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||
if prop_node is not None:
|
||||
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}
|
||||
|
||||
@@ -499,7 +556,7 @@ def main():
|
||||
|
||||
lines = []
|
||||
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(internal_info_xml)
|
||||
lines.append("\t\t<Properties>")
|
||||
@@ -513,6 +570,10 @@ def main():
|
||||
prop_val = source_props.get(prop_name, "false")
|
||||
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>")
|
||||
|
||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||
@@ -624,7 +685,26 @@ def main():
|
||||
first_level = {}
|
||||
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)
|
||||
segments = path.split(".")
|
||||
seg0 = segments[0]
|
||||
@@ -635,22 +715,14 @@ def main():
|
||||
seg1 = segments[1]
|
||||
if seg1 in STANDARD_FIELDS:
|
||||
continue
|
||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
||||
|
||||
# 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
|
||||
seg2 = segments[2] if len(segments) >= 3 else None
|
||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||
|
||||
# Deduplicate deep paths
|
||||
seen = set()
|
||||
unique_deep = []
|
||||
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:
|
||||
seen.add(key)
|
||||
unique_deep.append(dp)
|
||||
@@ -921,26 +993,40 @@ def main():
|
||||
# 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")
|
||||
|
||||
# Generate full object XML with attributes and TS
|
||||
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
|
||||
# Read existing object XML (needed for dedup + enrichment)
|
||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||
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:
|
||||
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||
obj_props_block = m_props.group(1) if m_props else ""
|
||||
props_xml = ""
|
||||
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}>"
|
||||
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
|
||||
if adopted_content:
|
||||
@@ -992,79 +1078,93 @@ def main():
|
||||
|
||||
# Step 5: Handle deep paths (Form mode only)
|
||||
if mode == "Form" and deep_paths:
|
||||
# Filter out deep paths where ObjectAttr is a TabularSection
|
||||
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
||||
|
||||
if real_deep:
|
||||
info(f" Processing {len(real_deep)} deep path(s)...")
|
||||
|
||||
# Group by ObjectAttr -> target catalog
|
||||
deep_by_attr = {}
|
||||
for dp in real_deep:
|
||||
if dp["ObjectAttr"] not in deep_by_attr:
|
||||
deep_by_attr[dp["ObjectAttr"]] = []
|
||||
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||
deep_by_attr = {}
|
||||
for dp in deep_paths:
|
||||
if dp["ObjectAttr"] in ts_names:
|
||||
continue
|
||||
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||
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():
|
||||
# Find the attribute's type to determine target catalog
|
||||
attr_info = None
|
||||
for a in src_attrs:
|
||||
if a["Name"] == attr_name:
|
||||
attr_info = a
|
||||
break
|
||||
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||
if not attr_info:
|
||||
continue
|
||||
|
||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||
if not cat_match:
|
||||
continue
|
||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||
|
||||
target_type_name = cat_match.group(1)
|
||||
target_obj_name = cat_match.group(2)
|
||||
|
||||
# Ensure target is borrowed
|
||||
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}")
|
||||
|
||||
# Resolve sub-attributes in target catalog
|
||||
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"])
|
||||
|
||||
# Collect and borrow ref types from deep 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']}")
|
||||
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||
ts_deep_by_col = {}
|
||||
for dp in deep_paths:
|
||||
if dp["ObjectAttr"] not in ts_names:
|
||||
continue
|
||||
if not dp.get("SubSubAttr"):
|
||||
continue
|
||||
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||
continue
|
||||
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||
ts_deep_by_col.setdefault(k, [])
|
||||
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||
if ts_deep_by_col:
|
||||
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||
if not ts_info:
|
||||
continue
|
||||
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||
if not col_info:
|
||||
continue
|
||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||
if not cat_match:
|
||||
continue
|
||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||
|
||||
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):
|
||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||
|
||||
@@ -1080,11 +1180,25 @@ def main():
|
||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||
src_form_content = fh.read()
|
||||
|
||||
# 3. Generate form metadata XML
|
||||
new_form_uuid = new_guid()
|
||||
# 3. Generate form metadata XML.
|
||||
# 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 = [
|
||||
'<?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}">',
|
||||
'\t\t<InternalInfo/>',
|
||||
'\t\t<Properties>',
|
||||
@@ -1111,7 +1225,10 @@ def main():
|
||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||
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
|
||||
form_props = []
|
||||
@@ -1129,25 +1246,21 @@ def main():
|
||||
continue
|
||||
if not reached_visual:
|
||||
# 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+)?="[^"]*"')
|
||||
|
||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||
auto_cmd_xml = ""
|
||||
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 = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||
# Strip DataPath in AutoCommandBar buttons
|
||||
if 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)
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||
|
||||
# ChildItems: copy full tree, clean up base-config references
|
||||
child_items_xml = ""
|
||||
@@ -1158,20 +1271,12 @@ def main():
|
||||
break
|
||||
|
||||
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)
|
||||
# Replace all CommandName values with 0
|
||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
||||
if 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 data-binding tags whose root attribute isn't borrowed
|
||||
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||
@@ -1408,12 +1513,16 @@ def main():
|
||||
save_text_bom(form_xml_file, "".join(parts))
|
||||
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")
|
||||
os.makedirs(module_dir, exist_ok=True)
|
||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||
save_text_bom(module_bsl_file, "")
|
||||
info(f" Created: {module_bsl_file}")
|
||||
if os.path.isfile(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
|
||||
register_form_in_object(type_name, obj_name, form_name)
|
||||
|
||||
@@ -1,70 +1,57 @@
|
||||
---
|
||||
name: cfe-diff
|
||||
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-diff — Анализ расширения конфигурации
|
||||
|
||||
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
||||
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
```
|
||||
|
||||
## Mode A — обзор расширения
|
||||
|
||||
Для каждого объекта показывает:
|
||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||
|
||||
Пример вывода:
|
||||
```
|
||||
[BORROWED] Catalog.Валюты
|
||||
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ...
|
||||
&Перед("ЗагрузитьКурсыВалют") — line 13 in ...
|
||||
ChildObjects: 1 own attrs, 1 own TS, 3 own forms
|
||||
Form.ФормаЭлемента (borrowed):
|
||||
Event:OnCreateAtServer [After] -> Расш1_ПриСозданииПосле
|
||||
Command:Подбор [Before] -> Расш1_ПодборПеред
|
||||
Form.Расш1_МояФорма (own)
|
||||
[OWN] Catalog.Расш5_Справочник1
|
||||
```
|
||||
|
||||
Для каждой формы заимствованного объекта показывается:
|
||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||
- callType-события формы и элементов
|
||||
- callType на командах
|
||||
|
||||
## Mode B — проверка переноса
|
||||
|
||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||
|
||||
Статусы:
|
||||
- `[TRANSFERRED]` — код найден в конфигурации
|
||||
- `[NOT_TRANSFERRED]` — код не найден
|
||||
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор — что изменено в расширении
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
|
||||
# Проверка переноса — все ли #Вставка перенесены
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
||||
```
|
||||
---
|
||||
name: cfe-diff
|
||||
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-diff — Анализ расширения конфигурации
|
||||
|
||||
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
||||
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
```
|
||||
|
||||
## Mode A — обзор расширения
|
||||
|
||||
Для каждого объекта показывает:
|
||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||
|
||||
Для каждой формы заимствованного объекта показывается:
|
||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||
- callType-события формы и элементов
|
||||
- callType на командах
|
||||
|
||||
## Mode B — проверка переноса
|
||||
|
||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||
|
||||
Статусы:
|
||||
- `[TRANSFERRED]` — код найден в конфигурации
|
||||
- `[NOT_TRANSFERRED]` — код не найден
|
||||
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор — что изменено в расширении
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
|
||||
# Проверка переноса — все ли #Вставка перенесены
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
||||
```
|
||||
|
||||
@@ -1,471 +1,471 @@
|
||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigPath,
|
||||
|
||||
[ValidateSet("A","B")]
|
||||
[string]$Mode = "A"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve paths ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
|
||||
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
$childTypeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
$extDoc = New-Object System.Xml.XmlDocument
|
||||
$extDoc.PreserveWhitespace = $false
|
||||
$extDoc.Load($extCfg)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
|
||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||
Write-Host " NamePrefix: $namePrefix"
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
if (-not $childObjNode) {
|
||||
Write-Host "[WARN] No ChildObjects in extension"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$objects = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Language") { continue }
|
||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||
}
|
||||
|
||||
if ($objects.Count -eq 0) {
|
||||
Write-Host "No objects (besides Language) in extension."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
function Get-ObjectInfo {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$objEl = $null
|
||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||
|
||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||
|
||||
$info = @{
|
||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||
File = $objFile
|
||||
Exists = $true
|
||||
Type = $objType
|
||||
Name = $objName
|
||||
DirName = $dirName
|
||||
ObjElement = $objEl
|
||||
ObjNs = $objNs
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
function Get-BslFiles {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||
|
||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||
|
||||
$bslFiles = @()
|
||||
$extDir = Join-Path $objDir "Ext"
|
||||
if (Test-Path $extDir) {
|
||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||
}
|
||||
|
||||
# Forms
|
||||
$formsDir = Join-Path $objDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||
}
|
||||
|
||||
return $bslFiles
|
||||
}
|
||||
|
||||
# --- Helper: parse interceptors from .bsl ---
|
||||
function Get-Interceptors {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$interceptors = @()
|
||||
$i = 0
|
||||
while ($i -lt $lines.Count) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
||||
$type = $Matches[1]
|
||||
$method = $Matches[2]
|
||||
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return $interceptors
|
||||
}
|
||||
|
||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||
function Get-InsertionBlocks {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$blocks = @()
|
||||
$inBlock = $false
|
||||
$blockLines = @()
|
||||
$startLine = 0
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -eq "#Вставка") {
|
||||
$inBlock = $true
|
||||
$blockLines = @()
|
||||
$startLine = $i + 1
|
||||
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
||||
$inBlock = $false
|
||||
$blocks += @{
|
||||
StartLine = $startLine
|
||||
EndLine = $i + 1
|
||||
Code = ($blockLines -join "`n").Trim()
|
||||
File = $bslPath
|
||||
}
|
||||
} elseif ($inBlock) {
|
||||
$blockLines += $lines[$i]
|
||||
}
|
||||
}
|
||||
return $blocks
|
||||
}
|
||||
|
||||
# --- Helper: analyze form for callType events and commands ---
|
||||
function Get-FormInterceptors {
|
||||
param([string]$formXmlPath)
|
||||
|
||||
if (-not (Test-Path $formXmlPath)) { return $null }
|
||||
|
||||
$formDoc = New-Object System.Xml.XmlDocument
|
||||
$formDoc.PreserveWhitespace = $false
|
||||
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
||||
|
||||
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
||||
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
|
||||
$fRoot = $formDoc.DocumentElement
|
||||
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
||||
$isBorrowed = ($baseForm -ne $null)
|
||||
|
||||
$interceptors = @()
|
||||
|
||||
# Form-level events with callType
|
||||
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
||||
if ($eventsNode) {
|
||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Element-level events with callType (scan all elements recursively)
|
||||
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
|
||||
if ($childItems) {
|
||||
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
|
||||
$elName = $evtNode.GetAttribute("name")
|
||||
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Commands with callType on Action
|
||||
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
|
||||
$ct = $action.GetAttribute("callType")
|
||||
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
IsBorrowed = $isBorrowed
|
||||
Interceptors = $interceptors
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE A: Extension overview
|
||||
# ============================================================
|
||||
if ($Mode -eq "A") {
|
||||
$borrowedList = @()
|
||||
$ownList = @()
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||
continue
|
||||
}
|
||||
if (-not $info.Exists) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($info.Borrowed) {
|
||||
$borrowedList += $obj
|
||||
|
||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
if ($interceptors.Count -gt 0) {
|
||||
foreach ($ic in $interceptors) {
|
||||
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host " $relPath (no interceptors)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$ownAttrs = 0
|
||||
$ownForms = 0
|
||||
$ownTS = 0
|
||||
$borrowedItems = 0
|
||||
$formNames = @()
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
||||
if ($cProps) {
|
||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||
$borrowedItems++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " ChildObjects: $($parts -join ', ')"
|
||||
}
|
||||
|
||||
# Analyze forms
|
||||
$borrowedFormCount = 0
|
||||
$ownFormCount = 0
|
||||
foreach ($fn in $formNames) {
|
||||
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
||||
$fi = Get-FormInterceptors $formXmlPath
|
||||
if (-not $fi) {
|
||||
Write-Host " Form.$fn (?)"
|
||||
continue
|
||||
}
|
||||
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
||||
if ($fi.Interceptors.Count -gt 0) {
|
||||
Write-Host " Form.$fn ($formTag):"
|
||||
foreach ($ic in $fi.Interceptors) {
|
||||
Write-Host " $ic"
|
||||
}
|
||||
} else {
|
||||
Write-Host " Form.$fn ($formTag)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ownList += $obj
|
||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Brief info for own objects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$attrs = 0; $forms = 0; $ts = 0
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $attrs++ }
|
||||
"TabularSection" { $ts++ }
|
||||
"Form" { $forms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " $($parts -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE B: Transfer check
|
||||
# ============================================================
|
||||
if ($Mode -eq "B") {
|
||||
$transferred = 0
|
||||
$notTransferred = 0
|
||||
$needsReview = 0
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||
|
||||
if ($macInterceptors.Count -eq 0) { continue }
|
||||
|
||||
foreach ($ic in $macInterceptors) {
|
||||
$methodName = $ic.Method
|
||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
$insertBlocks = Get-InsertionBlocks $bsl
|
||||
|
||||
if ($insertBlocks.Count -eq 0) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
# Find corresponding module in config
|
||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||
$dirName = $childTypeDirMap[$obj.Type]
|
||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||
|
||||
if (-not (Test-Path $configBsl)) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$allTransferred = $true
|
||||
foreach ($block in $insertBlocks) {
|
||||
$code = $block.Code
|
||||
if (-not $code) { continue }
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
$codeNorm = $code -replace '\s+', ' '
|
||||
$configNorm = $configContent -replace '\s+', ' '
|
||||
|
||||
if ($configNorm.Contains($codeNorm)) {
|
||||
# Found in config
|
||||
} else {
|
||||
$allTransferred = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allTransferred) {
|
||||
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||
$transferred++
|
||||
} else {
|
||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
||||
$notTransferred++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||
}
|
||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigPath,
|
||||
|
||||
[ValidateSet("A","B")]
|
||||
[string]$Mode = "A"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve paths ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
|
||||
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
$childTypeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
$extDoc = New-Object System.Xml.XmlDocument
|
||||
$extDoc.PreserveWhitespace = $false
|
||||
$extDoc.Load($extCfg)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
|
||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||
Write-Host " NamePrefix: $namePrefix"
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
if (-not $childObjNode) {
|
||||
Write-Host "[WARN] No ChildObjects in extension"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$objects = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Language") { continue }
|
||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||
}
|
||||
|
||||
if ($objects.Count -eq 0) {
|
||||
Write-Host "No objects (besides Language) in extension."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
function Get-ObjectInfo {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$objEl = $null
|
||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||
|
||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||
|
||||
$info = @{
|
||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||
File = $objFile
|
||||
Exists = $true
|
||||
Type = $objType
|
||||
Name = $objName
|
||||
DirName = $dirName
|
||||
ObjElement = $objEl
|
||||
ObjNs = $objNs
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
function Get-BslFiles {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||
|
||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||
|
||||
$bslFiles = @()
|
||||
$extDir = Join-Path $objDir "Ext"
|
||||
if (Test-Path $extDir) {
|
||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||
}
|
||||
|
||||
# Forms
|
||||
$formsDir = Join-Path $objDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||
}
|
||||
|
||||
return $bslFiles
|
||||
}
|
||||
|
||||
# --- Helper: parse interceptors from .bsl ---
|
||||
function Get-Interceptors {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$interceptors = @()
|
||||
$i = 0
|
||||
while ($i -lt $lines.Count) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
||||
$type = $Matches[1]
|
||||
$method = $Matches[2]
|
||||
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return $interceptors
|
||||
}
|
||||
|
||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||
function Get-InsertionBlocks {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$blocks = @()
|
||||
$inBlock = $false
|
||||
$blockLines = @()
|
||||
$startLine = 0
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -eq "#Вставка") {
|
||||
$inBlock = $true
|
||||
$blockLines = @()
|
||||
$startLine = $i + 1
|
||||
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
||||
$inBlock = $false
|
||||
$blocks += @{
|
||||
StartLine = $startLine
|
||||
EndLine = $i + 1
|
||||
Code = ($blockLines -join "`n").Trim()
|
||||
File = $bslPath
|
||||
}
|
||||
} elseif ($inBlock) {
|
||||
$blockLines += $lines[$i]
|
||||
}
|
||||
}
|
||||
return $blocks
|
||||
}
|
||||
|
||||
# --- Helper: analyze form for callType events and commands ---
|
||||
function Get-FormInterceptors {
|
||||
param([string]$formXmlPath)
|
||||
|
||||
if (-not (Test-Path $formXmlPath)) { return $null }
|
||||
|
||||
$formDoc = New-Object System.Xml.XmlDocument
|
||||
$formDoc.PreserveWhitespace = $false
|
||||
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
||||
|
||||
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
||||
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
|
||||
$fRoot = $formDoc.DocumentElement
|
||||
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
||||
$isBorrowed = ($baseForm -ne $null)
|
||||
|
||||
$interceptors = @()
|
||||
|
||||
# Form-level events with callType
|
||||
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
||||
if ($eventsNode) {
|
||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Element-level events with callType (scan all elements recursively)
|
||||
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
|
||||
if ($childItems) {
|
||||
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
|
||||
$elName = $evtNode.GetAttribute("name")
|
||||
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Commands with callType on Action
|
||||
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
|
||||
$ct = $action.GetAttribute("callType")
|
||||
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
IsBorrowed = $isBorrowed
|
||||
Interceptors = $interceptors
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE A: Extension overview
|
||||
# ============================================================
|
||||
if ($Mode -eq "A") {
|
||||
$borrowedList = @()
|
||||
$ownList = @()
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||
continue
|
||||
}
|
||||
if (-not $info.Exists) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($info.Borrowed) {
|
||||
$borrowedList += $obj
|
||||
|
||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
if ($interceptors.Count -gt 0) {
|
||||
foreach ($ic in $interceptors) {
|
||||
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host " $relPath (no interceptors)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$ownAttrs = 0
|
||||
$ownForms = 0
|
||||
$ownTS = 0
|
||||
$borrowedItems = 0
|
||||
$formNames = @()
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
||||
if ($cProps) {
|
||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||
$borrowedItems++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " ChildObjects: $($parts -join ', ')"
|
||||
}
|
||||
|
||||
# Analyze forms
|
||||
$borrowedFormCount = 0
|
||||
$ownFormCount = 0
|
||||
foreach ($fn in $formNames) {
|
||||
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
||||
$fi = Get-FormInterceptors $formXmlPath
|
||||
if (-not $fi) {
|
||||
Write-Host " Form.$fn (?)"
|
||||
continue
|
||||
}
|
||||
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
||||
if ($fi.Interceptors.Count -gt 0) {
|
||||
Write-Host " Form.$fn ($formTag):"
|
||||
foreach ($ic in $fi.Interceptors) {
|
||||
Write-Host " $ic"
|
||||
}
|
||||
} else {
|
||||
Write-Host " Form.$fn ($formTag)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ownList += $obj
|
||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Brief info for own objects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$attrs = 0; $forms = 0; $ts = 0
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $attrs++ }
|
||||
"TabularSection" { $ts++ }
|
||||
"Form" { $forms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " $($parts -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE B: Transfer check
|
||||
# ============================================================
|
||||
if ($Mode -eq "B") {
|
||||
$transferred = 0
|
||||
$notTransferred = 0
|
||||
$needsReview = 0
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||
|
||||
if ($macInterceptors.Count -eq 0) { continue }
|
||||
|
||||
foreach ($ic in $macInterceptors) {
|
||||
$methodName = $ic.Method
|
||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
$insertBlocks = Get-InsertionBlocks $bsl
|
||||
|
||||
if ($insertBlocks.Count -eq 0) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
# Find corresponding module in config
|
||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||
$dirName = $childTypeDirMap[$obj.Type]
|
||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||
|
||||
if (-not (Test-Path $configBsl)) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$allTransferred = $true
|
||||
foreach ($block in $insertBlocks) {
|
||||
$code = $block.Code
|
||||
if (-not $code) { continue }
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
$codeNorm = $code -replace '\s+', ' '
|
||||
$configNorm = $configContent -replace '\s+', ' '
|
||||
|
||||
if ($configNorm.Contains($codeNorm)) {
|
||||
# Found in config
|
||||
} else {
|
||||
$allTransferred = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allTransferred) {
|
||||
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||
$transferred++
|
||||
} else {
|
||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
||||
$notTransferred++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||
}
|
||||
|
||||
@@ -1,82 +1,71 @@
|
||||
---
|
||||
name: cfe-init
|
||||
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
|
||||
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-init — Создание расширения конфигурации 1С
|
||||
|
||||
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
|
||||
|
||||
## Подготовка
|
||||
|
||||
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `Name` | Имя расширения (обязат.) | — |
|
||||
| `Synonym` | Синоним | = Name |
|
||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||
| `OutputDir` | Каталог для создания | `src` |
|
||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||
| `Version` | Версия расширения | — |
|
||||
| `Vendor` | Поставщик | — |
|
||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||
| `NoRole` | Без основной роли | false |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<OutputDir>/
|
||||
├── Configuration.xml # Свойства расширения
|
||||
├── Languages/
|
||||
│ └── Русский.xml # Язык (заимствованный)
|
||||
└── Roles/ # Если не -NoRole
|
||||
└── <Prefix>ОсновнаяРоль.xml
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
||||
|
||||
# Расширение-исправление с явным режимом совместимости
|
||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
||||
|
||||
# Расширение-доработка с версией
|
||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
||||
|
||||
# Без роли, с явным префиксом
|
||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <OutputDir>
|
||||
```
|
||||
|
||||
---
|
||||
name: cfe-init
|
||||
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
|
||||
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-init — Создание расширения конфигурации 1С
|
||||
|
||||
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
|
||||
|
||||
## Подготовка
|
||||
|
||||
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `Name` | Имя расширения (обязат.) | — |
|
||||
| `Synonym` | Синоним | = Name |
|
||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||
| `OutputDir` | Каталог для создания | `src` |
|
||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||
| `Version` | Версия расширения | — |
|
||||
| `Vendor` | Поставщик | — |
|
||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||
| `NoRole` | Без основной роли | false |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-init.py" -Name "МоёРасширение"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
||||
|
||||
# Расширение-исправление с явным режимом совместимости
|
||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
||||
|
||||
# Расширение-доработка с версией
|
||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
||||
|
||||
# Без роли, с явным префиксом
|
||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <OutputDir>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,261 +1,279 @@
|
||||
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$NamePrefix,
|
||||
[string]$OutputDir = "src",
|
||||
[ValidateSet("Patch","Customization","AddOn")]
|
||||
[string]$Purpose = "Customization",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
[string]$ConfigPath,
|
||||
[switch]$NoRole
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Default NamePrefix ---
|
||||
if (-not $NamePrefix) {
|
||||
$NamePrefix = "${Name}_"
|
||||
}
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Resolve ConfigPath ---
|
||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||
if ($ConfigPath) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ConfigPath -PathType Container) {
|
||||
$candidate = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (Test-Path $candidate) { $ConfigPath = $candidate }
|
||||
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
|
||||
}
|
||||
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
|
||||
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
|
||||
|
||||
# 3a. Read Language UUID from base config
|
||||
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
|
||||
if (Test-Path $baseLangFile) {
|
||||
$baseLangDoc = New-Object System.Xml.XmlDocument
|
||||
$baseLangDoc.PreserveWhitespace = $false
|
||||
$baseLangDoc.Load($baseLangFile)
|
||||
$langEl = $null
|
||||
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
|
||||
}
|
||||
if ($langEl) {
|
||||
$baseLangUuid = $langEl.GetAttribute("uuid")
|
||||
Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
|
||||
} else {
|
||||
Write-Host "[WARN] No <Language> element in $baseLangFile"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[WARN] Base config language not found: $baseLangFile"
|
||||
}
|
||||
|
||||
# 3b. Read CompatibilityMode from base config
|
||||
$baseCfgDoc = New-Object System.Xml.XmlDocument
|
||||
$baseCfgDoc.PreserveWhitespace = $false
|
||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||
if ($compatNode -and $compatNode.InnerText) {
|
||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
|
||||
} else {
|
||||
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
$uuidRole = [guid]::NewGuid().ToString()
|
||||
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Role name ---
|
||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||
|
||||
# --- DefaultRoles XML ---
|
||||
$defaultRolesXml = ""
|
||||
if (-not $NoRole) {
|
||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- ChildObjects ---
|
||||
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
|
||||
if (-not $NoRole) {
|
||||
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
|
||||
}
|
||||
$childObjectsXml += "`r`n`t`t"
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?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">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
</Properties>
|
||||
<ChildObjects>$childObjectsXml</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
$langXml = @"
|
||||
<?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">
|
||||
<Language uuid="$uuidLang">
|
||||
<InternalInfo/>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>Русский</Name>
|
||||
<Comment/>
|
||||
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Role XML ---
|
||||
$roleXml = @"
|
||||
<?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">
|
||||
<Role uuid="$uuidRole">
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||
<Synonym/>
|
||||
<Comment/>
|
||||
</Properties>
|
||||
</Role>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Role ---
|
||||
if (-not $NoRole) {
|
||||
$roleDir = Join-Path $OutputDir "Roles"
|
||||
if (-not (Test-Path $roleDir)) {
|
||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||
}
|
||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создано расширение: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Назначение: $Purpose"
|
||||
Write-Host " Префикс: $NamePrefix"
|
||||
Write-Host " Совместимость: $CompatibilityMode"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
if (-not $NoRole) {
|
||||
Write-Host " Role: $roleFile"
|
||||
}
|
||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$NamePrefix,
|
||||
[string]$OutputDir = "src",
|
||||
[ValidateSet("Patch","Customization","AddOn")]
|
||||
[string]$Purpose = "Customization",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
[string]$ConfigPath,
|
||||
[switch]$NoRole
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Default NamePrefix ---
|
||||
if (-not $NamePrefix) {
|
||||
$NamePrefix = "${Name}_"
|
||||
}
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
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 ---
|
||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||
if ($ConfigPath) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ConfigPath -PathType Container) {
|
||||
$candidate = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (Test-Path $candidate) { $ConfigPath = $candidate }
|
||||
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
|
||||
}
|
||||
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
|
||||
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
|
||||
|
||||
# 3a. Read Language UUID from base config
|
||||
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
|
||||
if (Test-Path $baseLangFile) {
|
||||
$baseLangDoc = New-Object System.Xml.XmlDocument
|
||||
$baseLangDoc.PreserveWhitespace = $false
|
||||
$baseLangDoc.Load($baseLangFile)
|
||||
$langEl = $null
|
||||
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
|
||||
}
|
||||
if ($langEl) {
|
||||
$baseLangUuid = $langEl.GetAttribute("uuid")
|
||||
Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
|
||||
} else {
|
||||
Write-Host "[WARN] No <Language> element in $baseLangFile"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[WARN] Base config language not found: $baseLangFile"
|
||||
}
|
||||
|
||||
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
|
||||
$baseCfgDoc = New-Object System.Xml.XmlDocument
|
||||
$baseCfgDoc.PreserveWhitespace = $false
|
||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||
$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)
|
||||
if ($compatNode -and $compatNode.InnerText) {
|
||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
|
||||
} else {
|
||||
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
|
||||
}
|
||||
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
|
||||
if ($ifcNode -and $ifcNode.InnerText) {
|
||||
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
|
||||
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
|
||||
} else {
|
||||
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
|
||||
}
|
||||
} else {
|
||||
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
$uuidRole = [guid]::NewGuid().ToString()
|
||||
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Role name ---
|
||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||
|
||||
# --- DefaultRoles XML ---
|
||||
$defaultRolesXml = ""
|
||||
if (-not $NoRole) {
|
||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- ChildObjects ---
|
||||
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
|
||||
if (-not $NoRole) {
|
||||
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
|
||||
}
|
||||
$childObjectsXml += "`r`n`t`t"
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?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="$formatVersion">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
|
||||
</Properties>
|
||||
<ChildObjects>$childObjectsXml</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
$langXml = @"
|
||||
<?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="$formatVersion">
|
||||
<Language uuid="$uuidLang">
|
||||
<InternalInfo/>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>Русский</Name>
|
||||
<Comment/>
|
||||
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Role XML ---
|
||||
$roleXml = @"
|
||||
<?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="$formatVersion">
|
||||
<Role uuid="$uuidRole">
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||
<Synonym/>
|
||||
<Comment/>
|
||||
</Properties>
|
||||
</Role>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Role ---
|
||||
if (-not $NoRole) {
|
||||
$roleDir = Join-Path $OutputDir "Roles"
|
||||
if (-not (Test-Path $roleDir)) {
|
||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||
}
|
||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создано расширение: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Назначение: $Purpose"
|
||||
Write-Host " Префикс: $NamePrefix"
|
||||
Write-Host " Совместимость: $CompatibilityMode"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
if (-not $NoRole) {
|
||||
Write-Host " Role: $roleFile"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-init v1.0 — 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
|
||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||
import sys, os, argparse, uuid
|
||||
@@ -50,6 +50,10 @@ def main():
|
||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||
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 ---
|
||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
if args.ConfigPath:
|
||||
@@ -84,10 +88,14 @@ def main():
|
||||
else:
|
||||
print(f"[WARN] Base config language not found: {base_lang_file}")
|
||||
|
||||
# Read CompatibilityMode from base config
|
||||
# Read CompatibilityMode and InterfaceCompatibilityMode from base config
|
||||
try:
|
||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||
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'}
|
||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||
if compat_node is not None and compat_node.text:
|
||||
@@ -95,9 +103,18 @@ def main():
|
||||
print(f"[INFO] Base config CompatibilityMode: {compat}")
|
||||
else:
|
||||
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
|
||||
ifc_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:InterfaceCompatibilityMode', ns)
|
||||
if ifc_node is not None and ifc_node.text:
|
||||
ifc_mode = ifc_node.text.strip()
|
||||
print(f"[INFO] Base config InterfaceCompatibilityMode: {ifc_mode}")
|
||||
else:
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
print(f"[WARN] InterfaceCompatibilityMode not found in base config, using default: {ifc_mode}")
|
||||
except Exception:
|
||||
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
else:
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
@@ -146,7 +163,7 @@ def main():
|
||||
\t\t\t</xr:ContainedObject>\n"""
|
||||
|
||||
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\t<InternalInfo>
|
||||
{contained_objects}\t\t</InternalInfo>
|
||||
@@ -173,7 +190,7 @@ def main():
|
||||
\t\t\t<Copyright/>
|
||||
\t\t\t<VendorInformationAddress/>
|
||||
\t\t\t<ConfigurationInformationAddress/>
|
||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>{ifc_mode}</InterfaceCompatibilityMode>
|
||||
\t\t</Properties>
|
||||
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
|
||||
\t</Configuration>
|
||||
@@ -181,7 +198,7 @@ def main():
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
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\t<InternalInfo/>
|
||||
\t\t<Properties>
|
||||
@@ -196,7 +213,7 @@ def main():
|
||||
|
||||
# --- Role XML ---
|
||||
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\t<Properties>
|
||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
---
|
||||
name: cfe-patch-method
|
||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-patch-method — Генерация перехватчика метода
|
||||
|
||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
||||
| `Context` | Директива контекста | `НаСервере` |
|
||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
||||
|
||||
## Формат ModulePath
|
||||
|
||||
| ModulePath | Файл |
|
||||
|------------|------|
|
||||
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
||||
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
||||
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
||||
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
||||
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
|
||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||
|
||||
## Типы перехвата
|
||||
|
||||
| InterceptorType | Декоратор | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
||||
| `After` | `&После` | Код после вызова оригинального метода |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Перехват &Перед на сервере
|
||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
|
||||
# Перехват &После на клиенте
|
||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
||||
|
||||
# ИзменениеИКонтроль для функции
|
||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
||||
```
|
||||
|
||||
## Генерируемый код (Before)
|
||||
|
||||
```bsl
|
||||
&НаСервере
|
||||
&Перед("ПриЗаписи")
|
||||
Процедура Расш1_ПриЗаписи()
|
||||
// TODO: код перед вызовом оригинального метода
|
||||
КонецПроцедуры
|
||||
```
|
||||
---
|
||||
name: cfe-patch-method
|
||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-patch-method — Генерация перехватчика метода
|
||||
|
||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
||||
| `Context` | Директива контекста | `НаСервере` |
|
||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
||||
|
||||
## Формат ModulePath
|
||||
|
||||
| ModulePath | Файл |
|
||||
|------------|------|
|
||||
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
||||
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
||||
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
||||
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
||||
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
|
||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||
|
||||
## Типы перехвата
|
||||
|
||||
| InterceptorType | Декоратор | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
||||
| `After` | `&После` | Код после вызова оригинального метода |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.py" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Перехват &Перед на сервере
|
||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
|
||||
# Перехват &После на клиенте
|
||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
||||
|
||||
# ИзменениеИКонтроль для функции
|
||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
||||
```
|
||||
|
||||
## Генерируемый код (Before)
|
||||
|
||||
```bsl
|
||||
&НаСервере
|
||||
&Перед("ПриЗаписи")
|
||||
Процедура Расш1_ПриЗаписи()
|
||||
// TODO: код перед вызовом оригинального метода
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
@@ -1,201 +1,209 @@
|
||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ModulePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$MethodName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("Before","After","ModificationAndControl")]
|
||||
[string]$InterceptorType,
|
||||
|
||||
[string]$Context = "НаСервере",
|
||||
|
||||
[switch]$IsFunction
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve extension path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) {
|
||||
$ExtensionPath = Split-Path $ExtensionPath -Parent
|
||||
}
|
||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (-not (Test-Path $cfgFile)) {
|
||||
Write-Error "Configuration.xml not found in: $ExtensionPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Read NamePrefix from Configuration.xml ---
|
||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgDoc.PreserveWhitespace = $false
|
||||
$cfgDoc.Load($cfgFile)
|
||||
|
||||
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
|
||||
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
||||
|
||||
# --- Map ModulePath to file path ---
|
||||
# ModulePath formats:
|
||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
||||
|
||||
$typeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
||||
}
|
||||
|
||||
$parts = $ModulePath.Split(".")
|
||||
if ($parts.Count -lt 2) {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
|
||||
if (-not $typeDirMap.ContainsKey($objType)) {
|
||||
Write-Error "Unknown object type: $objType"
|
||||
exit 1
|
||||
}
|
||||
$dirName = $typeDirMap[$objType]
|
||||
|
||||
$bslFile = $null
|
||||
if ($objType -eq "CommonModule") {
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
||||
$formName = $parts[3]
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 3) {
|
||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
||||
$moduleName = $parts[2]
|
||||
$moduleFileName = switch ($moduleName) {
|
||||
"ObjectModule" { "ObjectModule.bsl" }
|
||||
"ManagerModule" { "ManagerModule.bsl" }
|
||||
"RecordSetModule" { "RecordSetModule.bsl" }
|
||||
"CommandModule" { "CommandModule.bsl" }
|
||||
default { "$moduleName.bsl" }
|
||||
}
|
||||
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
|
||||
} else {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Map InterceptorType to decorator ---
|
||||
$decorator = switch ($InterceptorType) {
|
||||
"Before" { "&Перед" }
|
||||
"After" { "&После" }
|
||||
"ModificationAndControl" { "&ИзменениеИКонтроль" }
|
||||
}
|
||||
|
||||
# --- Map Context to annotation ---
|
||||
$contextAnnotation = switch ($Context) {
|
||||
"НаСервере" { "&НаСервере" }
|
||||
"НаКлиенте" { "&НаКлиенте" }
|
||||
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
|
||||
default { "&$Context" }
|
||||
}
|
||||
|
||||
# --- Procedure name ---
|
||||
$procName = "${namePrefix}${MethodName}"
|
||||
|
||||
# --- Generate BSL code ---
|
||||
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
|
||||
$bodyLines = @()
|
||||
switch ($InterceptorType) {
|
||||
"Before" {
|
||||
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
|
||||
}
|
||||
"After" {
|
||||
$bodyLines += "`t// TODO: код после вызова оригинального метода"
|
||||
}
|
||||
"ModificationAndControl" {
|
||||
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
|
||||
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
|
||||
}
|
||||
}
|
||||
|
||||
if ($IsFunction) {
|
||||
$bodyLines += "`t"
|
||||
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
|
||||
}
|
||||
|
||||
$bslCode = @()
|
||||
$bslCode += "$contextAnnotation"
|
||||
$bslCode += "${decorator}(`"$MethodName`")"
|
||||
$bslCode += "$keyword ${procName}()"
|
||||
$bslCode += $bodyLines
|
||||
$bslCode += "$endKeyword"
|
||||
|
||||
$bslText = ($bslCode -join "`r`n") + "`r`n"
|
||||
|
||||
# --- Check form borrowing for .Form. paths ---
|
||||
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
$formName = $parts[3]
|
||||
$dirName = $typeDirMap[$objType]
|
||||
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
|
||||
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||
|
||||
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
|
||||
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
|
||||
Write-Host " Run /cfe-borrow first:"
|
||||
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check if file exists and append ---
|
||||
$bslDir = Split-Path $bslFile -Parent
|
||||
if (-not (Test-Path $bslDir)) {
|
||||
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
if (Test-Path $bslFile) {
|
||||
# Append to existing file
|
||||
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
|
||||
$separator = "`r`n"
|
||||
if ($existing -and -not $existing.EndsWith("`n")) {
|
||||
$separator = "`r`n`r`n"
|
||||
}
|
||||
$newContent = $existing + $separator + $bslText
|
||||
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
|
||||
Write-Host "[OK] Добавлен перехватчик в существующий файл"
|
||||
} else {
|
||||
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
|
||||
Write-Host "[OK] Создан файл модуля"
|
||||
}
|
||||
|
||||
Write-Host " Файл: $bslFile"
|
||||
Write-Host " Декоратор: $decorator(`"$MethodName`")"
|
||||
Write-Host " Процедура: ${procName}()"
|
||||
Write-Host " Контекст: $contextAnnotation"
|
||||
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ModulePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$MethodName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("Before","After","ModificationAndControl")]
|
||||
[string]$InterceptorType,
|
||||
|
||||
[string]$Context = "НаСервере",
|
||||
|
||||
[switch]$IsFunction
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve extension path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) {
|
||||
$ExtensionPath = Split-Path $ExtensionPath -Parent
|
||||
}
|
||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (-not (Test-Path $cfgFile)) {
|
||||
Write-Error "Configuration.xml not found in: $ExtensionPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Read NamePrefix from Configuration.xml ---
|
||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgDoc.PreserveWhitespace = $false
|
||||
$cfgDoc.Load($cfgFile)
|
||||
|
||||
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
|
||||
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
||||
|
||||
# --- Map ModulePath to file path ---
|
||||
# ModulePath formats:
|
||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
||||
|
||||
$typeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
||||
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
|
||||
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
|
||||
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
|
||||
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
|
||||
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
|
||||
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
|
||||
}
|
||||
|
||||
$parts = $ModulePath.Split(".")
|
||||
if ($parts.Count -lt 2) {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
|
||||
if (-not $typeDirMap.ContainsKey($objType)) {
|
||||
Write-Error "Unknown object type: $objType"
|
||||
exit 1
|
||||
}
|
||||
$dirName = $typeDirMap[$objType]
|
||||
|
||||
$bslFile = $null
|
||||
if ($objType -eq "CommonModule") {
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
||||
$formName = $parts[3]
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 3) {
|
||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
||||
$moduleName = $parts[2]
|
||||
$moduleFileName = switch ($moduleName) {
|
||||
"ObjectModule" { "ObjectModule.bsl" }
|
||||
"ManagerModule" { "ManagerModule.bsl" }
|
||||
"RecordSetModule" { "RecordSetModule.bsl" }
|
||||
"CommandModule" { "CommandModule.bsl" }
|
||||
default { "$moduleName.bsl" }
|
||||
}
|
||||
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
|
||||
} else {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Map InterceptorType to decorator ---
|
||||
$decorator = switch ($InterceptorType) {
|
||||
"Before" { "&Перед" }
|
||||
"After" { "&После" }
|
||||
"ModificationAndControl" { "&ИзменениеИКонтроль" }
|
||||
}
|
||||
|
||||
# --- Map Context to annotation ---
|
||||
$contextAnnotation = switch ($Context) {
|
||||
"НаСервере" { "&НаСервере" }
|
||||
"НаКлиенте" { "&НаКлиенте" }
|
||||
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
|
||||
default { "&$Context" }
|
||||
}
|
||||
|
||||
# --- Procedure name ---
|
||||
$procName = "${namePrefix}${MethodName}"
|
||||
|
||||
# --- Generate BSL code ---
|
||||
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
|
||||
$bodyLines = @()
|
||||
switch ($InterceptorType) {
|
||||
"Before" {
|
||||
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
|
||||
}
|
||||
"After" {
|
||||
$bodyLines += "`t// TODO: код после вызова оригинального метода"
|
||||
}
|
||||
"ModificationAndControl" {
|
||||
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
|
||||
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
|
||||
}
|
||||
}
|
||||
|
||||
if ($IsFunction) {
|
||||
$bodyLines += "`t"
|
||||
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
|
||||
}
|
||||
|
||||
$bslCode = @()
|
||||
$bslCode += "$contextAnnotation"
|
||||
$bslCode += "${decorator}(`"$MethodName`")"
|
||||
$bslCode += "$keyword ${procName}()"
|
||||
$bslCode += $bodyLines
|
||||
$bslCode += "$endKeyword"
|
||||
|
||||
$bslText = ($bslCode -join "`r`n") + "`r`n"
|
||||
|
||||
# --- Check form borrowing for .Form. paths ---
|
||||
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
$formName = $parts[3]
|
||||
$dirName = $typeDirMap[$objType]
|
||||
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
|
||||
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||
|
||||
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
|
||||
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
|
||||
Write-Host " Run /cfe-borrow first:"
|
||||
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check if file exists and append ---
|
||||
$bslDir = Split-Path $bslFile -Parent
|
||||
if (-not (Test-Path $bslDir)) {
|
||||
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
if (Test-Path $bslFile) {
|
||||
# Append to existing file
|
||||
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
|
||||
$separator = "`r`n"
|
||||
if ($existing -and -not $existing.EndsWith("`n")) {
|
||||
$separator = "`r`n`r`n"
|
||||
}
|
||||
$newContent = $existing + $separator + $bslText
|
||||
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
|
||||
Write-Host "[OK] Добавлен перехватчик в существующий файл"
|
||||
} else {
|
||||
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
|
||||
Write-Host "[OK] Создан файл модуля"
|
||||
}
|
||||
|
||||
Write-Host " Файл: $bslFile"
|
||||
Write-Host " Декоратор: $decorator(`"$MethodName`")"
|
||||
Write-Host " Процедура: ${procName}()"
|
||||
Write-Host " Контекст: $contextAnnotation"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -84,6 +84,22 @@ def main():
|
||||
"AccumulationRegister": "AccumulationRegisters",
|
||||
"AccountingRegister": "AccountingRegisters",
|
||||
"CalculationRegister": "CalculationRegisters",
|
||||
"Catalogs": "Catalogs",
|
||||
"Documents": "Documents",
|
||||
"Enums": "Enums",
|
||||
"CommonModules": "CommonModules",
|
||||
"Reports": "Reports",
|
||||
"DataProcessors": "DataProcessors",
|
||||
"ExchangePlans": "ExchangePlans",
|
||||
"ChartsOfAccounts": "ChartsOfAccounts",
|
||||
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||
"BusinessProcesses": "BusinessProcesses",
|
||||
"Tasks": "Tasks",
|
||||
"InformationRegisters": "InformationRegisters",
|
||||
"AccumulationRegisters": "AccumulationRegisters",
|
||||
"AccountingRegisters": "AccountingRegisters",
|
||||
"CalculationRegisters": "CalculationRegisters",
|
||||
}
|
||||
|
||||
parts = module_path.split(".")
|
||||
|
||||
@@ -1,49 +1,29 @@
|
||||
---
|
||||
name: cfe-validate
|
||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|-------------------------------------------------|
|
||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
|
||||
```
|
||||
|
||||
## Проверки (13 шагов)
|
||||
|
||||
| # | Проверка | Уровень |
|
||||
|---|----------|---------|
|
||||
| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR |
|
||||
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR |
|
||||
| 3 | Extension properties: ObjectBelonging=Adopted, Name, Purpose, NamePrefix, KeepMapping | ERROR |
|
||||
| 4 | Enum-значения: ConfigurationExtensionCompatibilityMode, DefaultRunMode, ScriptVariant, InterfaceCompatibilityMode | ERROR |
|
||||
| 5 | ChildObjects: валидные типы (44), нет дубликатов, каноничный порядок | ERROR/WARN |
|
||||
| 6 | DefaultLanguage ссылается на Language в ChildObjects | ERROR |
|
||||
| 7 | Файлы языков существуют | WARN |
|
||||
| 8 | Каталоги объектов существуют | WARN |
|
||||
| 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN |
|
||||
| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR |
|
||||
| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN |
|
||||
| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN |
|
||||
| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
---
|
||||
name: cfe-validate
|
||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|-------------------------------------------------|
|
||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src"
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||
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_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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||
'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'
|
||||
@@ -147,7 +150,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
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('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
@@ -213,8 +216,8 @@ def main():
|
||||
version = root.get('version', '')
|
||||
if not version:
|
||||
r.warn('1. Missing version attribute on MetaDataObject')
|
||||
elif version not in ('2.17', '2.20'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||
elif version not in ('2.17', '2.20', '2.21'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||
|
||||
# Must have Configuration child
|
||||
cfg_node = None
|
||||
|
||||
@@ -1,78 +1,70 @@
|
||||
---
|
||||
name: db-create
|
||||
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
|
||||
argument-hint: <path|name>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-create — Создание информационной базы
|
||||
|
||||
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-create <path> — файловая база по указанному пути
|
||||
/db-create <server>/<name> — серверная база
|
||||
/db-create — интерактивно
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
||||
| `-AddToList` | нет | Добавить в список баз 1С |
|
||||
| `-ListName <имя>` | нет | Имя базы в списке |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После создания
|
||||
|
||||
1. Прочитай лог-файл и покажи результат
|
||||
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Создать файловую базу
|
||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||
|
||||
# Создать серверную базу
|
||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||
|
||||
# Создать из шаблона CF
|
||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -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 "Новая база"
|
||||
```
|
||||
---
|
||||
name: db-create
|
||||
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
|
||||
argument-hint: <path|name>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-create — Создание информационной базы
|
||||
|
||||
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-create <path> — файловая база по указанному пути
|
||||
/db-create <server>/<name> — серверная база
|
||||
/db-create — интерактивно
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
||||
| `-AddToList` | нет | Добавить в список баз 1С |
|
||||
| `-ListName <имя>` | нет | Имя базы в списке |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## После создания
|
||||
|
||||
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Создать файловую базу
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
||||
|
||||
# Создать серверную базу
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||
|
||||
# Создать из шаблона CF
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||
|
||||
# Создать и добавить в список баз
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||
```
|
||||
|
||||
@@ -1,163 +1,249 @@
|
||||
# db-create v1.0 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Создание информационной базы 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Создаёт новую информационную базу 1С (файловую или серверную).
|
||||
Поддерживает создание из шаблона и добавление в список баз.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UseTemplate
|
||||
Путь к файлу шаблона (.cf или .dt)
|
||||
|
||||
.PARAMETER AddToList
|
||||
Добавить в список баз 1С
|
||||
|
||||
.PARAMETER ListName
|
||||
Имя базы в списке
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
||||
#>
|
||||
|
||||
[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]$UseTemplate,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AddToList,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListName
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate template ---
|
||||
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
|
||||
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("CREATEINFOBASE")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "File=`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
# --- Template ---
|
||||
if ($UseTemplate) {
|
||||
$arguments += "/UseTemplate", "`"$UseTemplate`""
|
||||
}
|
||||
|
||||
# --- Add to list ---
|
||||
if ($AddToList) {
|
||||
if ($ListName) {
|
||||
$arguments += "/AddToList", "`"$ListName`""
|
||||
} else {
|
||||
$arguments += "/AddToList"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "create_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) {
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "Error creating 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
|
||||
}
|
||||
}
|
||||
# db-create v1.6 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Создание информационной базы 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Создаёт новую информационную базу 1С (файловую или серверную).
|
||||
Поддерживает создание из шаблона и добавление в список баз.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UseTemplate
|
||||
Путь к файлу шаблона (.cf или .dt)
|
||||
|
||||
.PARAMETER AddToList
|
||||
Добавить в список баз 1С
|
||||
|
||||
.PARAMETER ListName
|
||||
Имя базы в списке
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
||||
#>
|
||||
|
||||
[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]$UseTemplate,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AddToList,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListName
|
||||
)
|
||||
|
||||
$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 template ---
|
||||
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
|
||||
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$arguments = @("CREATEINFOBASE")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "File=`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
# --- Template ---
|
||||
if ($UseTemplate) {
|
||||
$arguments += "/UseTemplate", "`"$UseTemplate`""
|
||||
}
|
||||
|
||||
# --- Add to list ---
|
||||
if ($AddToList) {
|
||||
if ($ListName) {
|
||||
$arguments += "/AddToList", "`"$ListName`""
|
||||
} else {
|
||||
$arguments += "/AddToList"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "create_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) {
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "Error creating 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,104 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -47,9 +116,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -58,6 +132,29 @@ def main():
|
||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
@@ -67,9 +164,11 @@ def main():
|
||||
arguments = ["CREATEINFOBASE"]
|
||||
|
||||
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:
|
||||
arguments.append(f'File="{args.InfoBasePath}"')
|
||||
arguments.append(f'File={args.InfoBasePath}')
|
||||
|
||||
# --- Template ---
|
||||
if args.UseTemplate:
|
||||
|
||||
@@ -1,79 +1,68 @@
|
||||
---
|
||||
name: db-dump-cf
|
||||
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
||||
argument-hint: "[database] [output.cf]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-dump-cf — Выгрузка конфигурации в CF-файл
|
||||
|
||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-dump-cf [database] [output.cf]
|
||||
/db-dump-cf dev config.cf
|
||||
/db-dump-cf — база по умолчанию, файл config.cf
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
|
||||
|
||||
## Примеры
|
||||
|
||||
```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"
|
||||
|
||||
# Серверная база
|
||||
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"
|
||||
|
||||
# Выгрузка расширения
|
||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
```
|
||||
---
|
||||
name: db-dump-cf
|
||||
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
||||
argument-hint: "[database] [output.cf]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-dump-cf — Выгрузка конфигурации в CF-файл
|
||||
|
||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-dump-cf [database] [output.cf]
|
||||
/db-dump-cf dev config.cf
|
||||
/db-dump-cf — база по умолчанию, файл config.cf
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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-cf.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Выгрузка конфигурации (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||
|
||||
# Серверная база
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||
|
||||
# Выгрузка расширения
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -1,166 +1,253 @@
|
||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в CF-файл
|
||||
|
||||
.DESCRIPTION
|
||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
||||
Поддерживает выгрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному CF-файлу
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-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_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/DumpCfg", "`"$OutputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_cf_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 "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (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
|
||||
}
|
||||
}
|
||||
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в CF-файл
|
||||
|
||||
.DESCRIPTION
|
||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
||||
Поддерживает выгрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному CF-файлу
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$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_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/DumpCfg", "`"$OutputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_cf_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 "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,104 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -49,9 +118,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -60,6 +134,34 @@ def main():
|
||||
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":
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||
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,97 +1,90 @@
|
||||
---
|
||||
name: db-dump-xml
|
||||
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
||||
argument-hint: "[database] [outputDir]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-dump-xml — Выгрузка конфигурации в XML
|
||||
|
||||
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-dump-xml [database] [outputDir]
|
||||
/db-dump-xml dev src/config
|
||||
/db-dump-xml dev src/config -Mode Full
|
||||
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-ConfigDir <путь>` | да | Каталог для выгрузки |
|
||||
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
|
||||
| `-Objects <список>` | для Partial | Имена объектов через запятую |
|
||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Режимы выгрузки
|
||||
|
||||
| Режим | Описание |
|
||||
|-------|----------|
|
||||
| `Full` | Полная выгрузка — все объекты конфигурации |
|
||||
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
|
||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||
|
||||
## Примеры
|
||||
|
||||
```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
|
||||
|
||||
# Инкрементальная выгрузка
|
||||
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
|
||||
|
||||
# Частичная выгрузка
|
||||
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 "Справочник.Номенклатура,Документ.Заказ"
|
||||
|
||||
# Серверная база
|
||||
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
|
||||
|
||||
# Выгрузка расширения
|
||||
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 "МоёРасширение"
|
||||
```
|
||||
---
|
||||
name: db-dump-xml
|
||||
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
||||
argument-hint: "[database] [outputDir]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-dump-xml — Выгрузка конфигурации в XML
|
||||
|
||||
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-dump-xml [database] [outputDir]
|
||||
/db-dump-xml dev src/config
|
||||
/db-dump-xml dev src/config -Mode Full
|
||||
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-ConfigDir <путь>` | да | Каталог для выгрузки |
|
||||
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
|
||||
| `-Objects <список>` | для Partial | Имена объектов через запятую |
|
||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Режимы выгрузки
|
||||
|
||||
| Режим | Описание |
|
||||
|-------|----------|
|
||||
| `Full` | Полная выгрузка — все объекты конфигурации |
|
||||
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
|
||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||
|
||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Полная выгрузка (файловая база)
|
||||
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
|
||||
|
||||
# Инкрементальная выгрузка
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -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 Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
|
||||
# Серверная база
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||
|
||||
# Выгрузка расширения
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -1,224 +1,323 @@
|
||||
# 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
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в XML-файлы
|
||||
|
||||
.DESCRIPTION
|
||||
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
|
||||
- Full: полная выгрузка всей конфигурации
|
||||
- Changes: инкрементальная выгрузка изменённых объектов
|
||||
- Partial: выгрузка конкретных объектов из списка
|
||||
- UpdateInfo: обновление только ConfigDumpInfo.xml
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог для выгрузки конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
|
||||
|
||||
.PARAMETER Objects
|
||||
Имена объектов метаданных через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
||||
[string]$Mode = "Changes",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Objects,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
||||
Write-Host "Created output directory: $ConfigDir"
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/DumpConfigToFiles", "`"$ConfigDir`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
switch ($Mode) {
|
||||
"Full" {
|
||||
Write-Host "Executing full configuration dump..."
|
||||
}
|
||||
"Changes" {
|
||||
Write-Host "Executing incremental configuration dump..."
|
||||
$arguments += "-update"
|
||||
$arguments += "-force"
|
||||
}
|
||||
"Partial" {
|
||||
Write-Host "Executing partial configuration dump..."
|
||||
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
|
||||
$listFile = Join-Path $tempDir "dump_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
|
||||
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
Write-Host "Objects to dump: $($objectList.Count)"
|
||||
foreach ($obj in $objectList) { Write-Host " $obj" }
|
||||
}
|
||||
"UpdateInfo" {
|
||||
Write-Host "Updating ConfigDumpInfo.xml..."
|
||||
$arguments += "-configDumpInfoOnly"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully" -ForegroundColor Green
|
||||
Write-Host "Configuration dumped to: $ConfigDir"
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (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
|
||||
}
|
||||
}
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в XML-файлы
|
||||
|
||||
.DESCRIPTION
|
||||
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
|
||||
- Full: полная выгрузка всей конфигурации
|
||||
- Changes: инкрементальная выгрузка изменённых объектов
|
||||
- Partial: выгрузка конкретных объектов из списка
|
||||
- UpdateInfo: обновление только ConfigDumpInfo.xml
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог для выгрузки конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
|
||||
|
||||
.PARAMETER Objects
|
||||
Имена объектов метаданных через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
||||
[string]$Mode = "Changes",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Objects,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$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 Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
||||
Write-Host "Created output directory: $ConfigDir"
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/DumpConfigToFiles", "`"$ConfigDir`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
switch ($Mode) {
|
||||
"Full" {
|
||||
Write-Host "Executing full configuration dump..."
|
||||
}
|
||||
"Changes" {
|
||||
Write-Host "Executing incremental configuration dump..."
|
||||
$arguments += "-update"
|
||||
$arguments += "-force"
|
||||
}
|
||||
"Partial" {
|
||||
Write-Host "Executing partial configuration dump..."
|
||||
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
|
||||
$listFile = Join-Path $tempDir "dump_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
|
||||
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
Write-Host "Objects to dump: $($objectList.Count)"
|
||||
foreach ($obj in $objectList) { Write-Host " $obj" }
|
||||
}
|
||||
"UpdateInfo" {
|
||||
Write-Host "Updating ConfigDumpInfo.xml..."
|
||||
$arguments += "-configDumpInfoOnly"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully" -ForegroundColor Green
|
||||
Write-Host "Configuration dumped to: $ConfigDir"
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,104 @@
|
||||
#!/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
|
||||
|
||||
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 1cv8.exe."""
|
||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
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:
|
||||
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)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -65,9 +132,14 @@ def main():
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -81,6 +153,46 @@ def main():
|
||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
+158
-158
@@ -1,158 +1,158 @@
|
||||
---
|
||||
name: db-list
|
||||
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
|
||||
argument-hint: "[add|remove|show]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-list — Управление реестром баз данных
|
||||
|
||||
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-list — показать список баз
|
||||
/db-list add — добавить базу (интерактивно)
|
||||
/db-list remove <id> — удалить базу из реестра
|
||||
/db-list show <id|alias> — подробности по базе
|
||||
```
|
||||
|
||||
## Формат `.v8-project.json`
|
||||
|
||||
Файл размещается в корне проекта (рядом с `.git/`).
|
||||
|
||||
```json
|
||||
{
|
||||
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
||||
"databases": [
|
||||
{
|
||||
"id": "dev",
|
||||
"name": "Разработка",
|
||||
"type": "file",
|
||||
"path": "C:\\Bases\\MyApp_Dev",
|
||||
"user": "Admin",
|
||||
"password": "",
|
||||
"aliases": ["dev", "разработка"],
|
||||
"branches": ["dev", "develop", "feature/*"],
|
||||
"configSrc": "C:\\WS\\myapp\\cfsrc"
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
"name": "Тестовая",
|
||||
"type": "server",
|
||||
"server": "srv01",
|
||||
"ref": "MyApp_Test",
|
||||
"user": "Admin",
|
||||
"password": "123",
|
||||
"aliases": ["test", "тест"]
|
||||
}
|
||||
],
|
||||
"default": "dev"
|
||||
}
|
||||
```
|
||||
|
||||
### Поля корневого объекта
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||
| `databases` | array | Массив баз данных |
|
||||
| `default` | string | id базы по умолчанию |
|
||||
|
||||
### Поля объекта базы данных
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|:------------:|----------|
|
||||
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
|
||||
| `name` | string | да | Человекочитаемое имя |
|
||||
| `type` | `"file"` / `"server"` | да | Тип подключения |
|
||||
| `path` | string | для file | Путь к каталогу файловой базы |
|
||||
| `server` | string | для server | Адрес сервера 1С |
|
||||
| `ref` | string | для server | Имя базы на сервере |
|
||||
| `user` | string | нет | Имя пользователя 1С |
|
||||
| `password` | string | нет | Пароль |
|
||||
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
|
||||
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
|
||||
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
|
||||
|
||||
## Алгоритм разрешения базы данных
|
||||
|
||||
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
|
||||
|
||||
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
|
||||
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
|
||||
1. По `id` (точное совпадение)
|
||||
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
|
||||
3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
|
||||
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
|
||||
- Точное совпадение: ветка `dev` → `"branches": ["dev"]`
|
||||
- Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
5. Если не найдено или неоднозначно — спроси пользователя
|
||||
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
|
||||
|
||||
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
|
||||
|
||||
### Автоопределение платформы
|
||||
|
||||
Если `v8path` не задан в конфиге:
|
||||
|
||||
```powershell
|
||||
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
### Показать список баз
|
||||
|
||||
Прочитай `.v8-project.json`, выведи таблицу:
|
||||
|
||||
```
|
||||
ID Имя Тип Путь/Сервер По умолч.
|
||||
dev Разработка file C:\Bases\MyApp_Dev ✓
|
||||
test Тестовая server srv01/MyApp_Test
|
||||
```
|
||||
|
||||
### Добавить базу
|
||||
|
||||
Спроси у пользователя через AskUserQuestion:
|
||||
- id, name, type (file/server)
|
||||
- path (для file) или server + ref (для server)
|
||||
- user, password (необязательно)
|
||||
- aliases, branches (необязательно)
|
||||
|
||||
Добавь в массив `databases`. Если это первая база — установи как `default`.
|
||||
|
||||
### Удалить базу
|
||||
|
||||
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
|
||||
|
||||
### Подробности по базе
|
||||
|
||||
Выведи все поля конкретной базы.
|
||||
|
||||
## Формирование строки подключения
|
||||
|
||||
Для использования в шаблонах команд других навыков:
|
||||
|
||||
**Файловая база:**
|
||||
```
|
||||
/F "<path>"
|
||||
```
|
||||
|
||||
**Серверная база:**
|
||||
```
|
||||
/S "<server>/<ref>"
|
||||
```
|
||||
|
||||
**Аутентификация** (добавляется если user задан):
|
||||
```
|
||||
/N"<user>" /P"<password>"
|
||||
```
|
||||
|
||||
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
|
||||
---
|
||||
name: db-list
|
||||
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
|
||||
argument-hint: "[add|remove|show]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-list — Управление реестром баз данных
|
||||
|
||||
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-list — показать список баз
|
||||
/db-list add — добавить базу (интерактивно)
|
||||
/db-list remove <id> — удалить базу из реестра
|
||||
/db-list show <id|alias> — подробности по базе
|
||||
```
|
||||
|
||||
## Формат `.v8-project.json`
|
||||
|
||||
Файл размещается в корне проекта (рядом с `.git/`).
|
||||
|
||||
```json
|
||||
{
|
||||
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
||||
"databases": [
|
||||
{
|
||||
"id": "dev",
|
||||
"name": "Разработка",
|
||||
"type": "file",
|
||||
"path": "C:\\Bases\\MyApp_Dev",
|
||||
"user": "Admin",
|
||||
"password": "",
|
||||
"aliases": ["dev", "разработка"],
|
||||
"branches": ["dev", "develop", "feature/*"],
|
||||
"configSrc": "C:\\WS\\myapp\\cfsrc"
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
"name": "Тестовая",
|
||||
"type": "server",
|
||||
"server": "srv01",
|
||||
"ref": "MyApp_Test",
|
||||
"user": "Admin",
|
||||
"password": "123",
|
||||
"aliases": ["test", "тест"]
|
||||
}
|
||||
],
|
||||
"default": "dev"
|
||||
}
|
||||
```
|
||||
|
||||
### Поля корневого объекта
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||
| `databases` | array | Массив баз данных |
|
||||
| `default` | string | id базы по умолчанию |
|
||||
|
||||
### Поля объекта базы данных
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|:------------:|----------|
|
||||
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
|
||||
| `name` | string | да | Человекочитаемое имя |
|
||||
| `type` | `"file"` / `"server"` | да | Тип подключения |
|
||||
| `path` | string | для file | Путь к каталогу файловой базы |
|
||||
| `server` | string | для server | Адрес сервера 1С |
|
||||
| `ref` | string | для server | Имя базы на сервере |
|
||||
| `user` | string | нет | Имя пользователя 1С |
|
||||
| `password` | string | нет | Пароль |
|
||||
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
|
||||
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
|
||||
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
|
||||
|
||||
## Алгоритм разрешения базы данных
|
||||
|
||||
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
|
||||
|
||||
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
|
||||
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
|
||||
1. По `id` (точное совпадение)
|
||||
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
|
||||
3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
|
||||
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
|
||||
- Точное совпадение: ветка `dev` → `"branches": ["dev"]`
|
||||
- Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
5. Если не найдено или неоднозначно — спроси пользователя
|
||||
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
|
||||
|
||||
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
|
||||
|
||||
### Автоопределение платформы
|
||||
|
||||
Если `v8path` не задан в конфиге:
|
||||
|
||||
```powershell
|
||||
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
### Показать список баз
|
||||
|
||||
Прочитай `.v8-project.json`, выведи таблицу:
|
||||
|
||||
```
|
||||
ID Имя Тип Путь/Сервер По умолч.
|
||||
dev Разработка file C:\Bases\MyApp_Dev ✓
|
||||
test Тестовая server srv01/MyApp_Test
|
||||
```
|
||||
|
||||
### Добавить базу
|
||||
|
||||
Спроси у пользователя через AskUserQuestion:
|
||||
- id, name, type (file/server)
|
||||
- path (для file) или server + ref (для server)
|
||||
- user, password (необязательно)
|
||||
- aliases, branches (необязательно)
|
||||
|
||||
Добавь в массив `databases`. Если это первая база — установи как `default`.
|
||||
|
||||
### Удалить базу
|
||||
|
||||
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
|
||||
|
||||
### Подробности по базе
|
||||
|
||||
Выведи все поля конкретной базы.
|
||||
|
||||
## Формирование строки подключения
|
||||
|
||||
Для использования в шаблонах команд других навыков:
|
||||
|
||||
**Файловая база:**
|
||||
```
|
||||
/F "<path>"
|
||||
```
|
||||
|
||||
**Серверная база:**
|
||||
```
|
||||
/S "<server>/<ref>"
|
||||
```
|
||||
|
||||
**Аутентификация** (добавляется если user задан):
|
||||
```
|
||||
/N"<user>" /P"<password>"
|
||||
```
|
||||
|
||||
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
|
||||
|
||||
@@ -1,81 +1,73 @@
|
||||
---
|
||||
name: db-load-cf
|
||||
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
|
||||
argument-hint: <input.cf> [database]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-load-cf — Загрузка конфигурации из CF-файла
|
||||
|
||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-load-cf <input.cf> [database]
|
||||
/db-load-cf config.cf dev
|
||||
```
|
||||
|
||||
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Прочитай лог-файл и покажи результат
|
||||
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||
|
||||
## Примеры
|
||||
|
||||
```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"
|
||||
|
||||
# Серверная база
|
||||
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"
|
||||
|
||||
# Загрузка расширения
|
||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
```
|
||||
---
|
||||
name: db-load-cf
|
||||
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
|
||||
argument-hint: <input.cf> [database]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-load-cf — Загрузка конфигурации из CF-файла
|
||||
|
||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-load-cf <input.cf> [database]
|
||||
/db-load-cf config.cf dev
|
||||
```
|
||||
|
||||
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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-cf.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## После выполнения
|
||||
|
||||
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Файловая база
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||
|
||||
# Серверная база
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||
|
||||
# Загрузка расширения
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -1,166 +1,253 @@
|
||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из CF-файла
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
||||
Поддерживает загрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к CF-файлу для загрузки
|
||||
|
||||
.PARAMETER Extension
|
||||
Загрузить как расширение
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения из архива
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-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_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/LoadCfg", "`"$InputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_cf_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 "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (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
|
||||
}
|
||||
}
|
||||
# db-load-cf v1.6 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из CF-файла
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
||||
Поддерживает загрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к CF-файлу для загрузки
|
||||
|
||||
.PARAMETER Extension
|
||||
Загрузить как расширение
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения из архива
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$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_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/LoadCfg", "`"$InputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_cf_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 "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,104 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -49,9 +118,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -60,6 +134,34 @@ def main():
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||
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
|
||||
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
||||
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
||||
argument-hint: "[database] [source]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
@@ -30,7 +30,7 @@ allowed-tools:
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
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`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||
@@ -38,14 +38,14 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```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 <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
@@ -64,15 +64,14 @@ powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.p
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Показать список загруженных файлов и результат из лога
|
||||
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||
|
||||
## Примеры
|
||||
|
||||
```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,359 +1,477 @@
|
||||
# db-load-git v1.3 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка изменений из Git в базу 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Определяет изменённые файлы конфигурации по данным Git и выполняет
|
||||
частичную загрузку в информационную базу.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-выгрузки конфигурации (git-репозиторий)
|
||||
|
||||
.PARAMETER Source
|
||||
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
|
||||
|
||||
.PARAMETER CommitRange
|
||||
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.PARAMETER DryRun
|
||||
Только показать что будет загружено (без загрузки)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
|
||||
[string]$Source = "All",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CommitRange,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$DryRun,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||
function Get-ObjectXmlFromSubFile {
|
||||
param([string]$RelativePath)
|
||||
|
||||
$parts = $RelativePath -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
return "$($parts[0])/$($parts[1]).xml"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Resolve V8Path (skip if DryRun) ---
|
||||
if (-not $DryRun) {
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Validate connection (skip if DryRun) ---
|
||||
if (-not $DryRun) {
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Validate config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Commit mode ---
|
||||
if ($Source -eq "Commit" -and -not $CommitRange) {
|
||||
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Check git ---
|
||||
try {
|
||||
$null = git --version 2>&1
|
||||
} catch {
|
||||
Write-Host "Error: git not found in PATH" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Get changed files from Git ---
|
||||
$changedFiles = @()
|
||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||
|
||||
Push-Location $ConfigDir
|
||||
try {
|
||||
switch ($Source) {
|
||||
"Staged" {
|
||||
Write-Host "Getting staged changes..."
|
||||
$raw = git diff --cached --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"Unstaged" {
|
||||
Write-Host "Getting unstaged changes..."
|
||||
$raw = git diff --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git ls-files --others --exclude-standard 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"Commit" {
|
||||
Write-Host "Getting changes from $CommitRange..."
|
||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"All" {
|
||||
Write-Host "Getting all uncommitted changes..."
|
||||
$raw = git diff --cached --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git diff --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git ls-files --others --exclude-standard 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
|
||||
if ($changedFiles.Count -eq 0) {
|
||||
Write-Host "No changes found"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Git changes detected: $($changedFiles.Count) files"
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
$configFiles = @()
|
||||
|
||||
foreach ($file in $changedFiles) {
|
||||
$file = $file.Trim().Replace('\', '/')
|
||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||
|
||||
# Skip service files
|
||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
||||
|
||||
$fullPath = Join-Path $ConfigDir $file
|
||||
|
||||
if ($file -match '\.xml$') {
|
||||
# XML file — add directly if exists
|
||||
if (Test-Path $fullPath) {
|
||||
if ($configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
||||
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
|
||||
if ($objectXml) {
|
||||
$fullXmlPath = Join-Path $ConfigDir $objectXml
|
||||
if (Test-Path $fullXmlPath) {
|
||||
if ($configFiles -notcontains $objectXml) {
|
||||
$configFiles += $objectXml
|
||||
}
|
||||
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
|
||||
# Add all files from Ext/ directory of the object
|
||||
$parts = $file -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
|
||||
if (Test-Path $extDir) {
|
||||
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
|
||||
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
|
||||
if ($configFiles -notcontains $extRelPath) {
|
||||
$configFiles += $extRelPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($configFiles.Count -eq 0) {
|
||||
Write-Host "No configuration files found in changes"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Files for loading: $($configFiles.Count)"
|
||||
foreach ($f in $configFiles) { Write-Host " $f" }
|
||||
|
||||
# --- DryRun: stop here ---
|
||||
if ($DryRun) {
|
||||
Write-Host ""
|
||||
Write-Host "DryRun mode - no changes applied"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Write list file (UTF-8 with BOM) ---
|
||||
$listFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
||||
|
||||
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
$arguments += "-Format", $Format
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host ""
|
||||
Write-Host "Executing partial configuration load..."
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
Write-Host ""
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (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
|
||||
}
|
||||
}
|
||||
# db-load-git v1.11 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка изменений из Git в базу 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Определяет изменённые файлы конфигурации по данным Git и выполняет
|
||||
частичную загрузку в информационную базу.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-выгрузки конфигурации (git-репозиторий)
|
||||
|
||||
.PARAMETER Source
|
||||
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
|
||||
|
||||
.PARAMETER CommitRange
|
||||
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.PARAMETER DryRun
|
||||
Только показать что будет загружено (без загрузки)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
|
||||
[string]$Source = "All",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CommitRange,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$DryRun,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||
function Get-ObjectXmlFromSubFile {
|
||||
param([string]$RelativePath)
|
||||
|
||||
$parts = $RelativePath -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
return "$($parts[0])/$($parts[1]).xml"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Resolve V8Path (skip if 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) {
|
||||
$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 + validate connection (skip if DryRun) ---
|
||||
$engine = "1cv8"
|
||||
if (-not $DryRun) {
|
||||
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
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Validate config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Commit mode ---
|
||||
if ($Source -eq "Commit" -and -not $CommitRange) {
|
||||
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Check git ---
|
||||
try {
|
||||
$null = git --version 2>&1
|
||||
} catch {
|
||||
Write-Host "Error: git not found in PATH" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 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 = @()
|
||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||
|
||||
Push-Location $ConfigDir
|
||||
try {
|
||||
switch ($Source) {
|
||||
"Staged" {
|
||||
Write-Host "Getting staged changes..."
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||
}
|
||||
"Unstaged" {
|
||||
Write-Host "Getting unstaged changes..."
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||
}
|
||||
"Commit" {
|
||||
Write-Host "Getting changes from $CommitRange..."
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||
}
|
||||
"All" {
|
||||
Write-Host "Getting all uncommitted changes..."
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
|
||||
if ($changedFiles.Count -eq 0) {
|
||||
Write-Host "No changes found"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Git changes detected: $($changedFiles.Count) files"
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
$configFiles = @()
|
||||
$supportSkipped = @()
|
||||
|
||||
foreach ($file in $changedFiles) {
|
||||
$file = $file.Trim().Replace('\', '/')
|
||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||
|
||||
# Skip service files (not partially loadable). Support-state files are tracked
|
||||
# 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
|
||||
|
||||
if ($file -match '\.xml$') {
|
||||
# XML file — add directly if exists
|
||||
if (Test-Path $fullPath) {
|
||||
if ($configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
||||
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
|
||||
if ($objectXml) {
|
||||
$fullXmlPath = Join-Path $ConfigDir $objectXml
|
||||
if (Test-Path $fullXmlPath) {
|
||||
if ($configFiles -notcontains $objectXml) {
|
||||
$configFiles += $objectXml
|
||||
}
|
||||
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
|
||||
# Add all files from Ext/ directory of the object
|
||||
$parts = $file -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
|
||||
if (Test-Path $extDir) {
|
||||
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
|
||||
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
|
||||
if ($configFiles -notcontains $extRelPath) {
|
||||
$configFiles += $extRelPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Write-Host "No configuration files found in changes"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Files for loading: $($configFiles.Count)"
|
||||
foreach ($f in $configFiles) { Write-Host " $f" }
|
||||
|
||||
# --- DryRun: stop here ---
|
||||
if ($DryRun) {
|
||||
Write-Host ""
|
||||
Write-Host "DryRun mode - no changes applied"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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) ---
|
||||
$listFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
||||
|
||||
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
$arguments += "-Format", $Format
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host ""
|
||||
Write-Host "Executing partial configuration load..."
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
Write-Host ""
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
@@ -13,26 +15,90 @@ 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 1cv8.exe."""
|
||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
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:
|
||||
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)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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 get_object_xml_from_subfile(relative_path):
|
||||
"""Map sub-file path (BSL, HTML, etc.) to object XML 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):
|
||||
"""Run a git command in config_dir and return output lines on success."""
|
||||
result = subprocess.run(
|
||||
["git"] + git_args,
|
||||
["git", "-c", "core.quotePath=false"] + git_args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
@@ -93,9 +159,15 @@ def main():
|
||||
if not args.DryRun:
|
||||
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.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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -146,14 +218,19 @@ def main():
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
config_files = []
|
||||
support_skipped = []
|
||||
|
||||
for file in changed_files:
|
||||
file = file.strip().replace("\\", "/")
|
||||
if not file:
|
||||
continue
|
||||
|
||||
# Skip service files
|
||||
if file == "ConfigDumpInfo.xml":
|
||||
# Skip service files (not partially loadable). Support-state files are
|
||||
# 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
|
||||
|
||||
full_path = os.path.join(args.ConfigDir, file)
|
||||
@@ -186,6 +263,12 @@ def main():
|
||||
if rel_path not in config_files:
|
||||
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:
|
||||
print("No configuration files found in changes")
|
||||
sys.exit(0)
|
||||
@@ -205,6 +288,58 @@ def main():
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
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) ---
|
||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||
|
||||
+101
-109
@@ -1,109 +1,101 @@
|
||||
---
|
||||
name: db-load-xml
|
||||
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
||||
argument-hint: <configDir> [database]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-load-xml — Загрузка конфигурации из XML
|
||||
|
||||
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-load-xml <configDir> [database]
|
||||
/db-load-xml src/config dev
|
||||
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
```
|
||||
|
||||
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-ConfigDir <путь>` | да | Каталог XML-исходников |
|
||||
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
|
||||
| `-Files <список>` | для Partial | Относительные пути файлов через запятую |
|
||||
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Режимы загрузки
|
||||
|
||||
| Режим | Описание |
|
||||
|-------|----------|
|
||||
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
|
||||
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
|
||||
|
||||
### Формат файла списка (listFile)
|
||||
|
||||
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
|
||||
|
||||
```
|
||||
Catalogs/Номенклатура.xml
|
||||
Catalogs/Номенклатура/Ext/ObjectModule.bsl
|
||||
Documents/Заказ.xml
|
||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||
```
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Прочитай лог и покажи результат
|
||||
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||
|
||||
## Примеры
|
||||
|
||||
```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
|
||||
|
||||
# Частичная загрузка конкретных файлов
|
||||
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"
|
||||
|
||||
# Загрузка расширения
|
||||
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 "МоёРасширение"
|
||||
|
||||
# Загрузка + обновление БД в одном запуске
|
||||
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
|
||||
```
|
||||
---
|
||||
name: db-load-xml
|
||||
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
||||
argument-hint: <configDir> [database]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-load-xml — Загрузка конфигурации из XML
|
||||
|
||||
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-load-xml <configDir> [database]
|
||||
/db-load-xml src/config dev
|
||||
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
```
|
||||
|
||||
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-ConfigDir <путь>` | да | Каталог XML-исходников |
|
||||
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
|
||||
| `-Files <список>` | для Partial | Относительные пути файлов через запятую |
|
||||
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Режимы загрузки
|
||||
|
||||
| Режим | Описание |
|
||||
|-------|----------|
|
||||
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
|
||||
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
|
||||
|
||||
### Формат файла списка (listFile)
|
||||
|
||||
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
|
||||
|
||||
```
|
||||
Catalogs/Номенклатура.xml
|
||||
Catalogs/Номенклатура/Ext/ObjectModule.bsl
|
||||
Documents/Заказ.xml
|
||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||
```
|
||||
|
||||
## После выполнения
|
||||
|
||||
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Полная загрузка
|
||||
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
|
||||
|
||||
# Частичная загрузка конкретных файлов
|
||||
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"
|
||||
|
||||
# Загрузка расширения
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -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\cfsrc" -Mode Full -UpdateDB
|
||||
```
|
||||
|
||||
@@ -1,238 +1,418 @@
|
||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из XML-файлов
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию в информационную базу из XML-файлов.
|
||||
Поддерживает полную и частичную загрузку.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-исходников конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим загрузки: Full или Partial (по умолчанию Full)
|
||||
|
||||
.PARAMETER Files
|
||||
Относительные пути файлов через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER ListFile
|
||||
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Partial")]
|
||||
[string]$Mode = "Full",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Files,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
|
||||
if ($Mode -eq "Full") {
|
||||
Write-Host "Executing full configuration load..."
|
||||
} else {
|
||||
Write-Host "Executing partial configuration load..."
|
||||
|
||||
# Build list file
|
||||
$generatedListFile = $null
|
||||
if ($ListFile) {
|
||||
# Use provided list file
|
||||
if (-not (Test-Path $ListFile)) {
|
||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$generatedListFile = $ListFile
|
||||
} else {
|
||||
# Generate from -Files parameter
|
||||
$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" }
|
||||
}
|
||||
|
||||
$arguments += "-listFile", "`"$generatedListFile`""
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
}
|
||||
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_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 "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (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
|
||||
}
|
||||
}
|
||||
# db-load-xml v1.12 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из XML-файлов
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию в информационную базу из XML-файлов.
|
||||
Поддерживает полную и частичную загрузку.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-исходников конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим загрузки: Full или Partial (по умолчанию Full)
|
||||
|
||||
.PARAMETER Files
|
||||
Относительные пути файлов через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER ListFile
|
||||
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
#>
|
||||
|
||||
[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]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Partial")]
|
||||
[string]$Mode = "Full",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Files,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$StrictLog
|
||||
)
|
||||
|
||||
$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 config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
|
||||
if ($Mode -eq "Full") {
|
||||
Write-Host "Executing full configuration load..."
|
||||
} else {
|
||||
Write-Host "Executing partial configuration load..."
|
||||
|
||||
# Build list file
|
||||
$rawList = @()
|
||||
if ($ListFile) {
|
||||
if (-not (Test-Path $ListFile)) {
|
||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
} else {
|
||||
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
# 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 += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
}
|
||||
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_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
|
||||
|
||||
# --- 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 ---
|
||||
# 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) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
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
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,104 @@
|
||||
#!/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
|
||||
|
||||
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 1cv8.exe."""
|
||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
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:
|
||||
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)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -63,13 +130,24 @@ def main():
|
||||
help="File format (default: Hierarchical)",
|
||||
)
|
||||
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()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -83,6 +161,77 @@ def main():
|
||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
@@ -109,23 +258,33 @@ def main():
|
||||
print("Executing partial configuration load...")
|
||||
|
||||
# Build list file
|
||||
generated_list_file = None
|
||||
if args.ListFile:
|
||||
# Use provided list file
|
||||
if not os.path.isfile(args.ListFile):
|
||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||
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:
|
||||
# Generate from -Files parameter
|
||||
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))
|
||||
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||
|
||||
print(f"Files to load: {len(file_list)}")
|
||||
for fl in file_list:
|
||||
print(f" {fl}")
|
||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
||||
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
|
||||
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.append("-partial")
|
||||
@@ -157,22 +316,60 @@ def main():
|
||||
)
|
||||
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 ---
|
||||
# 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:
|
||||
print("Load completed successfully")
|
||||
else:
|
||||
print(f"Error loading configuration (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
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
|
||||
if silent_failures:
|
||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
||||
print(
|
||||
f"[warning] log contains {len(silent_failures)} rejection(s) — "
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
---
|
||||
name: db-run
|
||||
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
|
||||
argument-hint: "[database]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-run — Запуск 1С:Предприятие
|
||||
|
||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-run [database]
|
||||
/db-run dev
|
||||
/db-run dev /Execute process.epf
|
||||
/db-run dev /C "параметр запуска"
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
||||
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
||||
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Важно
|
||||
|
||||
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Простой запуск
|
||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -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"
|
||||
|
||||
# Открыть по навигационной ссылке
|
||||
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -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 "ЗапуститьОбновление"
|
||||
```
|
||||
---
|
||||
name: db-run
|
||||
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
|
||||
argument-hint: "[database]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-run — Запуск 1С:Предприятие
|
||||
|
||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-run [database]
|
||||
/db-run dev
|
||||
/db-run dev /Execute process.epf
|
||||
/db-run dev /C "параметр запуска"
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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-run.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
||||
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
||||
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Важно
|
||||
|
||||
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Простой запуск
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
|
||||
# Запуск с обработкой
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||
|
||||
# Открыть по навигационной ссылке
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||
|
||||
# Серверная база с параметром запуска
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||
```
|
||||
|
||||
@@ -1,145 +1,171 @@
|
||||
# db-run v1.0 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Запуск 1С:Предприятие
|
||||
|
||||
.DESCRIPTION
|
||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
||||
Запуск в фоне — не ждёт завершения процесса.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Execute
|
||||
Путь к внешней обработке для запуска
|
||||
|
||||
.PARAMETER CParam
|
||||
Параметр запуска (/C)
|
||||
|
||||
.PARAMETER URL
|
||||
Навигационная ссылка (e1cib/...)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
||||
#>
|
||||
|
||||
[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=$false)]
|
||||
[string]$Execute,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CParam,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$URL
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Build arguments as single string ---
|
||||
# Note: Start-Process without -NoNewWindow uses ShellExecute.
|
||||
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
|
||||
# re-joins elements. Single string avoids this.
|
||||
$argString = "ENTERPRISE"
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$argString += " /F `"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $argString += " /N`"$UserName`"" }
|
||||
if ($Password) { $argString += " /P`"$Password`"" }
|
||||
|
||||
# --- Optional params ---
|
||||
if ($Execute) {
|
||||
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
|
||||
if ($ext -eq ".erf") {
|
||||
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
|
||||
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
|
||||
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
|
||||
$Execute = ""
|
||||
}
|
||||
}
|
||||
if ($Execute) {
|
||||
$argString += " /Execute `"$Execute`""
|
||||
}
|
||||
if ($CParam) {
|
||||
$argString += " /C `"$CParam`""
|
||||
}
|
||||
if ($URL) {
|
||||
$argString += " /URL `"$URL`""
|
||||
}
|
||||
|
||||
$argString += " /DisableStartupDialogs"
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
Write-Host "Running: 1cv8.exe $argString"
|
||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||
# db-run v1.2 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Запуск 1С:Предприятие
|
||||
|
||||
.DESCRIPTION
|
||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
||||
Запуск в фоне — не ждёт завершения процесса.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Execute
|
||||
Путь к внешней обработке для запуска
|
||||
|
||||
.PARAMETER CParam
|
||||
Параметр запуска (/C)
|
||||
|
||||
.PARAMETER URL
|
||||
Навигационная ссылка (e1cib/...)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
||||
#>
|
||||
|
||||
[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=$false)]
|
||||
[string]$Execute,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CParam,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$URL
|
||||
)
|
||||
|
||||
$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
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Build arguments as single string ---
|
||||
# Note: Start-Process without -NoNewWindow uses ShellExecute.
|
||||
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
|
||||
# re-joins elements. Single string avoids this.
|
||||
$argString = "ENTERPRISE"
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$argString += " /F `"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $argString += " /N`"$UserName`"" }
|
||||
if ($Password) { $argString += " /P`"$Password`"" }
|
||||
|
||||
# --- Optional params ---
|
||||
if ($Execute) {
|
||||
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
|
||||
if ($ext -eq ".erf") {
|
||||
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
|
||||
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
|
||||
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
|
||||
$Execute = ""
|
||||
}
|
||||
}
|
||||
if ($Execute) {
|
||||
$argString += " /Execute `"$Execute`""
|
||||
}
|
||||
if ($CParam) {
|
||||
$argString += " /C `"$CParam`""
|
||||
}
|
||||
if ($URL) {
|
||||
$argString += " /URL `"$URL`""
|
||||
}
|
||||
|
||||
$argString += " /DisableStartupDialogs"
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
Write-Host "Running: 1cv8.exe $argString"
|
||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||
|
||||
@@ -1,28 +1,75 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
@@ -1,93 +1,86 @@
|
||||
---
|
||||
name: db-update
|
||||
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
|
||||
argument-hint: "[database]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-update — Обновление конфигурации БД
|
||||
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-update [database]
|
||||
/db-update dev
|
||||
/db-update dev -Dynamic+
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Extension <имя>` | нет | Обновить расширение |
|
||||
| `-AllExtensions` | нет | Обновить все расширения |
|
||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||
| `-Server` | нет | Обновление на стороне сервера |
|
||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Фоновое обновление (серверная база)
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-BackgroundStart` | Начать фоновое обновление |
|
||||
| `-BackgroundFinish` | Дождаться окончания |
|
||||
| `-BackgroundCancel` | Отменить |
|
||||
| `-BackgroundSuspend` | Приостановить |
|
||||
| `-BackgroundResume` | Возобновить |
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## Предупреждения
|
||||
|
||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
|
||||
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обычное обновление (файловая база)
|
||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -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 "+"
|
||||
|
||||
# Обновление расширения
|
||||
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||
```
|
||||
---
|
||||
name: db-update
|
||||
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
|
||||
argument-hint: "[database]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-update — Обновление конфигурации БД
|
||||
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-update [database]
|
||||
/db-update dev
|
||||
/db-update dev -Dynamic+
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.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-update.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Extension <имя>` | нет | Обновить расширение |
|
||||
| `-AllExtensions` | нет | Обновить все расширения |
|
||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||
| `-Server` | нет | Обновление на стороне сервера |
|
||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Фоновое обновление (серверная база)
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-BackgroundStart` | Начать фоновое обновление |
|
||||
| `-BackgroundFinish` | Дождаться окончания |
|
||||
| `-BackgroundCancel` | Отменить |
|
||||
| `-BackgroundSuspend` | Приостановить |
|
||||
| `-BackgroundResume` | Возобновить |
|
||||
|
||||
## Предупреждения
|
||||
|
||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
|
||||
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обычное обновление (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
|
||||
# Динамическое обновление (серверная база)
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||
|
||||
# Обновление расширения
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -1,184 +1,272 @@
|
||||
# db-update v1.0 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Обновление конфигурации базы данных 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных.
|
||||
Поддерживает динамическое обновление, обновление расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для обновления
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Обновить все расширения
|
||||
|
||||
.PARAMETER Dynamic
|
||||
Динамическое обновление: "+" включить, "-" отключить
|
||||
|
||||
.PARAMETER Server
|
||||
Обновление на стороне сервера
|
||||
|
||||
.PARAMETER WarningsAsErrors
|
||||
Предупреждения считать ошибками
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("+", "-")]
|
||||
[string]$Dynamic,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Server,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$WarningsAsErrors
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/UpdateDBCfg"
|
||||
|
||||
# --- Options ---
|
||||
if ($Dynamic) {
|
||||
$arguments += "-Dynamic$Dynamic"
|
||||
}
|
||||
if ($Server) {
|
||||
$arguments += "-Server"
|
||||
}
|
||||
if ($WarningsAsErrors) {
|
||||
$arguments += "-WarningsAsErrors"
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "update_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 "Database configuration updated successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (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
|
||||
}
|
||||
}
|
||||
# db-update v1.6 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Обновление конфигурации базы данных 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных.
|
||||
Поддерживает динамическое обновление, обновление расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для обновления
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Обновить все расширения
|
||||
|
||||
.PARAMETER Dynamic
|
||||
Динамическое обновление: "+" включить, "-" отключить
|
||||
|
||||
.PARAMETER Server
|
||||
Обновление на стороне сервера
|
||||
|
||||
.PARAMETER WarningsAsErrors
|
||||
Предупреждения считать ошибками
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[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=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("+", "-")]
|
||||
[string]$Dynamic,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Server,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$WarningsAsErrors
|
||||
)
|
||||
|
||||
$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
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/UpdateDBCfg"
|
||||
|
||||
# --- Options ---
|
||||
if ($Dynamic) {
|
||||
$arguments += "-Dynamic$Dynamic"
|
||||
}
|
||||
if ($Server) {
|
||||
$arguments += "-Server"
|
||||
}
|
||||
if ($WarningsAsErrors) {
|
||||
$arguments += "-WarningsAsErrors"
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "update_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 "Database configuration updated successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,104 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -52,11 +121,48 @@ def main():
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- 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)
|
||||
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 = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||
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,251 +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"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
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,197 +1,196 @@
|
||||
---
|
||||
name: epf-bsp-add-command
|
||||
description: Добавить команду в дополнительную обработку БСП
|
||||
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-bsp-add-command — Добавление команды БСП
|
||||
|
||||
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
|
||||
|
||||
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|-----------------------|--------------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки |
|
||||
| Идентификатор | да | — | Внутреннее имя команды (латиница) |
|
||||
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
|
||||
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
|
||||
## Маппинг типов команд
|
||||
|
||||
Пользователь может указать тип в свободной форме:
|
||||
|
||||
| Пользователь пишет | ТипКоманды |
|
||||
|---------------------------------------|-----------------------------------------------------|
|
||||
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
|
||||
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
|
||||
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
|
||||
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
|
||||
|
||||
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
|
||||
|
||||
| Вид обработки (из кода) | ТипКоманды по умолчанию |
|
||||
|----------------------------|-------------------------------------------|
|
||||
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
|
||||
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| Отчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
|
||||
|
||||
## Шаблон добавления команды
|
||||
|
||||
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
|
||||
НоваяКоманда.Идентификатор = "{{Идентификатор}}";
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
```
|
||||
|
||||
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
```
|
||||
|
||||
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
|
||||
|
||||
## Шаблоны обработчиков
|
||||
|
||||
### ВызовСерверногоМетода — если обработчик уже есть
|
||||
|
||||
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
|
||||
|
||||
```bsl
|
||||
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
```
|
||||
|
||||
### ВызовСерверногоМетода — если обработчика нет
|
||||
|
||||
Для глобальных обработок (без `ОбъектыНазначения`):
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Для назначаемых обработок (с `ОбъектыНазначения`):
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### ПечатнаяФорма — если процедура Печать уже есть
|
||||
|
||||
Добавь блок перед `КонецПроцедуры`:
|
||||
|
||||
```bsl
|
||||
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
|
||||
Если ПечатнаяФорма <> Неопределено Тогда
|
||||
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
|
||||
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
|
||||
КонецЕсли;
|
||||
```
|
||||
|
||||
### ПечатнаяФорма — если процедуры Печать нет
|
||||
|
||||
```bsl
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
|
||||
Если ПечатнаяФорма <> Неопределено Тогда
|
||||
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
|
||||
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### ВызовКлиентскогоМетода
|
||||
|
||||
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
|
||||
|
||||
Для глобальных обработок:
|
||||
|
||||
```bsl
|
||||
&НаКлиенте
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Для назначаемых обработок:
|
||||
|
||||
```bsl
|
||||
&НаКлиенте
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Если процедура уже есть — добавь ветку `ИначеЕсли`.
|
||||
|
||||
## Инструкции
|
||||
|
||||
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
|
||||
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
|
||||
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
|
||||
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
|
||||
5. Добавь обработчик:
|
||||
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
|
||||
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
|
||||
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
|
||||
7. Используй табы для отступов
|
||||
|
||||
## Пример
|
||||
|
||||
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
|
||||
|
||||
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
|
||||
НоваяКоманда.Идентификатор = "ЗаказПокупателя";
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
```
|
||||
|
||||
И в существующую процедуру `Печать` добавится блок обработки.
|
||||
---
|
||||
name: epf-bsp-add-command
|
||||
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
|
||||
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-bsp-add-command — Добавление команды БСП
|
||||
|
||||
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
|
||||
|
||||
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|-----------------------|--------------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки |
|
||||
| Идентификатор | да | — | Внутреннее имя команды (латиница) |
|
||||
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
|
||||
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
|
||||
## Маппинг типов команд
|
||||
|
||||
Пользователь может указать тип в свободной форме:
|
||||
|
||||
| Пользователь пишет | ТипКоманды |
|
||||
|---------------------------------------|-----------------------------------------------------|
|
||||
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
|
||||
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
|
||||
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
|
||||
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
|
||||
|
||||
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
|
||||
|
||||
| Вид обработки (из кода) | ТипКоманды по умолчанию |
|
||||
|----------------------------|-------------------------------------------|
|
||||
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
|
||||
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| Отчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
|
||||
|
||||
## Шаблон добавления команды
|
||||
|
||||
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
|
||||
НоваяКоманда.Идентификатор = "{{Идентификатор}}";
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
```
|
||||
|
||||
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
```
|
||||
|
||||
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
|
||||
|
||||
## Шаблоны обработчиков
|
||||
|
||||
### ВызовСерверногоМетода — если обработчик уже есть
|
||||
|
||||
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
|
||||
|
||||
```bsl
|
||||
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
```
|
||||
|
||||
### ВызовСерверногоМетода — если обработчика нет
|
||||
|
||||
Для глобальных обработок (без `ОбъектыНазначения`):
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Для назначаемых обработок (с `ОбъектыНазначения`):
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### ПечатнаяФорма — если процедура Печать уже есть
|
||||
|
||||
Добавь блок перед `КонецПроцедуры`:
|
||||
|
||||
```bsl
|
||||
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
|
||||
Если ПечатнаяФорма <> Неопределено Тогда
|
||||
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
|
||||
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
|
||||
КонецЕсли;
|
||||
```
|
||||
|
||||
### ПечатнаяФорма — если процедуры Печать нет
|
||||
|
||||
```bsl
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
|
||||
Если ПечатнаяФорма <> Неопределено Тогда
|
||||
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
|
||||
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### ВызовКлиентскогоМетода
|
||||
|
||||
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
|
||||
|
||||
Для глобальных обработок:
|
||||
|
||||
```bsl
|
||||
&НаКлиенте
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Для назначаемых обработок:
|
||||
|
||||
```bsl
|
||||
&НаКлиенте
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
|
||||
|
||||
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
|
||||
// TODO: Реализация {{Идентификатор}}
|
||||
КонецЕсли;
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Если процедура уже есть — добавь ветку `ИначеЕсли`.
|
||||
|
||||
## Инструкции
|
||||
|
||||
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
|
||||
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
|
||||
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
|
||||
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
|
||||
5. Добавь обработчик:
|
||||
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
|
||||
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
|
||||
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
|
||||
7. Используй табы для отступов
|
||||
|
||||
## Пример
|
||||
|
||||
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
|
||||
|
||||
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
|
||||
НоваяКоманда.Идентификатор = "ЗаказПокупателя";
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
```
|
||||
|
||||
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
|
||||
|
||||
@@ -1,208 +1,208 @@
|
||||
---
|
||||
name: epf-bsp-init
|
||||
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
|
||||
argument-hint: <ProcessorName> <Вид>
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-bsp-init — Регистрация обработки в БСП
|
||||
|
||||
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-bsp-init <ProcessorName> <Вид> [Назначение...]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|--------------|---------------------------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
|
||||
| Вид | да | — | Вид обработки (см. маппинг ниже) |
|
||||
| Назначение | * | — | Объекты метаданных для назначаемых видов |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
|
||||
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
|
||||
|
||||
## Маппинг вида обработки
|
||||
|
||||
Пользователь может указать вид в свободной форме. Определи нужный по контексту:
|
||||
|
||||
| Пользователь пишет | Вид | API-метод |
|
||||
|-------------------------------------------|----------------------------|----------------------------------------------|
|
||||
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
|
||||
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
|
||||
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
|
||||
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
|
||||
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
|
||||
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
|
||||
|
||||
## Тип команды по умолчанию
|
||||
|
||||
| Вид | ТипКоманды по умолчанию |
|
||||
|----------------------------|-------------------------------------------|
|
||||
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
|
||||
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| Отчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
|
||||
|
||||
## Шаблон: СведенияОВнешнейОбработке
|
||||
|
||||
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
|
||||
|
||||
```bsl
|
||||
Функция СведенияОВнешнейОбработке() Экспорт
|
||||
|
||||
МетаданныеОбработки = Метаданные();
|
||||
|
||||
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
|
||||
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
|
||||
ПараметрыРегистрации.Версия = "1.0";
|
||||
|
||||
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
|
||||
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
|
||||
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
{{СЕКЦИЯ_МОДИФИКАТОР}}
|
||||
|
||||
Возврат ПараметрыРегистрации;
|
||||
|
||||
КонецФункции
|
||||
```
|
||||
|
||||
### Подстановки
|
||||
|
||||
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
|
||||
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
|
||||
|
||||
### Условные секции
|
||||
|
||||
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
|
||||
|
||||
```bsl
|
||||
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
|
||||
```
|
||||
|
||||
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
|
||||
|
||||
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
|
||||
|
||||
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
```
|
||||
|
||||
Для остальных видов — удалить вместе с пустой строкой.
|
||||
|
||||
## Шаблоны серверных обработчиков
|
||||
|
||||
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
|
||||
|
||||
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### Для ПечатнаяФорма
|
||||
|
||||
```bsl
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
|
||||
|
||||
Если пользователь явно выбрал серверный метод вместо открытия формы:
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
|
||||
|
||||
## Инструкции
|
||||
|
||||
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
|
||||
2. Прочитай файл
|
||||
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
|
||||
4. Если файл не найден — предложи сначала вызвать `/epf-init`
|
||||
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
|
||||
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
|
||||
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
|
||||
8. Используй табы для отступов (как в исходном файле)
|
||||
|
||||
## Пример
|
||||
|
||||
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
|
||||
|
||||
Результат в `ObjectModule.bsl`:
|
||||
|
||||
```bsl
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
Функция СведенияОВнешнейОбработке() Экспорт
|
||||
|
||||
МетаданныеОбработки = Метаданные();
|
||||
|
||||
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
|
||||
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
|
||||
ПараметрыРегистрации.Версия = "1.0";
|
||||
|
||||
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
|
||||
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
|
||||
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
|
||||
Возврат ПараметрыРегистрации;
|
||||
|
||||
КонецФункции
|
||||
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить ещё команду: `/epf-bsp-add-command`
|
||||
- Добавить форму: `/epf-add-form`
|
||||
- Добавить макет: `/template-add`
|
||||
- Собрать EPF: `/epf-build`
|
||||
---
|
||||
name: epf-bsp-init
|
||||
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
|
||||
argument-hint: <ProcessorName> <Вид>
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-bsp-init — Регистрация обработки в БСП
|
||||
|
||||
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-bsp-init <ProcessorName> <Вид> [Назначение...]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|--------------|---------------------------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
|
||||
| Вид | да | — | Вид обработки (см. маппинг ниже) |
|
||||
| Назначение | * | — | Объекты метаданных для назначаемых видов |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
|
||||
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
|
||||
|
||||
## Маппинг вида обработки
|
||||
|
||||
Пользователь может указать вид в свободной форме. Определи нужный по контексту:
|
||||
|
||||
| Пользователь пишет | Вид | API-метод |
|
||||
|-------------------------------------------|----------------------------|----------------------------------------------|
|
||||
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
|
||||
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
|
||||
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
|
||||
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
|
||||
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
|
||||
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
|
||||
|
||||
## Тип команды по умолчанию
|
||||
|
||||
| Вид | ТипКоманды по умолчанию |
|
||||
|----------------------------|-------------------------------------------|
|
||||
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
|
||||
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| Отчет | `ТипКомандыОткрытиеФормы()` |
|
||||
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
|
||||
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
|
||||
|
||||
## Шаблон: СведенияОВнешнейОбработке
|
||||
|
||||
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
|
||||
|
||||
```bsl
|
||||
Функция СведенияОВнешнейОбработке() Экспорт
|
||||
|
||||
МетаданныеОбработки = Метаданные();
|
||||
|
||||
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
|
||||
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
|
||||
ПараметрыРегистрации.Версия = "1.0";
|
||||
|
||||
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
|
||||
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
|
||||
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
{{СЕКЦИЯ_МОДИФИКАТОР}}
|
||||
|
||||
Возврат ПараметрыРегистрации;
|
||||
|
||||
КонецФункции
|
||||
```
|
||||
|
||||
### Подстановки
|
||||
|
||||
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
|
||||
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
|
||||
|
||||
### Условные секции
|
||||
|
||||
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
|
||||
|
||||
```bsl
|
||||
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
|
||||
```
|
||||
|
||||
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
|
||||
|
||||
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
|
||||
|
||||
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
|
||||
|
||||
```bsl
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
```
|
||||
|
||||
Для остальных видов — удалить вместе с пустой строкой.
|
||||
|
||||
## Шаблоны серверных обработчиков
|
||||
|
||||
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
|
||||
|
||||
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### Для ПечатнаяФорма
|
||||
|
||||
```bsl
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
|
||||
|
||||
Если пользователь явно выбрал серверный метод вместо открытия формы:
|
||||
|
||||
```bsl
|
||||
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
|
||||
|
||||
## Инструкции
|
||||
|
||||
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
|
||||
2. Прочитай файл
|
||||
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
|
||||
4. Если файл не найден — предложи сначала вызвать `/epf-init`
|
||||
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
|
||||
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
|
||||
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
|
||||
8. Используй табы для отступов (как в исходном файле)
|
||||
|
||||
## Пример
|
||||
|
||||
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
|
||||
|
||||
Результат в `ObjectModule.bsl`:
|
||||
|
||||
```bsl
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
Функция СведенияОВнешнейОбработке() Экспорт
|
||||
|
||||
МетаданныеОбработки = Метаданные();
|
||||
|
||||
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
|
||||
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
|
||||
ПараметрыРегистрации.Версия = "1.0";
|
||||
|
||||
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
|
||||
|
||||
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
|
||||
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
|
||||
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
|
||||
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
|
||||
НоваяКоманда.ПоказыватьОповещение = Ложь;
|
||||
НоваяКоманда.Модификатор = "ПечатьMXL";
|
||||
|
||||
Возврат ПараметрыРегистрации;
|
||||
|
||||
КонецФункции
|
||||
|
||||
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
|
||||
|
||||
// TODO: Реализация
|
||||
|
||||
КонецПроцедуры
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить ещё команду: `/epf-bsp-add-command`
|
||||
- Добавить форму: `/form-add`
|
||||
- Добавить макет: `/template-add`
|
||||
- Собрать EPF: `/epf-build`
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
---
|
||||
name: epf-build
|
||||
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
|
||||
argument-hint: <ProcessorName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-build — Сборка обработки
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-build <ProcessorName> [SrcDir] [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|--------------|--------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки (имя корневого XML) |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
| OutDir | нет | `build` | Каталог для результата |
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Сборка обработки (файловая база)
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -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"
|
||||
```
|
||||
---
|
||||
name: epf-build
|
||||
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
|
||||
argument-hint: <ProcessorName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-build — Сборка обработки
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-build <ProcessorName> [SrcDir] [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|--------------|--------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки (имя корневого XML) |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
| OutDir | нет | `build` | Каталог для результата |
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Сборка обработки (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -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,173 +1,254 @@
|
||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||
|
||||
.DESCRIPTION
|
||||
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
|
||||
Общий скрипт для epf-build и erf-build.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER SourceFile
|
||||
Путь к корневому XML-файлу исходников
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному EPF/ERF-файлу
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
||||
#>
|
||||
|
||||
[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]$SourceFile,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
||||
if ($stubProc.ExitCode -ne 0) {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$InfoBasePath = $autoBasePath
|
||||
$autoCreatedBase = $autoBasePath
|
||||
}
|
||||
|
||||
# --- Validate source file ---
|
||||
if (-not (Test-Path $SourceFile)) {
|
||||
Write-Host "Error: source file not found: $SourceFile" -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 "epf_build_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "build_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 "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error building (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
|
||||
}
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||
|
||||
.DESCRIPTION
|
||||
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
|
||||
Общий скрипт для epf-build и erf-build.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER SourceFile
|
||||
Путь к корневому XML-файлу исходников
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному EPF/ERF-файлу
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
||||
#>
|
||||
|
||||
[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]$SourceFile,
|
||||
|
||||
[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" }
|
||||
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
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
||||
if ($stubProc.ExitCode -ne 0) {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$InfoBasePath = $autoBasePath
|
||||
$autoCreatedBase = $autoBasePath
|
||||
}
|
||||
|
||||
# --- Validate source file ---
|
||||
if (-not (Test-Path $SourceFile)) {
|
||||
Write-Host "Error: source file not found: $SourceFile" -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 "epf_build_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "build_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 "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error building (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
|
||||
}
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,104 @@
|
||||
#!/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
|
||||
|
||||
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 1cv8.exe."""
|
||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
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:
|
||||
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)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -51,6 +118,10 @@ def main():
|
||||
|
||||
# --- Resolve 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_created_base = None
|
||||
@@ -84,6 +155,29 @@ def main():
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
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 ---
|
||||
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
|
||||
param(
|
||||
[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 ---
|
||||
Write-Host "Creating infobase: $TempBasePath"
|
||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
@@ -12,6 +12,27 @@ import tempfile
|
||||
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():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
@@ -1034,6 +1055,32 @@ def main():
|
||||
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.')
|
||||
|
||||
# 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
|
||||
print(f'Creating infobase: {temp_base}')
|
||||
result = subprocess.run(
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
---
|
||||
name: epf-dump
|
||||
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
|
||||
argument-hint: <EpfFile>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-dump — Разборка обработки
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-dump <EpfFile> [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|----------|:------------:|--------------|-------------------------------------|
|
||||
| EpfFile | да | — | Путь к EPF-файлу |
|
||||
| OutDir | нет | `src` | Каталог для выгрузки исходников |
|
||||
|
||||
## Параметры подключения (обязательно)
|
||||
|
||||
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
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`
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Разборка обработки (файловая база)
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -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"
|
||||
```
|
||||
---
|
||||
name: epf-dump
|
||||
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
|
||||
argument-hint: <EpfFile>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-dump — Разборка обработки
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-dump <EpfFile> [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|----------|:------------:|--------------|-------------------------------------|
|
||||
| EpfFile | да | — | Путь к EPF-файлу |
|
||||
| OutDir | нет | `src` | Каталог для выгрузки исходников |
|
||||
|
||||
## Параметры подключения (обязательно)
|
||||
|
||||
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Разборка обработки (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -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,167 +1,253 @@
|
||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||
|
||||
.DESCRIPTION
|
||||
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
|
||||
Общий скрипт для epf-dump и erf-dump.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к EPF/ERF-файлу
|
||||
|
||||
.PARAMETER OutputDir
|
||||
Каталог для выгрузки исходников
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
||||
#>
|
||||
|
||||
[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=$true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate database connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
|
||||
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate input file ---
|
||||
if (-not (Test-Path $InputFile)) {
|
||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- 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 += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping (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
|
||||
}
|
||||
}
|
||||
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||
|
||||
.DESCRIPTION
|
||||
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
|
||||
Общий скрипт для epf-dump и erf-dump.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к EPF/ERF-файлу
|
||||
|
||||
.PARAMETER OutputDir
|
||||
Каталог для выгрузки исходников
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
||||
#>
|
||||
|
||||
[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=$true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$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
|
||||
}
|
||||
|
||||
# --- Validate database connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
|
||||
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
|
||||
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 ---
|
||||
if (-not (Test-Path $InputFile)) {
|
||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
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 ---
|
||||
$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 += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,104 @@
|
||||
#!/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
|
||||
|
||||
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 1cv8.exe."""
|
||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
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:
|
||||
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)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
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: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
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")
|
||||
@@ -57,12 +124,20 @@ def main():
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
|
||||
# --- Validate database connection ---
|
||||
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("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||
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 ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
@@ -78,6 +153,28 @@ def main():
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
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 ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
|
||||
@@ -1,55 +1,41 @@
|
||||
---
|
||||
name: epf-init
|
||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
|
||||
argument-hint: <Name> [Synonym]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-init — Создание новой обработки
|
||||
|
||||
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-init <Name> [Synonym] [SrcDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-----------|:------------:|--------------|-------------------------------------|
|
||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<SrcDir>/
|
||||
├── <Name>.xml # Корневой файл метаданных (4 UUID)
|
||||
└── <Name>/
|
||||
└── Ext/
|
||||
└── ObjectModule.bsl # Модуль объекта с 3 регионами
|
||||
```
|
||||
|
||||
- Корневой XML содержит `MetaDataObject/ExternalDataProcessor` с пустыми `DefaultForm` и `ChildObjects`
|
||||
- ClassId фиксирован: `c3831ec8-d8d5-4f93-8a22-f9bfae07327f`
|
||||
- Файл создаётся в UTF-8 с BOM
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить форму: `/epf-add-form`
|
||||
- Добавить макет: `/template-add`
|
||||
- Добавить справку: `/help-add`
|
||||
- Собрать EPF: `/epf-build`
|
||||
---
|
||||
name: epf-init
|
||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
|
||||
argument-hint: <Name> [Synonym]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-init — Создание новой обработки
|
||||
|
||||
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-init <Name> [Synonym] [SrcDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-----------|:------------:|--------------|-------------------------------------|
|
||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить форму: `/form-add`
|
||||
- Добавить макет: `/template-add`
|
||||
- Добавить справку: `/help-add`
|
||||
- Собрать EPF: `/epf-build`
|
||||
|
||||
@@ -1,88 +1,90 @@
|
||||
# epf-init v1.0 — Init 1C external data processor scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
[string]$Synonym = $Name,
|
||||
|
||||
[string]$SrcDir = "src"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$uuid1 = [guid]::NewGuid().ToString()
|
||||
$uuid2 = [guid]::NewGuid().ToString()
|
||||
$uuid3 = [guid]::NewGuid().ToString()
|
||||
$uuid4 = [guid]::NewGuid().ToString()
|
||||
|
||||
$xml = @"
|
||||
<?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">
|
||||
<ExternalDataProcessor uuid="$uuid1">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
|
||||
<xr:TypeId>$uuid3</xr:TypeId>
|
||||
<xr:ValueId>$uuid4</xr:ValueId>
|
||||
</xr:GeneratedType>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$Name</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<DefaultForm/>
|
||||
<AuxiliaryForm/>
|
||||
</Properties>
|
||||
<ChildObjects/>
|
||||
</ExternalDataProcessor>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
||||
$processorDir = Join-Path $SrcDir $Name
|
||||
|
||||
if (Test-Path $rootFile) {
|
||||
Write-Error "Файл уже существует: $rootFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $SrcDir)) {
|
||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
||||
}
|
||||
$extDir = Join-Path $processorDir "Ext"
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
||||
|
||||
# --- Модуль объекта ---
|
||||
|
||||
$moduleBsl = @"
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
"@
|
||||
|
||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||
|
||||
Write-Host "[OK] Создана обработка: $rootFile"
|
||||
Write-Host " Каталог: $processorDir"
|
||||
Write-Host " Модуль: $modulePath"
|
||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
[string]$Synonym = $Name,
|
||||
|
||||
[string]$SrcDir = "src"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$uuid1 = [guid]::NewGuid().ToString()
|
||||
$uuid2 = [guid]::NewGuid().ToString()
|
||||
$uuid3 = [guid]::NewGuid().ToString()
|
||||
$uuid4 = [guid]::NewGuid().ToString()
|
||||
|
||||
$xml = @"
|
||||
<?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">
|
||||
<ExternalDataProcessor uuid="$uuid1">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
|
||||
<xr:TypeId>$uuid3</xr:TypeId>
|
||||
<xr:ValueId>$uuid4</xr:ValueId>
|
||||
</xr:GeneratedType>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$Name</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<DefaultForm/>
|
||||
<AuxiliaryForm/>
|
||||
</Properties>
|
||||
<ChildObjects/>
|
||||
</ExternalDataProcessor>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
||||
$processorDir = Join-Path $SrcDir $Name
|
||||
|
||||
if (Test-Path $rootFile) {
|
||||
Write-Error "Файл уже существует: $rootFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $SrcDir)) {
|
||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
||||
}
|
||||
$extDir = Join-Path $processorDir "Ext"
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
||||
|
||||
# --- Модуль объекта ---
|
||||
|
||||
$moduleBsl = @"
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
"@
|
||||
|
||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||
|
||||
Write-Host "[OK] Создана обработка: $rootFile"
|
||||
Write-Host " Каталог: $processorDir"
|
||||
Write-Host " Модуль: $modulePath"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
"""Generates minimal XML source files for a 1C external data processor."""
|
||||
import sys, os, argparse, uuid
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
---
|
||||
name: epf-validate
|
||||
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /epf-validate — валидация внешней обработки (EPF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|-------------------------------------------------------|--------------|
|
||||
| 1 | Root structure: MetaDataObject/ExternalDataProcessor | ERROR |
|
||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
||||
| 3 | Properties: Name (identifier), Synonym | ERROR / WARN |
|
||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
||||
| 5 | Cross-references: DefaultForm → Form, AuxiliaryForm | ERROR / WARN |
|
||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
---
|
||||
name: epf-validate
|
||||
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /epf-validate — валидация внешней обработки (EPF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||
|
||||
@@ -46,7 +46,7 @@ def main():
|
||||
sys.stdout.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.add_argument("-ObjectPath", required=True)
|
||||
parser.add_argument("-ObjectPath", "-Path", required=True)
|
||||
parser.add_argument("-Detailed", action="store_true")
|
||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||
parser.add_argument("-OutFile", default=None)
|
||||
@@ -165,8 +165,8 @@ def main():
|
||||
version = root.get("version", "")
|
||||
if not version:
|
||||
report_warn("1. Missing version attribute on MetaDataObject")
|
||||
elif version not in ("2.17", "2.20"):
|
||||
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||
elif version not in ("2.17", "2.20", "2.21"):
|
||||
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||
|
||||
# Detect type
|
||||
child_elements = []
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
---
|
||||
name: erf-build
|
||||
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
|
||||
argument-hint: <ReportName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-build — Сборка отчёта
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-build <ReportName> [SrcDir] [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|------------|:------------:|--------------|--------------------------------------|
|
||||
| ReportName | да | — | Имя отчёта (имя корневого XML) |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
| OutDir | нет | `build` | Каталог для результата |
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
Используй общий скрипт из epf-build:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Сборка отчёта (файловая база)
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -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"
|
||||
```
|
||||
---
|
||||
name: erf-build
|
||||
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
|
||||
argument-hint: <ReportName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-build — Сборка отчёта
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-build <ReportName> [SrcDir] [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|------------|:------------:|--------------|--------------------------------------|
|
||||
| ReportName | да | — | Имя отчёта (имя корневого XML) |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
| OutDir | нет | `build` | Каталог для результата |
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
Используй общий скрипт из epf-build:
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Сборка отчёта (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -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"
|
||||
```
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
---
|
||||
name: erf-dump
|
||||
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
|
||||
argument-hint: <ErfFile>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-dump — Разборка отчёта
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-dump <ErfFile> [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|----------|:------------:|--------------|-------------------------------------|
|
||||
| ErfFile | да | — | Путь к ERF-файлу |
|
||||
| OutDir | нет | `src` | Каталог для выгрузки исходников |
|
||||
|
||||
## Параметры подключения (обязательно)
|
||||
|
||||
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
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`
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
Используй общий скрипт из epf-dump:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Разборка отчёта (файловая база)
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -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"
|
||||
```
|
||||
---
|
||||
name: erf-dump
|
||||
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
|
||||
argument-hint: <ErfFile>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-dump — Разборка отчёта
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-dump <ErfFile> [OutDir]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|----------|:------------:|--------------|-------------------------------------|
|
||||
| ErfFile | да | — | Путь к ERF-файлу |
|
||||
| OutDir | нет | `src` | Каталог для выгрузки исходников |
|
||||
|
||||
## Параметры подключения (обязательно)
|
||||
|
||||
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
5. Если ветка не совпала — используй `default`
|
||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
Используй общий скрипт из epf-dump:
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
|
||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Разборка отчёта (файловая база)
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -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,68 +1,42 @@
|
||||
---
|
||||
name: erf-init
|
||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
|
||||
argument-hint: <Name> [Synonym] [--with-skd]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-init — Создание нового отчёта
|
||||
|
||||
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-----------|:------------:|--------------|---------------------------------------|
|
||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<SrcDir>/
|
||||
├── <Name>.xml # Корневой файл метаданных (4 UUID)
|
||||
└── <Name>/
|
||||
└── Ext/
|
||||
└── ObjectModule.bsl # Модуль объекта с 3 регионами
|
||||
```
|
||||
|
||||
При `--WithSKD` дополнительно:
|
||||
|
||||
```
|
||||
<SrcDir>/<Name>/
|
||||
Templates/
|
||||
├── ОсновнаяСхемаКомпоновкиДанных.xml # Метаданные макета
|
||||
└── ОсновнаяСхемаКомпоновкиДанных/
|
||||
└── Ext/
|
||||
└── Template.xml # Пустая СКД
|
||||
```
|
||||
|
||||
- Корневой XML содержит `MetaDataObject/ExternalReport` с пустыми `DefaultForm`, `MainDataCompositionSchema` и `ChildObjects`
|
||||
- При `--WithSKD` — `MainDataCompositionSchema` заполняется ссылкой на макет, `ChildObjects` содержит `<Template>`
|
||||
- ClassId фиксирован: `e41aff26-25cf-4bb6-b6c1-3f478a75f374`
|
||||
- Файл создаётся в UTF-8 с BOM
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить форму: `/form-add`
|
||||
- Добавить макет: `/template-add`
|
||||
- Добавить справку: `/help-add`
|
||||
- Собрать ERF: `/erf-build`
|
||||
---
|
||||
name: erf-init
|
||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
|
||||
argument-hint: <Name> [Synonym] [--with-skd]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /erf-init — Создание нового отчёта
|
||||
|
||||
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-----------|:------------:|--------------|---------------------------------------|
|
||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
- Добавить форму: `/form-add`
|
||||
- Добавить макет: `/template-add`
|
||||
- Добавить справку: `/help-add`
|
||||
- Собрать ERF: `/erf-build`
|
||||
|
||||
@@ -1,178 +1,180 @@
|
||||
# erf-init v1.0 — Init 1C external report scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
[string]$Synonym = $Name,
|
||||
|
||||
[string]$SrcDir = "src",
|
||||
|
||||
[switch]$WithSKD
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$uuid1 = [guid]::NewGuid().ToString()
|
||||
$uuid2 = [guid]::NewGuid().ToString()
|
||||
$uuid3 = [guid]::NewGuid().ToString()
|
||||
$uuid4 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Формируем Properties ---
|
||||
|
||||
$mainDCSValue = ""
|
||||
$childObjectsContent = ""
|
||||
|
||||
if ($WithSKD) {
|
||||
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
|
||||
$childObjectsContent = @"
|
||||
|
||||
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
|
||||
|
||||
"@
|
||||
}
|
||||
|
||||
$mainDCSElement = if ($mainDCSValue) {
|
||||
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
|
||||
} else {
|
||||
"<MainDataCompositionSchema/>"
|
||||
}
|
||||
|
||||
$childObjectsXml = if ($childObjectsContent) {
|
||||
"<ChildObjects>$childObjectsContent</ChildObjects>"
|
||||
} else {
|
||||
"<ChildObjects/>"
|
||||
}
|
||||
|
||||
$xml = @"
|
||||
<?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">
|
||||
<ExternalReport uuid="$uuid1">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
|
||||
<xr:TypeId>$uuid3</xr:TypeId>
|
||||
<xr:ValueId>$uuid4</xr:ValueId>
|
||||
</xr:GeneratedType>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$Name</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<DefaultForm/>
|
||||
<AuxiliaryForm/>
|
||||
$mainDCSElement
|
||||
<DefaultSettingsForm/>
|
||||
<AuxiliarySettingsForm/>
|
||||
<DefaultVariantForm/>
|
||||
<VariantsStorage/>
|
||||
<SettingsStorage/>
|
||||
</Properties>
|
||||
$childObjectsXml
|
||||
</ExternalReport>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
||||
$reportDir = Join-Path $SrcDir $Name
|
||||
|
||||
if (Test-Path $rootFile) {
|
||||
Write-Error "Файл уже существует: $rootFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $SrcDir)) {
|
||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
||||
}
|
||||
$extDir = Join-Path $reportDir "Ext"
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
||||
|
||||
# --- Модуль объекта ---
|
||||
|
||||
$moduleBsl = @"
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
"@
|
||||
|
||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||
|
||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
||||
Write-Host " Каталог: $reportDir"
|
||||
Write-Host " Модуль: $modulePath"
|
||||
|
||||
# --- СКД-макет (если --WithSKD) ---
|
||||
|
||||
if ($WithSKD) {
|
||||
$templatesDir = Join-Path $reportDir "Templates"
|
||||
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
|
||||
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
|
||||
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
|
||||
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
|
||||
|
||||
$skdUuid = [guid]::NewGuid().ToString()
|
||||
|
||||
$skdMetaXml = @"
|
||||
<?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">
|
||||
<Template uuid="$skdUuid">
|
||||
<Properties>
|
||||
<Name>$skdName</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Основная схема компоновки данных</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<TemplateType>DataCompositionSchema</TemplateType>
|
||||
</Properties>
|
||||
</Template>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
||||
|
||||
$skdContent = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dataSource>
|
||||
<name>ИсточникДанных1</name>
|
||||
<dataSourceType>Local</dataSourceType>
|
||||
</dataSource>
|
||||
</DataCompositionSchema>
|
||||
"@
|
||||
|
||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
||||
|
||||
Write-Host " СКД: $skdMetaPath"
|
||||
Write-Host " Тело: $skdFilePath"
|
||||
}
|
||||
# erf-init v1.1 — Init 1C external report scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
[string]$Synonym = $Name,
|
||||
|
||||
[string]$SrcDir = "src",
|
||||
|
||||
[switch]$WithSKD
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$uuid1 = [guid]::NewGuid().ToString()
|
||||
$uuid2 = [guid]::NewGuid().ToString()
|
||||
$uuid3 = [guid]::NewGuid().ToString()
|
||||
$uuid4 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Формируем Properties ---
|
||||
|
||||
$mainDCSValue = ""
|
||||
$childObjectsContent = ""
|
||||
|
||||
if ($WithSKD) {
|
||||
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
|
||||
$childObjectsContent = @"
|
||||
|
||||
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
|
||||
|
||||
"@
|
||||
}
|
||||
|
||||
$mainDCSElement = if ($mainDCSValue) {
|
||||
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
|
||||
} else {
|
||||
"<MainDataCompositionSchema/>"
|
||||
}
|
||||
|
||||
$childObjectsXml = if ($childObjectsContent) {
|
||||
"<ChildObjects>$childObjectsContent</ChildObjects>"
|
||||
} else {
|
||||
"<ChildObjects/>"
|
||||
}
|
||||
|
||||
$xml = @"
|
||||
<?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">
|
||||
<ExternalReport uuid="$uuid1">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
|
||||
<xr:TypeId>$uuid3</xr:TypeId>
|
||||
<xr:ValueId>$uuid4</xr:ValueId>
|
||||
</xr:GeneratedType>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$Name</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<DefaultForm/>
|
||||
<AuxiliaryForm/>
|
||||
$mainDCSElement
|
||||
<DefaultSettingsForm/>
|
||||
<AuxiliarySettingsForm/>
|
||||
<DefaultVariantForm/>
|
||||
<VariantsStorage/>
|
||||
<SettingsStorage/>
|
||||
</Properties>
|
||||
$childObjectsXml
|
||||
</ExternalReport>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
||||
$reportDir = Join-Path $SrcDir $Name
|
||||
|
||||
if (Test-Path $rootFile) {
|
||||
Write-Error "Файл уже существует: $rootFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $SrcDir)) {
|
||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
||||
}
|
||||
$extDir = Join-Path $reportDir "Ext"
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
||||
|
||||
# --- Модуль объекта ---
|
||||
|
||||
$moduleBsl = @"
|
||||
#Область ОписаниеПеременных
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ПрограммныйИнтерфейс
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
"@
|
||||
|
||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||
|
||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
||||
Write-Host " Каталог: $reportDir"
|
||||
Write-Host " Модуль: $modulePath"
|
||||
|
||||
# --- СКД-макет (если --WithSKD) ---
|
||||
|
||||
if ($WithSKD) {
|
||||
$templatesDir = Join-Path $reportDir "Templates"
|
||||
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
|
||||
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
|
||||
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
|
||||
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
|
||||
|
||||
$skdUuid = [guid]::NewGuid().ToString()
|
||||
|
||||
$skdMetaXml = @"
|
||||
<?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">
|
||||
<Template uuid="$skdUuid">
|
||||
<Properties>
|
||||
<Name>$skdName</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Основная схема компоновки данных</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<TemplateType>DataCompositionSchema</TemplateType>
|
||||
</Properties>
|
||||
</Template>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
||||
|
||||
$skdContent = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dataSource>
|
||||
<name>ИсточникДанных1</name>
|
||||
<dataSourceType>Local</dataSourceType>
|
||||
</dataSource>
|
||||
</DataCompositionSchema>
|
||||
"@
|
||||
|
||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
||||
|
||||
Write-Host " СКД: $skdMetaPath"
|
||||
Write-Host " Тело: $skdFilePath"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
"""Generates minimal XML source files for a 1C external report."""
|
||||
import sys, os, argparse, uuid
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
---
|
||||
name: erf-validate
|
||||
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /erf-validate — валидация внешнего отчёта (ERF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
|
||||
|
||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|-------------------------------------------------------|--------------|
|
||||
| 1 | Root structure: MetaDataObject/ExternalReport | ERROR |
|
||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
||||
| 3 | Properties: Name, Synonym, MainDataCompositionSchema | ERROR / WARN |
|
||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
||||
| 5 | Cross-references: DefaultForm, MainDCS → Template | ERROR / WARN |
|
||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
---
|
||||
name: erf-validate
|
||||
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /erf-validate — валидация внешнего отчёта (ERF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
|
||||
|
||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
|
||||
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,91 +1,71 @@
|
||||
---
|
||||
name: form-add
|
||||
description: Добавить управляемую форму к объекту конфигурации 1С
|
||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /form-add — Добавление формы к объекту конфигурации
|
||||
|
||||
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-------------|:------------:|--------------|----------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
||||
| FormName | да | — | Имя формы (ФормаДокумента) |
|
||||
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
|
||||
| Synonym | нет | = FormName | Синоним формы |
|
||||
| --set-default | нет | авто | Установить как форму по умолчанию |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||
```
|
||||
|
||||
## Purpose — назначение формы
|
||||
|
||||
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|
||||
|---------|-------------------------|-------------------|---------------------|
|
||||
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
|
||||
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
|
||||
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
|
||||
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<ObjectDir>/Forms/
|
||||
├── <FormName>.xml # Метаданные формы (UUID)
|
||||
└── <FormName>/
|
||||
└── Ext/
|
||||
├── Form.xml # Описание формы (logform namespace)
|
||||
└── Form/
|
||||
└── Module.bsl # BSL-модуль с 5 регионами + ПриСозданииНаСервере
|
||||
```
|
||||
|
||||
## Что модифицируется
|
||||
|
||||
- `<ObjectPath>` — добавляется `<Form>` в `ChildObjects` (перед `<Template>` или `<TabularSection>`), обновляется Default*Form (автоматически если пустое, или явно при `--set-default`)
|
||||
|
||||
## Поддерживаемые типы объектов
|
||||
|
||||
Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, InformationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ExchangePlan, BusinessProcess, Task
|
||||
|
||||
## Примеры
|
||||
|
||||
```
|
||||
# Форма документа
|
||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
|
||||
|
||||
# Форма списка каталога
|
||||
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
|
||||
|
||||
# Форма записи регистра сведений
|
||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
|
||||
|
||||
# Форма выбора с синонимом
|
||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
|
||||
|
||||
# Установить как форму по умолчанию
|
||||
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. `/form-add` — создать каркас формы
|
||||
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
|
||||
3. `/form-validate` — проверить корректность
|
||||
4. `/form-info` — проанализировать результат
|
||||
---
|
||||
name: form-add
|
||||
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /form-add — Добавление формы к объекту конфигурации
|
||||
|
||||
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-------------|:------------:|--------------|----------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
||||
| FormName | да | — | Имя формы (ФормаДокумента) |
|
||||
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
|
||||
| Synonym | нет | = FormName | Синоним формы |
|
||||
| --set-default | нет | авто | Установить как форму по умолчанию |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
python "${CLAUDE_SKILL_DIR}/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||
```
|
||||
|
||||
## Purpose — назначение формы
|
||||
|
||||
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|
||||
|---------|-------------------------|-------------------|---------------------|
|
||||
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
|
||||
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
|
||||
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
|
||||
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
|
||||
|
||||
## Примеры
|
||||
|
||||
```
|
||||
# Форма документа
|
||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
|
||||
|
||||
# Форма списка каталога
|
||||
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
|
||||
|
||||
# Форма записи регистра сведений
|
||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
|
||||
|
||||
# Форма выбора с синонимом
|
||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
|
||||
|
||||
# Установить как форму по умолчанию
|
||||
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. `/form-add` — создать каркас формы
|
||||
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
|
||||
3. `/form-validate` — проверить корректность
|
||||
4. `/form-info` — проанализировать результат
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.0 — Add managed form to 1C config object
|
||||
# form-add v1.7 — Add managed form to 1C config object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
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 = {
|
||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||||
"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):
|
||||
"""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"encoding='UTF-8'", b'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)
|
||||
@@ -49,13 +229,25 @@ def main():
|
||||
|
||||
# --- Phase 1: Determine object type ---
|
||||
|
||||
# Resolve ObjectPath (directory → .xml)
|
||||
if not os.path.isabs(object_path):
|
||||
object_path = os.path.join(os.getcwd(), object_path)
|
||||
if os.path.isdir(object_path):
|
||||
object_path = object_path.rstrip("/\\") + ".xml"
|
||||
dir_name = os.path.basename(object_path.rstrip("/\\"))
|
||||
candidate = os.path.join(object_path, dir_name + ".xml")
|
||||
sibling = os.path.join(os.path.dirname(object_path.rstrip("/\\")), dir_name + ".xml")
|
||||
if os.path.isfile(candidate):
|
||||
object_path = candidate
|
||||
elif os.path.isfile(sibling):
|
||||
object_path = sibling
|
||||
if not os.path.isfile(object_path):
|
||||
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
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)
|
||||
tree = etree.parse(object_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
@@ -63,7 +255,7 @@ def main():
|
||||
supported_types = [
|
||||
"Document", "Catalog", "DataProcessor", "Report",
|
||||
"ExternalDataProcessor", "ExternalReport",
|
||||
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||
"ExchangePlan", "BusinessProcess", "Task",
|
||||
]
|
||||
|
||||
@@ -160,7 +352,7 @@ def main():
|
||||
' 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' version="{format_version}">\n'
|
||||
f'\t<Form uuid="{form_uuid}">\n'
|
||||
'\t\t<Properties>\n'
|
||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||
@@ -177,8 +369,8 @@ def main():
|
||||
'\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\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
||||
+ '\t\t</Properties>\n'
|
||||
'\t</Form>\n'
|
||||
'</MetaDataObject>'
|
||||
)
|
||||
@@ -214,13 +406,10 @@ def main():
|
||||
|
||||
form_xml = (
|
||||
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\t<Autofill>true</Autofill>\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<Attributes>\n'
|
||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
||||
@@ -243,13 +432,10 @@ def main():
|
||||
|
||||
form_xml = (
|
||||
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\t<Autofill>true</Autofill>\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<Attributes>\n'
|
||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||
@@ -280,19 +466,22 @@ def main():
|
||||
"BusinessProcess": "BusinessProcessObject",
|
||||
"Task": "TaskObject",
|
||||
"InformationRegister": "InformationRegisterRecordManager",
|
||||
"AccumulationRegister": "AccumulationRegisterRecordSet",
|
||||
}
|
||||
|
||||
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 = (
|
||||
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\t<Autofill>true</Autofill>\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<Attributes>\n'
|
||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||
@@ -300,7 +489,7 @@ def main():
|
||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||
'\t\t\t</Type>\n'
|
||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||
'\t\t\t<SavedData>true</SavedData>\n'
|
||||
f'{saved_data_line}'
|
||||
'\t\t</Attribute>\n'
|
||||
'\t</Attributes>\n'
|
||||
'</Form>'
|
||||
@@ -318,11 +507,6 @@ def main():
|
||||
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'
|
||||
'&\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'
|
||||
'\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,415 +1,567 @@
|
||||
---
|
||||
name: form-compile
|
||||
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов
|
||||
argument-hint: <JsonPath> <OutputPath>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /form-compile — Генерация Form.xml из JSON DSL
|
||||
|
||||
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID.
|
||||
|
||||
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно.
|
||||
|
||||
## Использование
|
||||
|
||||
```
|
||||
/form-compile <JsonPath> <OutputPath>
|
||||
```
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|------------|:------------:|-----------------------------------|
|
||||
| JsonPath | да | Путь к JSON-определению формы |
|
||||
| OutputPath | да | Путь к выходному файлу Form.xml |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>"
|
||||
```
|
||||
|
||||
## JSON DSL — справка
|
||||
|
||||
### Структура верхнего уровня
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Заголовок формы",
|
||||
"properties": { "autoTitle": false, ... },
|
||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||
"excludedCommands": ["Reread"],
|
||||
"elements": [ ... ],
|
||||
"attributes": [ ... ],
|
||||
"commands": [ ... ],
|
||||
"parameters": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне
|
||||
- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др.
|
||||
- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры)
|
||||
- `excludedCommands` — исключённые стандартные команды
|
||||
|
||||
### Элементы (ключ определяет тип)
|
||||
|
||||
| DSL ключ | XML элемент | Значение ключа |
|
||||
|--------------|-------------------|---------------------------------------------------|
|
||||
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
|
||||
| `"input"` | InputField | имя элемента |
|
||||
| `"check"` | CheckBoxField | имя |
|
||||
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
||||
| `"labelField"` | LabelField | имя |
|
||||
| `"table"` | Table | имя |
|
||||
| `"pages"` | Pages | имя |
|
||||
| `"page"` | Page | имя |
|
||||
| `"button"` | Button | имя |
|
||||
| `"picture"` | PictureDecoration | имя |
|
||||
| `"picField"` | PictureField | имя |
|
||||
| `"calendar"` | CalendarField | имя |
|
||||
| `"cmdBar"` | CommandBar | имя |
|
||||
| `"popup"` | Popup | имя |
|
||||
|
||||
### Общие свойства (все типы элементов)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа) |
|
||||
| `title` | Заголовок элемента |
|
||||
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
||||
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
||||
| `readOnly: true` | Только чтение |
|
||||
| `on: [...]` | События с автоименованием обработчиков |
|
||||
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` |
|
||||
|
||||
### Допустимые имена событий (`on`)
|
||||
|
||||
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
||||
|
||||
**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening`
|
||||
|
||||
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
||||
|
||||
**check**: `OnChange`
|
||||
|
||||
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
||||
|
||||
**label / picture**: `Click`, `URLProcessing`
|
||||
|
||||
**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing`
|
||||
|
||||
**button**: `Click`
|
||||
|
||||
**pages**: `OnCurrentPageChange`
|
||||
|
||||
### Поле ввода (input)
|
||||
|
||||
| Ключ | Описание | Пример |
|
||||
|------|----------|--------|
|
||||
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
|
||||
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
||||
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
||||
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
||||
| `clearButton: true` | Кнопка очистки ("X") | |
|
||||
| `spinButton: true` | Кнопка прокрутки | числовые поля |
|
||||
| `dropListButton: true` | Кнопка выпадающего списка | |
|
||||
| `markIncomplete: true` | Пометка незаполненного | обязательные поля |
|
||||
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
||||
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
||||
| `width` / `height` | Размер | числа |
|
||||
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей |
|
||||
| `horizontalStretch: true` | Растягивать по ширине | |
|
||||
|
||||
### Чекбокс (check)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `path` | DataPath |
|
||||
| `titleLocation` | Размещение заголовка |
|
||||
|
||||
### Надпись-декорация (label)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `title` | Текст надписи (обязательно) |
|
||||
| `hyperlink: true` | Сделать ссылкой |
|
||||
| `width` / `height` | Размер |
|
||||
|
||||
### Группа (group)
|
||||
|
||||
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`.
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `showTitle: true` | Показывать заголовок группы |
|
||||
| `united: false` | Не объединять рамку |
|
||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||
| `children: [...]` | Вложенные элементы |
|
||||
|
||||
### Таблица (table)
|
||||
|
||||
**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки").
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `path` | DataPath (привязка к реквизиту-таблице) |
|
||||
| `columns: [...]` | Колонки — массив элементов (обычно `input`) |
|
||||
| `changeRowSet: true` | Разрешить добавление/удаление строк |
|
||||
| `changeRowOrder: true` | Разрешить перемещение строк |
|
||||
| `height` | Высота в строках таблицы |
|
||||
| `header: false` | Скрыть шапку |
|
||||
| `footer: true` | Показать подвал |
|
||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||
|
||||
### Страницы (pages + page)
|
||||
|
||||
| Ключ (pages) | Описание |
|
||||
|------|----------|
|
||||
| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. |
|
||||
| `children: [...]` | Массив `page` |
|
||||
|
||||
| Ключ (page) | Описание |
|
||||
|------|----------|
|
||||
| `title` | Заголовок вкладки |
|
||||
| `group` | Ориентация внутри страницы |
|
||||
| `children: [...]` | Содержимое страницы |
|
||||
|
||||
### Кнопка (button)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `command` | Имя команды формы → `Form.Command.Имя` |
|
||||
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
||||
| `defaultButton: true` | Кнопка по умолчанию |
|
||||
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` |
|
||||
| `picture` | Картинка кнопки |
|
||||
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
||||
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
||||
|
||||
### Командная панель (cmdBar)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `autofill: true` | Автозаполнение стандартными командами |
|
||||
| `children: [...]` | Кнопки панели |
|
||||
|
||||
### Выпадающее меню (popup)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `title` | Заголовок подменю |
|
||||
| `children: [...]` | Кнопки подменю |
|
||||
|
||||
Используется внутри `cmdBar` для группировки кнопок в подменю:
|
||||
```json
|
||||
{ "cmdBar": "Панель", "children": [
|
||||
{ "popup": "Добавить", "title": "Добавить", "children": [
|
||||
{ "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" },
|
||||
{ "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" }
|
||||
]}
|
||||
]}
|
||||
```
|
||||
|
||||
### Реквизиты (attributes)
|
||||
|
||||
```json
|
||||
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
|
||||
{ "name": "Итого", "type": "decimal(15,2)" }
|
||||
{ "name": "Таблица", "type": "ValueTable", "columns": [
|
||||
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
|
||||
{ "name": "Количество", "type": "decimal(10,3)" }
|
||||
]}
|
||||
```
|
||||
|
||||
- `savedData: true` — сохраняемые данные
|
||||
|
||||
### Команды (commands)
|
||||
|
||||
```json
|
||||
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
|
||||
```
|
||||
|
||||
- `title` — заголовок (если отличается от name)
|
||||
- `picture` — картинка команды
|
||||
|
||||
### Система типов
|
||||
|
||||
| DSL | XML |
|
||||
|------------------------|----------------------------------------|
|
||||
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
|
||||
| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers |
|
||||
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
|
||||
| `"boolean"` | `xs:boolean` |
|
||||
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
|
||||
| `"CatalogRef.XXX"` | `cfg:CatalogRef.XXX` |
|
||||
| `"DocumentRef.XXX"` | `cfg:DocumentRef.XXX` |
|
||||
| `"ValueTable"` | `v8:ValueTable` |
|
||||
| `"ValueList"` | `v8:ValueListType` |
|
||||
| `"Type1 \| Type2"` | составной тип |
|
||||
|
||||
## Связки: элемент + реквизит
|
||||
|
||||
Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`.
|
||||
|
||||
**Таблица** — элемент `table` + реквизит `ValueTable`:
|
||||
```json
|
||||
{
|
||||
"elements": [
|
||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true,
|
||||
"columns": [
|
||||
{ "name": "Товары", "type": "ValueTable", "columns": [
|
||||
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Или, если таблица привязана к реквизиту формы (не к Объект):
|
||||
```json
|
||||
{
|
||||
"elements": [
|
||||
{ "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [
|
||||
{ "input": "Наименование", "path": "ТаблицаДанных.Наименование" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "ТаблицаДанных", "type": "ValueTable", "columns": [
|
||||
{ "name": "Наименование", "type": "string(150)" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Паттерны
|
||||
|
||||
### Диалог загрузки файла
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Загрузка из файла",
|
||||
"properties": { "autoTitle": false },
|
||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] },
|
||||
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
||||
]},
|
||||
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
||||
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
|
||||
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
||||
{ "button": "Закрыть", "stdCommand": "Close" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true },
|
||||
{ "name": "ИмяФайла", "type": "string" },
|
||||
{ "name": "ПерваяСтрокаЗаголовок", "type": "boolean" },
|
||||
{ "name": "Результат", "type": "string" }
|
||||
],
|
||||
"commands": [
|
||||
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Мастер (wizard) с шагами
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Мастер настройки",
|
||||
"properties": { "autoTitle": false },
|
||||
"elements": [
|
||||
{ "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [
|
||||
{ "page": "Шаг1", "title": "Параметры", "children": [
|
||||
{ "input": "Параметр1", "path": "Параметр1" }
|
||||
]},
|
||||
{ "page": "Шаг2", "title": "Результат", "children": [
|
||||
{ "input": "Итог", "path": "Итог", "readOnly": true }
|
||||
]}
|
||||
]},
|
||||
{ "group": "horizontal", "name": "Навигация", "children": [
|
||||
{ "button": "Назад", "command": "Назад", "title": "< Назад" },
|
||||
{ "button": "Далее", "command": "Далее", "title": "Далее >" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true },
|
||||
{ "name": "Параметр1", "type": "string" },
|
||||
{ "name": "Итог", "type": "string" }
|
||||
],
|
||||
"commands": [
|
||||
{ "name": "Назад", "action": "НазадОбработка" },
|
||||
{ "name": "Далее", "action": "ДалееОбработка" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Список с фильтром и таблицей
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Просмотр данных",
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "Фильтр", "children": [
|
||||
{ "input": "Период", "path": "Период", "on": ["OnChange"] },
|
||||
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] }
|
||||
]},
|
||||
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
||||
{ "input": "Дата", "path": "Данные.Дата" },
|
||||
{ "input": "Сумма", "path": "Данные.Сумма" },
|
||||
{ "input": "Комментарий", "path": "Данные.Комментарий" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true },
|
||||
{ "name": "Период", "type": "date" },
|
||||
{ "name": "Организация", "type": "string" },
|
||||
{ "name": "Данные", "type": "ValueTable", "columns": [
|
||||
{ "name": "Дата", "type": "date" },
|
||||
{ "name": "Сумма", "type": "decimal(15,2)" },
|
||||
{ "name": "Комментарий", "type": "string(200)" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Автогенерация
|
||||
|
||||
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
||||
- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении`
|
||||
- **Namespace**: все 17 namespace-деклараций
|
||||
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
||||
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
|
||||
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
|
||||
3. **Проверка**: `/form-validate`, `/form-info`.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/form-validate <OutputPath> — проверка корректности XML
|
||||
/form-info <OutputPath> — визуальная сводка структуры
|
||||
```
|
||||
|
||||
## Особенности для внешних обработок (EPF)
|
||||
|
||||
- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`)
|
||||
- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных
|
||||
- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`)
|
||||
---
|
||||
name: form-compile
|
||||
description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
|
||||
argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /form-compile — Генерация Form.xml
|
||||
|
||||
Два режима:
|
||||
1. **JSON DSL** — из JSON-определения формы
|
||||
2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
|
||||
|
||||
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|------------|:------------:|---------------------------------|
|
||||
| JsonPath | режим 1 | Путь к JSON-определению формы |
|
||||
| OutputPath | да | Путь к выходному Form.xml |
|
||||
| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
# Режим 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
|
||||
{
|
||||
"title": "Заголовок формы",
|
||||
"properties": { "autoTitle": false, ... },
|
||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||
"excludedCommands": ["Reread"],
|
||||
"elements": [ ... ],
|
||||
"attributes": [ ... ],
|
||||
"commands": [ ... ],
|
||||
"parameters": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне
|
||||
- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др.
|
||||
- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры)
|
||||
- `excludedCommands` — исключённые стандартные команды
|
||||
|
||||
### Элементы (ключ определяет тип)
|
||||
|
||||
| DSL ключ | XML элемент | Значение ключа |
|
||||
|--------------|-------------------|---------------------------------------------------|
|
||||
| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
|
||||
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
|
||||
| `"input"` | InputField | имя элемента |
|
||||
| `"check"` | CheckBoxField | имя |
|
||||
| `"radio"` | RadioButtonField | имя |
|
||||
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
||||
| `"labelField"` | LabelField | имя |
|
||||
| `"table"` | Table | имя |
|
||||
| `"pages"` | Pages | имя |
|
||||
| `"page"` | Page | имя |
|
||||
| `"button"` | Button | имя |
|
||||
| `"picture"` | PictureDecoration | имя |
|
||||
| `"picField"` | PictureField | имя |
|
||||
| `"calendar"` | CalendarField | имя |
|
||||
| `"cmdBar"` | CommandBar | имя |
|
||||
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
|
||||
| `"popup"` | Popup | имя |
|
||||
|
||||
### Общие свойства (все типы элементов)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
|
||||
| `title` | Заголовок элемента |
|
||||
| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
|
||||
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
||||
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
||||
| `readOnly: true` | Только чтение |
|
||||
| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
|
||||
|
||||
### Допустимые имена событий (`events`)
|
||||
|
||||
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
||||
|
||||
**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening`
|
||||
|
||||
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
||||
|
||||
**check / radio**: `OnChange`
|
||||
|
||||
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
||||
|
||||
**label / picture**: `Click`, `URLProcessing`
|
||||
|
||||
**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing`
|
||||
|
||||
**button**: `Click`
|
||||
|
||||
**pages**: `OnCurrentPageChange`
|
||||
|
||||
### Поле ввода (input)
|
||||
|
||||
| Ключ | Описание | Пример |
|
||||
|------|----------|--------|
|
||||
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
|
||||
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
||||
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
||||
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
||||
| `clearButton: true` | Кнопка очистки ("X") | |
|
||||
| `spinButton: true` | Кнопка прокрутки | числовые поля |
|
||||
| `dropListButton: true` | Кнопка выпадающего списка | |
|
||||
| `markIncomplete: true` | Пометка незаполненного | обязательные поля |
|
||||
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
||||
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
||||
| `width` / `height` | Размер | числа |
|
||||
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
|
||||
| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
|
||||
| `horizontalStretch: true` | Растягивать по ширине | |
|
||||
|
||||
### Чекбокс (check)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `path` | DataPath |
|
||||
| `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)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `title` | Текст надписи (обязательно) |
|
||||
| `hyperlink: true` | Сделать ссылкой |
|
||||
| `width` / `height` | Размер |
|
||||
|
||||
### Группа (group)
|
||||
|
||||
Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
|
||||
| `showTitle: true` | Показывать заголовок группы |
|
||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||
| `children: [...]` | Вложенные элементы |
|
||||
|
||||
### Таблица (table)
|
||||
|
||||
**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки").
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `path` | DataPath (привязка к реквизиту-таблице) |
|
||||
| `columns: [...]` | Колонки — массив элементов (обычно `input`) |
|
||||
| `changeRowSet: true` | Разрешить добавление/удаление строк |
|
||||
| `changeRowOrder: true` | Разрешить перемещение строк |
|
||||
| `height` | Высота в строках таблицы |
|
||||
| `header: false` | Скрыть шапку |
|
||||
| `footer: true` | Показать подвал |
|
||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"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) | Описание |
|
||||
|------|----------|
|
||||
| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. |
|
||||
| `children: [...]` | Массив `page` |
|
||||
|
||||
| Ключ (page) | Описание |
|
||||
|------|----------|
|
||||
| `title` | Заголовок вкладки |
|
||||
| `group` | Ориентация внутри страницы |
|
||||
| `children: [...]` | Содержимое страницы |
|
||||
|
||||
### Кнопка (button)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `command` | Имя команды формы → `Form.Command.Имя` |
|
||||
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
||||
| `defaultButton: true` | Кнопка по умолчанию |
|
||||
| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
|
||||
| `picture` | Картинка кнопки |
|
||||
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
||||
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
||||
|
||||
### Командная панель (cmdBar)
|
||||
|
||||
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `autofill: true` | Автозаполнение стандартными командами |
|
||||
| `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)
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `title` | Заголовок подменю |
|
||||
| `children: [...]` | Кнопки подменю |
|
||||
|
||||
Используется внутри `cmdBar` для группировки кнопок в подменю:
|
||||
```json
|
||||
{ "cmdBar": "Панель", "children": [
|
||||
{ "popup": "Добавить", "title": "Добавить", "children": [
|
||||
{ "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" },
|
||||
{ "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" }
|
||||
]}
|
||||
]}
|
||||
```
|
||||
|
||||
### Реквизиты (attributes)
|
||||
|
||||
```json
|
||||
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
|
||||
{ "name": "Список", "type": "DynamicList", "main": true, "settings": {
|
||||
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
|
||||
}}
|
||||
{ "name": "Итого", "type": "decimal(15,2)" }
|
||||
{ "name": "Таблица", "type": "ValueTable", "columns": [
|
||||
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
|
||||
{ "name": "Количество", "type": "decimal(10,3)" }
|
||||
]}
|
||||
```
|
||||
|
||||
- `savedData: true` — сохраняемые данные
|
||||
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
|
||||
|
||||
### Команды (commands)
|
||||
|
||||
```json
|
||||
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
|
||||
```
|
||||
|
||||
- `title` — заголовок (если отличается от name)
|
||||
- `picture` — картинка команды
|
||||
|
||||
### Система типов
|
||||
|
||||
**Примитивные:**
|
||||
|
||||
| DSL | XML |
|
||||
|------------------------|----------------------------------------|
|
||||
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
|
||||
| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers |
|
||||
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
|
||||
| `"boolean"` | `xs:boolean` |
|
||||
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
|
||||
|
||||
**Ссылочные и объектные (`cfg:Prefix.Name`):**
|
||||
|
||||
| DSL | Описание |
|
||||
|-----|----------|
|
||||
| `"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`.
|
||||
|
||||
## Связки: элемент + реквизит
|
||||
|
||||
Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`.
|
||||
|
||||
**Таблица** — элемент `table` + реквизит `ValueTable`:
|
||||
```json
|
||||
{
|
||||
"elements": [
|
||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true,
|
||||
"columns": [
|
||||
{ "name": "Товары", "type": "ValueTable", "columns": [
|
||||
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Или, если таблица привязана к реквизиту формы (не к Объект):
|
||||
```json
|
||||
{
|
||||
"elements": [
|
||||
{ "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [
|
||||
{ "input": "Наименование", "path": "ТаблицаДанных.Наименование" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "ТаблицаДанных", "type": "ValueTable", "columns": [
|
||||
{ "name": "Наименование", "type": "string(150)" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Паттерны
|
||||
|
||||
### Диалог загрузки файла
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Загрузка из файла",
|
||||
"properties": { "autoTitle": false },
|
||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
|
||||
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
||||
]},
|
||||
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
||||
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
|
||||
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
||||
{ "button": "Закрыть", "stdCommand": "Close" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true },
|
||||
{ "name": "ИмяФайла", "type": "string" },
|
||||
{ "name": "ПерваяСтрокаЗаголовок", "type": "boolean" },
|
||||
{ "name": "Результат", "type": "string" }
|
||||
],
|
||||
"commands": [
|
||||
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Мастер (wizard) с шагами
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Мастер настройки",
|
||||
"properties": { "autoTitle": false },
|
||||
"elements": [
|
||||
{ "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [
|
||||
{ "page": "Шаг1", "title": "Параметры", "children": [
|
||||
{ "input": "Параметр1", "path": "Параметр1" }
|
||||
]},
|
||||
{ "page": "Шаг2", "title": "Результат", "children": [
|
||||
{ "input": "Итог", "path": "Итог", "readOnly": true }
|
||||
]}
|
||||
]},
|
||||
{ "group": "horizontal", "name": "Навигация", "children": [
|
||||
{ "button": "Назад", "command": "Назад", "title": "< Назад" },
|
||||
{ "button": "Далее", "command": "Далее", "title": "Далее >" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true },
|
||||
{ "name": "Параметр1", "type": "string" },
|
||||
{ "name": "Итог", "type": "string" }
|
||||
],
|
||||
"commands": [
|
||||
{ "name": "Назад", "action": "НазадОбработка" },
|
||||
{ "name": "Далее", "action": "ДалееОбработка" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Список с фильтром и таблицей
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Просмотр данных",
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "Фильтр", "children": [
|
||||
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
|
||||
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
|
||||
]},
|
||||
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
||||
{ "input": "Дата", "path": "Данные.Дата" },
|
||||
{ "input": "Сумма", "path": "Данные.Сумма" },
|
||||
{ "input": "Комментарий", "path": "Данные.Комментарий" }
|
||||
]}
|
||||
],
|
||||
"attributes": [
|
||||
{ "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true },
|
||||
{ "name": "Период", "type": "date" },
|
||||
{ "name": "Организация", "type": "string" },
|
||||
{ "name": "Данные", "type": "ValueTable", "columns": [
|
||||
{ "name": "Дата", "type": "date" },
|
||||
{ "name": "Сумма", "type": "decimal(15,2)" },
|
||||
{ "name": "Комментарий", "type": "string(200)" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Продвинутые конструкции (по необходимости)
|
||||
|
||||
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `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 и др. создаются автоматически
|
||||
- **Namespace**: все 17 namespace-деклараций
|
||||
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
||||
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
|
||||
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
|
||||
3. **Проверка**: `/form-validate`, `/form-info`.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/form-validate <OutputPath> — проверка корректности XML
|
||||
/form-info <OutputPath> — визуальная сводка структуры
|
||||
```
|
||||
|
||||
## Особенности для внешних обработок (EPF)
|
||||
|
||||
- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`)
|
||||
- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных
|
||||
- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`)
|
||||
|
||||
@@ -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 } } }
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user