merge dev: meta-compile v1.2, meta-edit v1.4, batch mode, validation improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-08 18:46:52 +03:00
13 changed files with 1526 additions and 212 deletions
+60 -121
View File
@@ -1,6 +1,6 @@
---
name: meta-compile
description: Создать исходники объекта метаданных 1С (справочник, документ, регистр, перечисление, константа, общий модуль, обработка, HTTP-сервис и др.) в выгрузке конфигурации. Используй когда пользователь просит добавить или создать объект конфигурации
description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
argument-hint: <JsonPath> <OutputDir>
allowed-tools:
- Bash
@@ -13,101 +13,91 @@ allowed-tools:
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
## Параметры и команда
## Порядок работы
| Параметр | Описание |
|----------|----------|
| `JsonPath` | Путь к JSON-определению объекта |
| `OutputDir` | Корневая директория выгрузки конфигурации (где `Catalogs/`, `Documents/` и т.д.) |
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
2. Запусти скрипт meta-compile
3. Если нужно изменить созданный объект — `/meta-edit`
4. Если нужно проверить — `/meta-validate`
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-compile/scripts/meta-compile.ps1 -JsonPath "<json>" -OutputDir "<ConfigDir>"
```
`OutputDir` — директория, содержащая подпапки типов (`Catalogs/`, `Documents/`, ...) и `Configuration.xml`.
| Параметр | Описание |
|----------|----------|
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
## Поддерживаемые типы (23)
## JSON DSL
### Ссылочные
Catalog (Справочник), Document (Документ), Enum (Перечисление), ExchangePlan (ПланОбмена), ChartOfAccounts (ПланСчетов), ChartOfCharacteristicTypes (ПВХ), ChartOfCalculationTypes (ПВР), BusinessProcess (БизнесПроцесс), Task (Задача)
### Регистры
InformationRegister (РегистрСведений), AccumulationRegister (РегистрНакопления), AccountingRegister (РегистрБухгалтерии), CalculationRegister (РегистрРасчёта)
### Отчёты/Обработки
Report (Отчёт), DataProcessor (Обработка)
### Сервисные
Constant (Константа), DefinedType (ОпределяемыйТип), CommonModule (ОбщийМодуль), ScheduledJob (РегламентноеЗадание), EventSubscription (ПодпискаНаСобытие), DocumentJournal (ЖурналДокументов), HTTPService (HTTPСервис), WebService (ВебСервис)
## JSON DSL — краткий справочник
Полная спецификация: `docs/meta-dsl-spec.md`.
### Корневая структура
### Общая структура
```json
{
"type": "Catalog",
"name": "Номенклатура",
"synonym": "авто из name",
...type-specific...,
"attributes": [...],
"tabularSections": {...}
}
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
```
### Реквизиты — shorthand
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
### Shorthand реквизитов
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
```
"ИмяРеквизита" String без квалификаторов
"ИмяРеквизита: Тип" с типом
"ИмяРеквизита: Тип | req, index" с флагами
"ИмяРеквизита" String(10) по умолчанию
"ИмяРеквизита: Тип" с типом
"ИмяРеквизита: Тип | req, index" с флагами
```
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `ChartOfAccountsRef.Xxx`, `ChartOfCharacteristicTypesRef.Xxx`, `ChartOfCalculationTypesRef.Xxx`, `ExchangePlanRef.Xxx`, `BusinessProcessRef.Xxx`, `TaskRef.Xxx`, `DefinedType.Xxx`.
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
Русские синонимы типов: `Строка`, `Число`, `Булево`, `Дата`, `СправочникСсылка.Xxx`, `ДокументСсылка.Xxx`, `ПланСчетовСсылка.Xxx`.
Составной тип (несколько допустимых типов через `+`): `"Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"`.
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
## Примеры
### Свойства по типам
### Справочник
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
- `reference/types-web.md` — HTTPService, WebService
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
## Примеры паттернов DSL
### Минимальный объект
```json
{ "type": "Catalog", "name": "Валюты" }
```
### Перечисление
### С реквизитами
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] }
{
"type": "Catalog", "name": "Организации",
"descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
}
```
### Константа
### С табличной частью
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
{
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
}
```
### Определяемый тип
```json
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
```
### Общий модуль
```json
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
```
Шорткаты context: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
### Регистр сведений
### Регистровый паттерн (измерения + ресурсы)
```json
{
@@ -117,70 +107,19 @@ Constant (Константа), DefinedType (ОпределяемыйТип), Com
}
```
### План обмена
### Batch — несколько объектов в одном файле
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
```
### Журнал документов
```json
{
"type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.ТелефонныйЗвонок"],
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
}
```
### HTTP-сервис
```json
{
"type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": { "Users": { "template": "/v1/users", "methods": { "Get": "GET", "Create": "POST" } } }
}
```
### Веб-сервис
```json
{
"type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
"operations": { "TestConnection": { "returnType": "xs:boolean", "handler": "ПроверкаПодключения", "parameters": { "ErrorMessage": { "type": "xs:string", "direction": "Out" } } } }
}
```
### План счетов
```json
{
"type": "ChartOfAccounts", "name": "Хозрасчетный",
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
"codeMask": "@@@.@@.@", "codeLength": 8,
"accountingFlags": ["Валютный", "Количественный"],
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
}
```
### Бизнес-процесс
```json
{ "type": "BusinessProcess", "name": "Задание", "attributes": ["Описание: String(200)"] }
[
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Catalog", "name": "Валюты" },
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
]
```
## Что генерируется
- `{OutputDir}/{TypePlural}/{Name}.xml` — метаданные объекта
- `{OutputDir}/{TypePlural}/{Name}/Ext/ObjectModule.bsl` — модуль объекта (Catalog, Document, Report, DataProcessor, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task)
- `{OutputDir}/{TypePlural}/{Name}/Ext/RecordSetModule.bsl` — модуль набора записей (4 типа регистров)
- `{OutputDir}/{TypePlural}/{Name}/Ext/Module.bsl` — модуль (CommonModule, HTTPService, WebService)
- `{OutputDir}/{TypePlural}/{Name}/Ext/Content.xml` — состав плана обмена (ExchangePlan)
- `{OutputDir}/{TypePlural}/{Name}/Ext/Flowchart.xml` — карта маршрута (BusinessProcess)
- `{TypePlural}/{Name}.xml` — метаданные объекта
- `{TypePlural}/{Name}/Ext/*.bsl` — модули (ObjectModule, RecordSetModule, Module — зависит от типа)
- `Configuration.xml` — автоматическая регистрация в `<ChildObjects>`
## Верификация
```
/meta-info <OutputDir>/<TypePlural>/<Name>.xml — проверка структуры
```
@@ -0,0 +1,109 @@
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
## Catalog
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `hierarchical` | `false` | Hierarchical |
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
| `codeLength` | `9` | CodeLength |
| `codeType` | `String` | CodeType |
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Document
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
| `numberPeriodicity` | `Year` | NumberPeriodicity |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `posting` | `Allow` | Posting |
| `realTimePosting` | `Deny` | RealTimePosting |
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
| `registerRecords` | `[]` | RegisterRecords |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
```json
{
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
}
```
## Enum
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `values` | `[]` | → EnumValue в ChildObjects |
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
```
## Constant
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueType` | `String` | Type |
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
```
## DefinedType
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueTypes` | `[]` | Type (составной тип) |
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
```json
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
## Report
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Report", "name": "ОстаткиТоваров" }
```
## DataProcessor
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
```
@@ -0,0 +1,136 @@
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
## BusinessProcess
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `task` | `""` | Task (ссылка `Task.XXX`) |
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
```json
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
```
## Task
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `14` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `descriptionLength` | `150` | DescriptionLength |
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
| `currentPerformer` | `""` | CurrentPerformer |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
```json
{
"type": "Task", "name": "ЗадачаИсполнителя",
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
}
```
## ExchangePlan
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `100` | DescriptionLength |
| `distributedInfoBase` | `false` | DistributedInfoBase |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
```
## CommonModule
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `context` | — | Шорткат (см. ниже) |
| `global` | `false` | Global |
| `server` | `false` | Server |
| `serverCall` | `false` | ServerCall |
| `clientManagedApplication` | `false` | ClientManagedApplication |
| `externalConnection` | `false` | ExternalConnection |
| `privileged` | `false` | Privileged |
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
```
## ScheduledJob
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `methodName` | `""` | MethodName |
| `description` | = synonym | Description |
| `use` | `false` | Use |
| `predefined` | `false` | Predefined |
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
```
## EventSubscription
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
| `event` | `BeforeWrite` | Event |
| `handler` | `""` | Handler |
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
```json
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
```
## DocumentJournal
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
| `columns` | `[]` | → Column |
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
```json
{
"type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
}
```
## Зависимости
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
- **BusinessProcess** → `Task` (задача должна существовать)
@@ -0,0 +1,174 @@
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
## Измерения и ресурсы (общее)
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
```json
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
"resources": ["Сумма: Number(15,2)"]
```
---
## InformationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `writeMode` | `Independent` | WriteMode |
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
```json
{
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
}
```
## AccumulationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
"resources": ["Количество: Number(15,3)"]
}
```
## AccountingRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
| `correspondence` | `false` | Correspondence |
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccountingRegister", "name": "Хозрасчетный",
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
"dimensions": ["Организация: CatalogRef.Организации"],
"resources": ["Сумма: Number(15,2)"]
}
```
## CalculationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
| `periodicity` | `Month` | Periodicity |
| `actionPeriod` | `false` | ActionPeriod |
| `basePeriod` | `false` | BasePeriod |
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "CalculationRegister", "name": "Начисления",
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
"periodicity": "Month",
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
"resources": ["Сумма: Number(15,2)"]
}
```
---
## ChartOfCharacteristicTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `characteristicExtValues` | `""` | CharacteristicExtValues |
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
| `hierarchical` | `false` | Hierarchical |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
```json
{
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
}
```
## ChartOfAccounts
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
| `codeMask` | `""` | CodeMask |
| `codeLength` | `8` | CodeLength |
| `descriptionLength` | `120` | DescriptionLength |
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
| `autoOrderByCode` | `true` | AutoOrderByCode |
| `orderLength` | `5` | OrderLength |
| `hierarchical` | `false` | Hierarchical |
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
```json
{
"type": "ChartOfAccounts", "name": "Хозрасчетный",
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
"codeLength": 8, "codeMask": "@@@.@@.@",
"accountingFlags": ["Валютный", "Количественный"],
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
}
```
## ChartOfCalculationTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `dependenceOnCalculationTypes` | `NotUsed` | DependenceOnCalculationTypes |
| `actionPeriodUse` | `false` | ActionPeriodUse |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
`dependenceOnCalculationTypes`: `NotUsed`, `ExclusionAndDependence`, `ExclusionOnly`.
```json
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "ExclusionAndDependence" }
```
## Зависимости
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
@@ -0,0 +1,103 @@
# Веб-сервисы: HTTPService, WebService
## HTTPService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `rootURL` | `= name.toLower()` | RootURL |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `urlTemplates` | `{}` | → URLTemplate |
Модули: `Ext/Module.bsl`.
### urlTemplates — вложенная структура
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
Каждый `templateDef`:
- Строка — URL-шаблон: `"/v1/users"` (без методов)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
```json
{
"type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": {
"Users": {
"template": "/v1/users/{id}",
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
},
"Health": "/health"
}
}
```
## WebService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
| `xdtoPackages` | `""` | XDTOPackages |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `operations` | `{}` | → Operation |
Модули: `Ext/Module.bsl`.
### operations — вложенная структура
`operations` — объект `{ "OperationName": operationDef, ... }`.
Каждый `operationDef`:
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `returnType` | `xs:string` | XDTO-тип возврата |
| `nillable` | `false` | Может ли вернуть null |
| `transactioned` | `false` | Выполнять в транзакции |
| `handler` | `= operationName` | Имя процедуры в модуле |
| `parameters` | `{}` | Параметры операции |
### parameters — параметры операции
`parameters` — объект `{ "ParamName": paramDef, ... }`.
Каждый `paramDef`:
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `type` | `xs:string` | XDTO-тип параметра |
| `nillable` | `true` | Может ли быть null |
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
```json
{
"type": "WebService", "name": "DataExchange",
"namespace": "http://www.1c.ru/DataExchange",
"operations": {
"TestConnection": {
"returnType": "xs:boolean",
"handler": "ПроверкаПодключения",
"parameters": {
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
}
},
"GetVersion": "xs:string"
}
}
```
@@ -1,4 +1,4 @@
# meta-compile v1.0 — Compile 1C metadata object from JSON
# meta-compile v1.2 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -21,6 +21,28 @@ if (-not (Test-Path $JsonPath)) {
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
# --- Batch mode: JSON array of objects ---
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
$batchOk = 0
$batchFail = 0
$idx = 0
foreach ($item in $def) {
$idx++
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json"
try {
$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
if ($proc.ExitCode -eq 0) { $batchOk++ } else { $batchFail++ }
} finally {
Remove-Item $tmpJson -Force -ErrorAction SilentlyContinue
}
}
Write-Host ""
Write-Host "=== Batch: $idx objects, $batchOk compiled, $batchFail failed ==="
if ($batchFail -gt 0) { exit 1 }
exit 0
}
# Normalize field synonyms: accept "objectType" as alias for "type"
if (-not $def.type -and $def.objectType) {
$def | Add-Member -NotePropertyName "type" -NotePropertyValue $def.objectType
@@ -163,6 +185,10 @@ $script:typeSynonyms["бизнеспроцессссылка"] = "Bus
$script:typeSynonyms["задачассылка"] = "TaskRef"
$script:typeSynonyms["определяемыйтип"] = "DefinedType"
$script:typeSynonyms["definedtype"] = "DefinedType"
# English lowercase ref synonyms
$script:typeSynonyms["catalogref"] = "CatalogRef"
$script:typeSynonyms["documentref"] = "DocumentRef"
$script:typeSynonyms["enumref"] = "EnumRef"
function Resolve-TypeStr {
param([string]$typeStr)
@@ -217,7 +243,7 @@ function Emit-TypeContent {
# String or String(N)
if ($typeStr -match '^String(\((\d+)\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
$len = if ($Matches[2]) { $Matches[2] } else { "10" }
X "$indent<v8:Type>xs:string</v8:Type>"
X "$indent<v8:StringQualifiers>"
X "$indent`t<v8:Length>$len</v8:Length>"
@@ -226,6 +252,17 @@ function Emit-TypeContent {
return
}
# Number without params → Number(10,0)
if ($typeStr -eq "Number") {
X "$indent<v8:Type>xs:decimal</v8:Type>"
X "$indent<v8:NumberQualifiers>"
X "$indent`t<v8:Digits>10</v8:Digits>"
X "$indent`t<v8:FractionDigits>0</v8:FractionDigits>"
X "$indent`t<v8:AllowedSign>Any</v8:AllowedSign>"
X "$indent</v8:NumberQualifiers>"
return
}
# Number(D,F) or Number(D,F,nonneg)
if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') {
$digits = $Matches[1]
@@ -263,6 +300,12 @@ function Emit-TypeContent {
return
}
# ValueStorage
if ($typeStr -eq "ValueStorage") {
X "$indent<v8:Type>xs:base64Binary</v8:Type>"
return
}
# Reference types — use local xmlns declaration for 1C compatibility
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') {
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
@@ -311,6 +354,21 @@ function Emit-FillValue {
# --- 5. Attribute shorthand parser ---
function Build-TypeStr {
param($obj)
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
if ($t -and -not $t.Contains('(')) {
if ($t -eq "String" -and $obj.length) {
$t = "String($($obj.length))"
} elseif ($t -eq "Number" -and $obj.length) {
$p = if ($obj.precision) { $obj.precision } else { 0 }
$nn = if ($obj.nonneg -or $obj.nonnegative) { ",nonneg" } else { "" }
$t = "Number($($obj.length),$p$nn)"
}
}
return $t
}
function Parse-AttributeShorthand {
param($val)
@@ -347,7 +405,7 @@ function Parse-AttributeShorthand {
$name = "$($val.name)"
return @{
name = $name
type = if ($val.type) { "$($val.type)" } else { "" }
type = Build-TypeStr $val
synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name }
comment = if ($val.comment) { "$($val.comment)" } else { "" }
flags = @(if ($val.flags) { $val.flags } else { @() })
@@ -436,13 +494,16 @@ $script:generatedTypes = @{
@{ prefix = "CalculationRegisterList"; category = "List" }
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
)
"ChartOfAccounts" = @(
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
@{ prefix = "ChartOfAccountsRef"; category = "Ref" }
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
@{ prefix = "ChartOfAccountsList"; category = "List" }
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
@{ prefix = "ChartOfAccountsRef"; category = "Ref" }
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
@{ prefix = "ChartOfAccountsList"; category = "List" }
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
)
"ChartOfCharacteristicTypes" = @(
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
@@ -487,6 +548,9 @@ $script:generatedTypes = @{
@{ prefix = "ExchangePlanList"; category = "List" }
@{ prefix = "ExchangePlanManager"; category = "Manager" }
)
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"DocumentJournal" = @(
@{ prefix = "DocumentJournalSelection"; category = "Selection" }
@{ prefix = "DocumentJournalList"; category = "List" }
@@ -592,9 +656,26 @@ function Emit-TabularStandardAttributes {
# --- 8. Attribute emitter ---
$script:reservedAttrNames = @{
"Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование"
"Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец"
"IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных"
"Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность"
"Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый"
"Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача"
"Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс"
"ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого"
"CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись"
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
}
function Emit-Attribute {
param([string]$indent, $parsed, [string]$context)
# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register"
$attrName = $parsed.name
if ($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName)) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
}
$uuid = New-Guid-String
X "$indent<Attribute uuid=`"$uuid`">"
X "$indent`t<Properties>"
@@ -1055,7 +1136,7 @@ function Emit-DocumentProperties {
if ($regRecords.Count -gt 0) {
X "$i<RegisterRecords>"
foreach ($rr in $regRecords) {
X "$i`t<xr:Record>$rr</xr:Record>"
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$rr</xr:Item>"
}
X "$i</RegisterRecords>"
} else {
@@ -1117,7 +1198,8 @@ function Emit-ConstantProperties {
X "$i<Comment/>"
# Type
$valueType = if ($def.valueType) { "$($def.valueType)" } else { "String" }
$valueType = Build-TypeStr $def
if (-not $valueType) { $valueType = "String" }
Emit-ValueType $i $valueType
X "$i<UseStandardCommands>true</UseStandardCommands>"
@@ -1242,10 +1324,12 @@ function Emit-DefinedTypeProperties {
Emit-MLText $i "Synonym" $synonym
X "$i<Comment/>"
# Type — composite type with multiple v8:Type entries
# Type — composite type with multiple v8:Type entries (accept both valueType and valueTypes)
$valueTypes = @()
if ($def.valueTypes) {
$valueTypes = @($def.valueTypes)
} elseif ($def.valueType) {
$valueTypes = @($def.valueType)
}
if ($valueTypes.Count -gt 0) {
X "$i<Type>"
@@ -1322,6 +1406,10 @@ function Emit-ScheduledJobProperties {
X "$i<Comment/>"
$methodName = if ($def.methodName) { "$($def.methodName)" } else { "" }
# Ensure CommonModule. prefix
if ($methodName -and -not $methodName.StartsWith("CommonModule.")) {
$methodName = "CommonModule.$methodName"
}
X "$i<MethodName>$(Esc-Xml $methodName)</MethodName>"
$description = if ($def.description) { "$($def.description)" } else { $synonym }
@@ -1368,6 +1456,10 @@ function Emit-EventSubscriptionProperties {
X "$i<Event>$event</Event>"
$handler = if ($def.handler) { "$($def.handler)" } else { "" }
# Ensure CommonModule. prefix
if ($handler -and -not $handler.StartsWith("CommonModule.")) {
$handler = "CommonModule.$handler"
}
X "$i<Handler>$(Esc-Xml $handler)</Handler>"
}
@@ -1440,19 +1532,13 @@ function Emit-ExchangePlanProperties {
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "100" }
$codeType = if ($def.codeType) { "$($def.codeType)" } else { "String" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }
X "$i<CodeLength>$codeLength</CodeLength>"
X "$i<CodeType>$codeType</CodeType>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<DefaultPresentation>AsDescription</DefaultPresentation>"
X "$i<EditType>InDialog</EditType>"
X "$i<CheckUnique>$checkUnique</CheckUnique>"
X "$i<Autonumbering>$autonumbering</Autonumbering>"
Emit-StandardAttributes $i "ExchangePlan"
@@ -1509,13 +1595,11 @@ function Emit-ChartOfCharacteristicTypesProperties {
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
$codeType = if ($def.codeType) { "$($def.codeType)" } else { "String" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }
X "$i<CodeLength>$codeLength</CodeLength>"
X "$i<CodeType>$codeType</CodeType>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<CheckUnique>$checkUnique</CheckUnique>"
@@ -1541,7 +1625,7 @@ function Emit-ChartOfCharacteristicTypesProperties {
X "$i`t<v8:Type>xs:boolean</v8:Type>"
X "$i`t<v8:Type>xs:string</v8:Type>"
X "$i`t<v8:StringQualifiers>"
X "$i`t`t<v8:Length>0</v8:Length>"
X "$i`t`t<v8:Length>100</v8:Length>"
X "$i`t`t<v8:AllowedLength>Variable</v8:AllowedLength>"
X "$i`t</v8:StringQualifiers>"
X "$i`t<v8:Type>xs:decimal</v8:Type>"
@@ -1685,14 +1769,10 @@ function Emit-ChartOfAccountsProperties {
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<CodeSeries>$codeSeries</CodeSeries>"
X "$i<CheckUnique>false</CheckUnique>"
X "$i<Autonumbering>true</Autonumbering>"
X "$i<DefaultPresentation>AsDescription</DefaultPresentation>"
X "$i<AutoOrderByCode>$autoOrder</AutoOrderByCode>"
X "$i<OrderLength>$orderLength</OrderLength>"
$hierarchical = if ($def.hierarchical -eq $true) { "true" } else { "false" }
X "$i<Hierarchical>$hierarchical</Hierarchical>"
X "$i<EditType>InDialog</EditType>"
Emit-StandardAttributes $i "ChartOfAccounts"
@@ -1796,18 +1876,14 @@ function Emit-ChartOfCalculationTypesProperties {
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
$codeType = if ($def.codeType) { "$($def.codeType)" } else { "String" }
$codeAllowedLength = if ($def.codeAllowedLength) { "$($def.codeAllowedLength)" } else { "Variable" }
$autonumbering = if ($def.autonumbering -eq $false) { "false" } else { "true" }
$checkUnique = if ($def.checkUnique -eq $true) { "true" } else { "false" }
X "$i<CodeLength>$codeLength</CodeLength>"
X "$i<CodeType>$codeType</CodeType>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<DefaultPresentation>AsDescription</DefaultPresentation>"
X "$i<CheckUnique>$checkUnique</CheckUnique>"
X "$i<Autonumbering>$autonumbering</Autonumbering>"
$dependence = if ($def.dependenceOnCalculationTypes) { "$($def.dependenceOnCalculationTypes)" } else { "NotUsed" }
$dependence = if ($def.dependenceOnCalculationTypes) { "$($def.dependenceOnCalculationTypes)" } else { "DontUse" }
X "$i<DependenceOnCalculationTypes>$dependence</DependenceOnCalculationTypes>"
# BaseCalculationTypes
@@ -1941,6 +2017,13 @@ function Emit-BusinessProcessProperties {
Emit-StandardAttributes $i "BusinessProcess"
X "$i<Characteristics/>"
$task = if ($def.task) { "$($def.task)" } else { "" }
if ($task) {
X "$i<Task>$task</Task>"
} else {
X "$i<Task/>"
}
X "$i<BasedOn/>"
X "$i<InputByString>"
X "$i`t<xr:Field>BusinessProcess.$objName.StandardAttribute.Number</xr:Field>"
@@ -2330,13 +2413,11 @@ function Emit-AddressingAttribute {
$addressingDimension = ""
$indexing = "Index"
if ($addrDef -is [string]) {
$name = "$addrDef"
$attrSynonym = Split-CamelCase $name
} else {
$name = "$($addrDef.name)"
$attrSynonym = if ($addrDef.synonym) { "$($addrDef.synonym)" } else { Split-CamelCase $name }
if ($addrDef.type) { $typeStr = "$($addrDef.type)" }
$parsed = Parse-AttributeShorthand $addrDef
$name = $parsed.name
$attrSynonym = $parsed.synonym
$typeStr = $parsed.type
if ($addrDef -isnot [string]) {
if ($addrDef.addressingDimension) { $addressingDimension = "$($addrDef.addressingDimension)" }
if ($addrDef.indexing) { $indexing = "$($addrDef.indexing)" }
}
@@ -2708,7 +2789,7 @@ if ($objType -in $typesWithModule) {
if ($objType -eq "ExchangePlan") {
$contentPath = Join-Path $extDir "Content.xml"
if (-not (Test-Path $contentPath)) {
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/MDClasses`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`"/>`r`n"
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/xcf/extrnprops`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" version=`"2.17`"/>`r`n"
[System.IO.File]::WriteAllText($contentPath, $contentXml, $enc)
$modulesCreated += $contentPath
}
@@ -2716,7 +2797,7 @@ if ($objType -eq "ExchangePlan") {
if ($objType -eq "BusinessProcess") {
$flowchartPath = Join-Path $extDir "Flowchart.xml"
if (-not (Test-Path $flowchartPath)) {
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`"/>`r`n"
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"2.17`"/>`r`n"
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc)
$modulesCreated += $flowchartPath
}
@@ -2842,3 +2923,24 @@ switch ($regResult) {
"no-childobj" { Write-Warning "Configuration.xml found but <ChildObjects> not found" }
"no-config" { Write-Host " Configuration.xml: not found at $configXmlPath (register manually)" }
}
# Cross-reference hints
if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) {
Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:"
Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
}
if ($objType -eq "CalculationRegister" -and -not $def.chartOfCalculationTypes) {
Write-Host "[HINT] CalculationRegister requires ChartOfCalculationTypes reference:"
Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`""
}
if ($objType -eq "BusinessProcess" -and -not $def.task) {
Write-Host "[HINT] BusinessProcess requires Task reference:"
Write-Host " /meta-edit -Operation modify-property -Value `"Task=Task.XXX`""
}
if ($objType -eq "ChartOfAccounts") {
$maxExtDim = if ($null -ne $def.maxExtDimensionCount) { [int]$def.maxExtDimensionCount } else { 0 }
if ($maxExtDim -gt 0 -and -not $def.extDimensionTypes) {
Write-Host "[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:"
Write-Host " /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`""
}
}
@@ -1,12 +1,14 @@
#!/usr/bin/env python3
# meta-compile v1.0 — Compile 1C metadata object from JSON
# meta-compile v1.2 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import uuid
import xml.etree.ElementTree as ET
@@ -81,6 +83,27 @@ with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.loads(json_text)
# --- Batch mode: JSON array of objects ---
if isinstance(defn, list):
batch_ok = 0
batch_fail = 0
for idx, item in enumerate(defn, 1):
tmp_fd, tmp_path = tempfile.mkstemp(suffix='.json', prefix=f'meta-compile-batch-{idx}-')
try:
with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f:
json.dump(item, f, ensure_ascii=False, indent=2)
rc = subprocess.call([sys.executable, __file__, '-JsonPath', tmp_path, '-OutputDir', output_dir])
if rc == 0:
batch_ok += 1
else:
batch_fail += 1
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
print()
print(f"=== Batch: {len(defn)} objects, {batch_ok} compiled, {batch_fail} failed ===")
sys.exit(1 if batch_fail > 0 else 0)
# Normalize field synonyms: accept "objectType" as alias for "type"
if not defn.get('type') and defn.get('objectType'):
defn['type'] = defn['objectType']
@@ -175,6 +198,10 @@ type_synonyms = {
'задачассылка': 'TaskRef',
'определяемыйтип': 'DefinedType',
'definedtype': 'DefinedType',
# English lowercase ref synonyms
'catalogref': 'CatalogRef',
'documentref': 'DocumentRef',
'enumref': 'EnumRef',
}
def resolve_type_str(type_str):
@@ -221,13 +248,23 @@ def emit_type_content(indent, type_str):
# String or String(N)
m = re.match(r'^String(\((\d+)\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '0'
length = m.group(2) if m.group(2) else '10'
X(f'{indent}<v8:Type>xs:string</v8:Type>')
X(f'{indent}<v8:StringQualifiers>')
X(f'{indent}\t<v8:Length>{length}</v8:Length>')
X(f'{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>')
X(f'{indent}</v8:StringQualifiers>')
return
# Number without params -> Number(10,0)
if type_str == 'Number':
X(f'{indent}<v8:Type>xs:decimal</v8:Type>')
X(f'{indent}<v8:NumberQualifiers>')
X(f'{indent}\t<v8:Digits>10</v8:Digits>')
X(f'{indent}\t<v8:FractionDigits>0</v8:FractionDigits>')
X(f'{indent}\t<v8:AllowedSign>Any</v8:AllowedSign>')
X(f'{indent}</v8:NumberQualifiers>')
return
# Number(D,F) or Number(D,F,nonneg)
m = re.match(r'^Number\((\d+),(\d+)(,nonneg)?\)$', type_str)
if m:
@@ -260,6 +297,11 @@ def emit_type_content(indent, type_str):
dt_name = m.group(1)
X(f'{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>')
return
# ValueStorage
if type_str == 'ValueStorage':
X(f'{indent}<v8:Type>xs:base64Binary</v8:Type>')
return
# Reference types — use local xmlns declaration for 1C compatibility
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$', type_str)
if m:
@@ -296,6 +338,17 @@ def emit_fill_value(indent, type_str):
# 5. Attribute shorthand parser
# ---------------------------------------------------------------------------
def build_type_str(obj):
t = str(obj.get('valueType') or obj.get('type') or '')
if t and '(' not in t:
if t == 'String' and obj.get('length'):
t = f"String({obj['length']})"
elif t == 'Number' and obj.get('length'):
prec = obj.get('precision', 0)
nn = ',nonneg' if obj.get('nonneg') or obj.get('nonnegative') else ''
t = f"Number({obj['length']},{prec}{nn})"
return t
def parse_attribute_shorthand(val):
if isinstance(val, str):
parsed = {
@@ -322,7 +375,7 @@ def parse_attribute_shorthand(val):
name = str(val.get('name', ''))
return {
'name': name,
'type': str(val['type']) if val.get('type') else '',
'type': build_type_str(val),
'synonym': str(val['synonym']) if val.get('synonym') else split_camel_case(name),
'comment': str(val['comment']) if val.get('comment') else '',
'flags': list(val.get('flags', [])),
@@ -406,6 +459,7 @@ generated_types = {
{'prefix': 'CalculationRegisterList', 'category': 'List'},
{'prefix': 'CalculationRegisterRecordSet', 'category': 'RecordSet'},
{'prefix': 'CalculationRegisterRecordKey', 'category': 'RecordKey'},
{'prefix': 'RecalculationsManager', 'category': 'Recalcs'},
],
'ChartOfAccounts': [
{'prefix': 'ChartOfAccountsObject', 'category': 'Object'},
@@ -413,6 +467,8 @@ generated_types = {
{'prefix': 'ChartOfAccountsSelection', 'category': 'Selection'},
{'prefix': 'ChartOfAccountsList', 'category': 'List'},
{'prefix': 'ChartOfAccountsManager', 'category': 'Manager'},
{'prefix': 'ChartOfAccountsExtDimensionTypes', 'category': 'ExtDimensionTypes'},
{'prefix': 'ChartOfAccountsExtDimensionTypesRow', 'category': 'ExtDimensionTypesRow'},
],
'ChartOfCharacteristicTypes': [
{'prefix': 'ChartOfCharacteristicTypesObject', 'category': 'Object'},
@@ -457,6 +513,9 @@ generated_types = {
{'prefix': 'ExchangePlanList', 'category': 'List'},
{'prefix': 'ExchangePlanManager', 'category': 'Manager'},
],
'DefinedType': [
{'prefix': 'DefinedType', 'category': 'DefinedType'},
],
'DocumentJournal': [
{'prefix': 'DocumentJournalSelection', 'category': 'Selection'},
{'prefix': 'DocumentJournalList', 'category': 'List'},
@@ -554,7 +613,40 @@ def emit_tabular_standard_attributes(indent):
# 8. Attribute emitter
# ---------------------------------------------------------------------------
RESERVED_ATTR_NAMES = {
'Ref', 'DeletionMark', 'Code', 'Description', 'Date', 'Number', 'Posted',
'Parent', 'Owner', 'IsFolder', 'Predefined', 'PredefinedDataName',
'Recorder', 'Period', 'LineNumber', 'Active', 'Order', 'Type', 'OffBalance',
'Started', 'Completed', 'HeadTask', 'Executed', 'RoutePoint', 'BusinessProcess',
'ThisNode', 'SentNo', 'ReceivedNo', 'CalculationType', 'RegistrationPeriod',
'ReversingEntry', 'Account', 'ValueType', 'ActionPeriodIsBasic',
}
RESERVED_ATTR_NAMES_RU = {
'\u0421\u0441\u044b\u043b\u043a\u0430', '\u041f\u043e\u043c\u0435\u0442\u043a\u0430\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f',
'\u041a\u043e\u0434', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435',
'\u0414\u0430\u0442\u0430', '\u041d\u043e\u043c\u0435\u0440', '\u041f\u0440\u043e\u0432\u0435\u0434\u0435\u043d',
'\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446',
'\u042d\u0442\u043e\u0413\u0440\u0443\u043f\u043f\u0430', '\u041f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439',
'\u0418\u043c\u044f\u041f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0414\u0430\u043d\u043d\u044b\u0445',
'\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440', '\u041f\u0435\u0440\u0438\u043e\u0434',
'\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438', '\u0410\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c',
'\u041f\u043e\u0440\u044f\u0434\u043e\u043a', '\u0422\u0438\u043f', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439',
'\u0421\u0442\u0430\u0440\u0442\u043e\u0432\u0430\u043d', '\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d',
'\u0412\u0435\u0434\u0443\u0449\u0430\u044f\u0417\u0430\u0434\u0430\u0447\u0430',
'\u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430', '\u0422\u043e\u0447\u043a\u0430\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0430',
'\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441',
'\u042d\u0442\u043e\u0442\u0423\u0437\u0435\u043b', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e',
'\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e',
'\u0412\u0438\u0434\u0420\u0430\u0441\u0447\u0435\u0442\u0430', '\u041f\u0435\u0440\u0438\u043e\u0434\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438',
'\u0421\u0442\u043e\u0440\u043d\u043e\u0417\u0430\u043f\u0438\u0441\u044c',
'\u0421\u0447\u0435\u0442', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f',
'\u041f\u0435\u0440\u0438\u043e\u0434\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f\u0411\u0430\u0437\u043e\u0432\u044b\u0439',
}
def emit_attribute(indent, parsed, context):
attr_name = parsed['name']
if attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU:
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
uid = new_uuid()
X(f'{indent}<Attribute uuid="{uid}">')
X(f'{indent}\t<Properties>')
@@ -929,7 +1021,7 @@ def emit_document_properties(indent):
if reg_records:
X(f'{i}<RegisterRecords>')
for rr in reg_records:
X(f'{i}\t<xr:Record>{rr}</xr:Record>')
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{rr}</xr:Item>')
X(f'{i}</RegisterRecords>')
else:
X(f'{i}<RegisterRecords/>')
@@ -975,7 +1067,8 @@ def emit_constant_properties(indent):
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
value_type = str(defn['valueType']) if defn.get('valueType') else 'String'
# Type
value_type = build_type_str(defn) or 'String'
emit_value_type(i, value_type)
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultForm/>')
@@ -1072,7 +1165,11 @@ def emit_defined_type_properties(indent):
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
# Accept both valueType and valueTypes
value_types = list(defn.get('valueTypes', []))
if not value_types and defn.get('valueType'):
vt_raw = defn['valueType']
value_types = list(vt_raw) if isinstance(vt_raw, list) else [vt_raw]
if value_types:
X(f'{i}<Type>')
for vt in value_types:
@@ -1143,6 +1240,9 @@ def emit_scheduled_job_properties(indent):
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
method_name = str(defn['methodName']) if defn.get('methodName') else ''
# Ensure CommonModule. prefix
if method_name and not method_name.startswith('CommonModule.'):
method_name = f'CommonModule.{method_name}'
X(f'{i}<MethodName>{esc_xml(method_name)}</MethodName>')
description = str(defn['description']) if defn.get('description') else synonym
X(f'{i}<Description>{esc_xml(description)}</Description>')
@@ -1174,6 +1274,9 @@ def emit_event_subscription_properties(indent):
event = str(defn['event']) if defn.get('event') else 'BeforeWrite'
X(f'{i}<Event>{event}</Event>')
handler = str(defn['handler']) if defn.get('handler') else ''
# Ensure CommonModule. prefix
if handler and not handler.startswith('CommonModule.'):
handler = f'CommonModule.{handler}'
X(f'{i}<Handler>{esc_xml(handler)}</Handler>')
# --- 13b. Report, DataProcessor ---
@@ -1250,18 +1353,12 @@ def emit_exchange_plan_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '100'
code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<DefaultPresentation>AsDescription</DefaultPresentation>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
emit_standard_attributes(i, 'ExchangePlan')
distributed = 'true' if defn.get('distributedInfoBase') is True else 'false'
include_ext = 'true' if defn.get('includeConfigurationExtensions') is True else 'false'
@@ -1308,12 +1405,10 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
@@ -1335,7 +1430,7 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}\t<v8:Type>xs:boolean</v8:Type>')
X(f'{i}\t<v8:Type>xs:string</v8:Type>')
X(f'{i}\t<v8:StringQualifiers>')
X(f'{i}\t\t<v8:Length>0</v8:Length>')
X(f'{i}\t\t<v8:Length>100</v8:Length>')
X(f'{i}\t\t<v8:AllowedLength>Variable</v8:AllowedLength>')
X(f'{i}\t</v8:StringQualifiers>')
X(f'{i}\t<v8:Type>xs:decimal</v8:Type>')
@@ -1457,12 +1552,9 @@ def emit_chart_of_accounts_properties(indent):
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeSeries>{code_series}</CodeSeries>')
X(f'{i}<CheckUnique>false</CheckUnique>')
X(f'{i}<Autonumbering>true</Autonumbering>')
X(f'{i}<DefaultPresentation>AsDescription</DefaultPresentation>')
X(f'{i}<AutoOrderByCode>{auto_order}</AutoOrderByCode>')
X(f'{i}<OrderLength>{order_length}</OrderLength>')
hierarchical = 'true' if defn.get('hierarchical') is True else 'false'
X(f'{i}<Hierarchical>{hierarchical}</Hierarchical>')
X(f'{i}<EditType>InDialog</EditType>')
emit_standard_attributes(i, 'ChartOfAccounts')
X(f'{i}<StandardTabularSections>')
@@ -1545,16 +1637,12 @@ def emit_chart_of_calculation_types_properties(indent):
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
code_allowed_length = str(defn['codeAllowedLength']) if defn.get('codeAllowedLength') else 'Variable'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<DefaultPresentation>AsDescription</DefaultPresentation>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
dependence = str(defn['dependenceOnCalculationTypes']) if defn.get('dependenceOnCalculationTypes') else 'NotUsed'
dependence = str(defn['dependenceOnCalculationTypes']) if defn.get('dependenceOnCalculationTypes') else 'DontUse'
X(f'{i}<DependenceOnCalculationTypes>{dependence}</DependenceOnCalculationTypes>')
base_types = list(defn.get('baseCalculationTypes', []))
if base_types:
@@ -1664,6 +1752,11 @@ def emit_business_process_properties(indent):
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
emit_standard_attributes(i, 'BusinessProcess')
X(f'{i}<Characteristics/>')
task_ref = str(defn['task']) if defn.get('task') else ''
if task_ref:
X(f'{i}<Task>{task_ref}</Task>')
else:
X(f'{i}<Task/>')
X(f'{i}<BasedOn/>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>BusinessProcess.{obj_name}.StandardAttribute.Number</xr:Field>')
@@ -1994,14 +2087,11 @@ def emit_addressing_attribute(indent, addr_def):
type_str = ''
addressing_dimension = ''
indexing = 'Index'
if isinstance(addr_def, str):
name = addr_def
attr_synonym = split_camel_case(name)
else:
name = str(addr_def.get('name', ''))
attr_synonym = str(addr_def['synonym']) if addr_def.get('synonym') else split_camel_case(name)
if addr_def.get('type'):
type_str = str(addr_def['type'])
parsed = parse_attribute_shorthand(addr_def)
name = parsed['name']
attr_synonym = parsed['synonym']
type_str = parsed['type']
if not isinstance(addr_def, str):
if addr_def.get('addressingDimension'):
addressing_dimension = str(addr_def['addressingDimension'])
if addr_def.get('indexing'):
@@ -2335,14 +2425,14 @@ if obj_type in types_with_module:
if obj_type == 'ExchangePlan':
content_path = os.path.join(ext_dir, 'Content.xml')
if not os.path.isfile(content_path):
content_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>\r\n'
content_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="2.17"/>\r\n'
write_utf8_bom(content_path, content_xml)
modules_created.append(content_path)
if obj_type == 'BusinessProcess':
flowchart_path = os.path.join(ext_dir, 'Flowchart.xml')
if not os.path.isfile(flowchart_path):
flowchart_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses"/>\r\n'
flowchart_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="2.17"/>\r\n'
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
@@ -2464,3 +2554,19 @@ elif reg_result == 'no-childobj':
print('WARNING: Configuration.xml found but <ChildObjects> not found', file=sys.stderr)
elif reg_result == 'no-config':
print(f' Configuration.xml: not found at {config_xml_path} (register manually)')
# Cross-reference hints
if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'):
print('[HINT] AccountingRegister requires ChartOfAccounts reference:')
print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
if obj_type == 'CalculationRegister' and not defn.get('chartOfCalculationTypes'):
print('[HINT] CalculationRegister requires ChartOfCalculationTypes reference:')
print(' /meta-edit -Operation modify-property -Value "ChartOfCalculationTypes=ChartOfCalculationTypes.XXX"')
if obj_type == 'BusinessProcess' and not defn.get('task'):
print('[HINT] BusinessProcess requires Task reference:')
print(' /meta-edit -Operation modify-property -Value "Task=Task.XXX"')
if obj_type == 'ChartOfAccounts':
max_ext_dim = int(defn['maxExtDimensionCount']) if defn.get('maxExtDimensionCount') is not None else 0
if max_ext_dim > 0 and not defn.get('extDimensionTypes'):
print('[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:')
print(' /meta-edit -Operation modify-property -Value "ExtDimensionTypes=ChartOfCharacteristicTypes.XXX"')
+26 -2
View File
@@ -1,4 +1,4 @@
# meta-edit v1.3 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.4 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -297,7 +297,7 @@ function Build-TypeContentXml {
# String or String(N)
if ($typeStr -match '^String(\((\d+)\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
$len = if ($Matches[2]) { $Matches[2] } else { "10" }
$sb.AppendLine("$indent<v8:Type>xs:string</v8:Type>") | Out-Null
$sb.AppendLine("$indent<v8:StringQualifiers>") | Out-Null
$sb.AppendLine("$indent`t<v8:Length>$len</v8:Length>") | Out-Null
@@ -689,10 +689,34 @@ function Get-AttributeContext {
}
}
$script:reservedAttrNames = @{
"Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование"
"Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец"
"IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных"
"Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность"
"Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый"
"Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача"
"Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс"
"ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого"
"CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись"
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
}
function Build-AttributeFragment {
param($parsed, [string]$context, [string]$indent)
if (-not $context) { $context = Get-AttributeContext }
# Check reserved attribute names
$attrName = $parsed.name
if ($script:reservedAttrNames.ContainsKey($attrName)) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
}
$ruValues = $script:reservedAttrNames.Values
if ($ruValues -contains $attrName) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name (Russian). This may cause errors when loading into 1C."
}
$uuid = New-Guid-String
$sb = New-Object System.Text.StringBuilder
+30 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.0 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.4 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -214,7 +214,7 @@ def build_type_content_xml(indent, type_str):
# String or String(N)
m = re.match(r"^String(\((\d+)\))?$", type_str)
if m:
length = m.group(2) if m.group(2) else "0"
length = m.group(2) if m.group(2) else "10"
lines.append(f"{indent}<v8:Type>xs:string</v8:Type>")
lines.append(f"{indent}<v8:StringQualifiers>")
lines.append(f"{indent}\t<v8:Length>{length}</v8:Length>")
@@ -600,10 +600,38 @@ def get_attribute_context():
return "object"
RESERVED_ATTR_NAMES = {
'Ref', 'DeletionMark', 'Code', 'Description', 'Date', 'Number', 'Posted',
'Parent', 'Owner', 'IsFolder', 'Predefined', 'PredefinedDataName',
'Recorder', 'Period', 'LineNumber', 'Active', 'Order', 'Type', 'OffBalance',
'Started', 'Completed', 'HeadTask', 'Executed', 'RoutePoint', 'BusinessProcess',
'ThisNode', 'SentNo', 'ReceivedNo', 'CalculationType', 'RegistrationPeriod',
'ReversingEntry', 'Account', 'ValueType', 'ActionPeriodIsBasic',
}
RESERVED_ATTR_NAMES_RU = {
'Ссылка', 'ПометкаУдаления', 'Код', 'Наименование',
'Дата', 'Номер', 'Проведен', 'Родитель', 'Владелец',
'ЭтоГруппа', 'Предопределенный', 'ИмяПредопределенныхДанных',
'Регистратор', 'Период', 'НомерСтроки', 'Активность',
'Порядок', 'Тип', 'Забалансовый',
'Стартован', 'Завершен', 'ВедущаяЗадача',
'Выполнена', 'ТочкаМаршрута', 'БизнесПроцесс',
'ЭтотУзел', 'НомерОтправленного', 'НомерПринятого',
'ВидРасчета', 'ПериодРегистрации', 'СторноЗапись',
'Счет', 'ТипЗначения', 'ПериодДействияБазовый',
}
def build_attribute_fragment(parsed, context, indent):
"""Build XML fragment string for an Attribute element."""
if not context:
context = get_attribute_context()
# Check reserved attribute names
attr_name = parsed['name']
if attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU:
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
uid = new_uuid()
lines = []
+9 -3
View File
@@ -1,7 +1,7 @@
---
name: meta-validate
description: Валидация объекта метаданных 1С. Используй после создания или модификации объекта конфигурации для проверки корректности
argument-hint: <ObjectPath> [-MaxErrors 30]
argument-hint: <ObjectPath> [-MaxErrors 30] — pipe-separated paths for batch
allowed-tools:
- Bash
- Read
@@ -16,18 +16,21 @@ allowed-tools:
```
/meta-validate <ObjectPath>
/meta-validate path1.xml|path2.xml|path3.xml — batch mode
```
## Параметры
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|-------------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу или каталогу объекта |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| ObjectPath | да | — | Путь к XML-файлу или каталогу объекта. Несколько путей через `\|` для batch |
| MaxErrors | нет | 30 | Остановиться после N ошибок (per object) |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
`ObjectPath` авторезолв: если указана директория — ищет `<dirName>/<dirName>.xml`.
**Batch mode**: при нескольких путях через `|` каждый объект валидируется отдельно, в конце выводится сводка `=== Batch: N objects, X passed, Y failed ===`.
## Команда
```powershell
@@ -94,6 +97,9 @@ Errors: 1, Warnings: 1
# С записью в файл
... -ObjectPath Catalogs/Номенклатура.xml -OutFile result.txt
# Batch: несколько объектов через |
... -ObjectPath "Catalogs/Банки.xml|Documents/Заказ.xml|Enums/ВидДоговора.xml"
```
## Верификация
@@ -1,4 +1,4 @@
# meta-validate v1.0 — Validate 1C metadata object structure
# meta-validate v1.1 — Validate 1C metadata object structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,31 @@ param(
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Batch mode: pipe-separated paths (comma reserved by PowerShell) ---
$pathList = @($ObjectPath -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($pathList.Count -gt 1) {
$batchOk = 0
$batchFail = 0
foreach ($singlePath in $pathList) {
$callArgs = @{ ObjectPath = $singlePath; MaxErrors = $MaxErrors }
if ($OutFile) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutFile)
$ext = [System.IO.Path]::GetExtension($OutFile)
$dir = Split-Path $OutFile
if (-not $dir) { $dir = "." }
$objLeaf = [System.IO.Path]::GetFileNameWithoutExtension($singlePath)
$callArgs.OutFile = Join-Path $dir "$baseName`_$objLeaf$ext"
}
& $PSCommandPath @callArgs
if ($LASTEXITCODE -eq 0) { $batchOk++ } else { $batchFail++ }
}
Write-Host ""
Write-Host "=== Batch: $($pathList.Count) objects, $batchOk passed, $batchFail failed ==="
if ($batchFail -gt 0) { exit 1 }
exit 0
}
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) {
@@ -54,6 +79,19 @@ if (-not (Test-Path $ObjectPath)) {
$resolvedPath = (Resolve-Path $ObjectPath).Path
# --- Detect config directory (for cross-object checks) ---
$script:configDir = $null
$probe = Split-Path $resolvedPath
for ($depth = 0; $depth -lt 4; $depth++) {
if (-not $probe) { break }
if (Test-Path (Join-Path $probe "Configuration.xml")) {
$script:configDir = $probe
break
}
$probe = Split-Path $probe
}
# --- Output infrastructure ---
$script:errors = 0
@@ -216,6 +254,15 @@ $validPropertyValues = @{
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"DataHistory" = @("Use","DontUse")
"DependenceOnCalculationTypes" = @("DontUse","RequireCalculationTypes")
}
# Properties forbidden per type (would cause LoadConfigFromFiles error)
$forbiddenProperties = @{
"ChartOfCharacteristicTypes" = @("CodeType")
"ChartOfAccounts" = @("Autonumbering","Hierarchical")
"ChartOfCalculationTypes" = @("CheckUnique","Autonumbering")
"ExchangePlan" = @("CodeType","CheckUnique","Autonumbering")
}
# --- 1. Parse XML ---
@@ -649,6 +696,41 @@ if ($childObjNode) {
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 7b: Reserved attribute names ---
$reservedAttrNames = @(
"Ref","DeletionMark","Code","Description","Date","Number","Posted","Parent","Owner",
"IsFolder","Predefined","PredefinedDataName","Recorder","Period","LineNumber","Active",
"Order","Type","OffBalance","Started","Completed","HeadTask","Executed","RoutePoint",
"BusinessProcess","ThisNode","SentNo","ReceivedNo","CalculationType","RegistrationPeriod",
"ReversingEntry","Account","ValueType","ActionPeriodIsBasic"
)
if ($childObjNode) {
$check7bOk = $true
$attrNodes = $childObjNode.SelectNodes("md:Attribute", $ns)
foreach ($attrNode in $attrNodes) {
$attrProps = $attrNode.SelectSingleNode("md:Properties", $ns)
if ($attrProps) {
$attrNameNode = $attrProps.SelectSingleNode("md:Name", $ns)
if ($attrNameNode -and $attrNameNode.InnerText) {
$an = $attrNameNode.InnerText
if ($reservedAttrNames -contains $an) {
Report-Warn "7b. Attribute '$an' conflicts with a standard attribute name"
$check7bOk = $false
}
}
}
}
if ($check7bOk) {
Report-OK "7b. Reserved attribute names: no conflicts"
}
} else {
Report-OK "7b. Reserved attribute names: N/A"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 8: Name uniqueness ---
function Check-Uniqueness {
@@ -887,6 +969,97 @@ if ($propsNode) {
$check10Issues++
}
}
# AccountingRegister: ChartOfAccounts must not be empty
if ($mdType -eq "AccountingRegister") {
$coa = $propsNode.SelectSingleNode("md:ChartOfAccounts", $ns)
if (-not $coa -or -not $coa.InnerText.Trim()) {
Report-Error "10. AccountingRegister: empty ChartOfAccounts"
$check10Ok = $false
$check10Issues++
Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
}
}
# CalculationRegister: ChartOfCalculationTypes must not be empty
if ($mdType -eq "CalculationRegister") {
$coct = $propsNode.SelectSingleNode("md:ChartOfCalculationTypes", $ns)
if (-not $coct -or -not $coct.InnerText.Trim()) {
Report-Error "10. CalculationRegister: empty ChartOfCalculationTypes"
$check10Ok = $false
$check10Issues++
Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ChartOfCalculationTypes=ChartOfCalculationTypes.XXX`""
}
}
# BusinessProcess: Task should not be empty
if ($mdType -eq "BusinessProcess") {
$taskProp = $propsNode.SelectSingleNode("md:Task", $ns)
if (-not $taskProp -or -not $taskProp.InnerText.Trim()) {
Report-Warn "10. BusinessProcess: empty Task reference"
$check10Issues++
Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"Task=Task.XXX`""
}
}
# DocumentJournal: RegisteredDocuments should not be empty
if ($mdType -eq "DocumentJournal") {
$regDocs = $propsNode.SelectSingleNode("md:RegisteredDocuments", $ns)
$hasRegDocs = $false
if ($regDocs) {
$items = $regDocs.SelectNodes("v8:Type", $ns)
if ($items.Count -gt 0) { $hasRegDocs = $true }
}
if (-not $hasRegDocs) {
Report-Warn "10. DocumentJournal: no RegisteredDocuments specified"
$check10Issues++
}
}
# ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0
if ($mdType -eq "ChartOfAccounts") {
$maxExtDim = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns)
if ($maxExtDim -and [int]$maxExtDim.InnerText -gt 0) {
$edt = $propsNode.SelectSingleNode("md:ExtDimensionTypes", $ns)
if (-not $edt -or -not $edt.InnerText.Trim()) {
Report-Warn "10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty"
$check10Issues++
Write-Host "[HINT] /meta-edit -Operation modify-property -Value `"ExtDimensionTypes=ChartOfCharacteristicTypes.XXX`""
}
}
}
# Register: must have at least one registrar document
$registerTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister")
if ($registerTypes -contains $mdType -and $script:configDir -and $objName -ne "(unknown)") {
$needsRegistrar = $true
# InformationRegister with WriteMode=Independent does not need a registrar
if ($mdType -eq "InformationRegister") {
$writeMode = $propsNode.SelectSingleNode("md:WriteMode", $ns)
if (-not $writeMode -or $writeMode.InnerText -ne "RecorderSubordinate") {
$needsRegistrar = $false
}
}
if ($needsRegistrar) {
$regRef = "$mdType.$objName"
$docsDir = Join-Path $script:configDir "Documents"
$hasRegistrar = $false
if (Test-Path $docsDir) {
$docXmls = Get-ChildItem $docsDir -Filter "*.xml" -File -ErrorAction SilentlyContinue
foreach ($docXml in $docXmls) {
$content = [System.IO.File]::ReadAllText($docXml.FullName, [System.Text.Encoding]::UTF8)
if ($content.Contains($regRef)) {
$hasRegistrar = $true
break
}
}
}
if (-not $hasRegistrar) {
Report-Warn "10. $mdType`: no registrar document found (none references '$regRef' in RegisterRecords)"
$check10Issues++
}
}
}
}
if ($check10Ok -and $check10Issues -eq 0) {
@@ -991,6 +1164,129 @@ if ($mdType -eq "HTTPService" -and $childObjNode) {
Report-OK "11. HTTPService/WebService: N/A"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 12: Forbidden properties per type ---
if ($propsNode -and $forbiddenProperties.ContainsKey($mdType)) {
$forbidden = $forbiddenProperties[$mdType]
$check12Ok = $true
foreach ($fp in $forbidden) {
$fpNode = $propsNode.SelectSingleNode("md:$fp", $ns)
if ($fpNode) {
Report-Error "12. Forbidden property '$fp' present in $mdType (will fail on LoadConfigFromFiles)"
$check12Ok = $false
}
}
if ($check12Ok) {
Report-OK "12. Forbidden properties: none found"
}
} else {
Report-OK "12. Forbidden properties: N/A"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 13: Method reference validation (EventSubscription.Handler, ScheduledJob.MethodName) ---
if ($propsNode -and $mdType -in @("EventSubscription","ScheduledJob") -and $script:configDir) {
$check13Ok = $true
$methodRef = $null
$propLabel = $null
if ($mdType -eq "EventSubscription") {
$hNode = $propsNode.SelectSingleNode("md:Handler", $ns)
if ($hNode) { $methodRef = $hNode.InnerText.Trim() }
$propLabel = "Handler"
} elseif ($mdType -eq "ScheduledJob") {
$mNode = $propsNode.SelectSingleNode("md:MethodName", $ns)
if ($mNode) { $methodRef = $mNode.InnerText.Trim() }
$propLabel = "MethodName"
}
if ($methodRef) {
$parts = $methodRef.Split('.')
# Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy)
if ($parts.Count -eq 3 -and $parts[0] -eq "CommonModule") {
$cmName = $parts[1]
$procName = $parts[2]
} elseif ($parts.Count -eq 2) {
$cmName = $parts[0]
$procName = $parts[1]
} else {
Report-Error "13. ${mdType}.${propLabel} = '$methodRef': expected format 'CommonModule.ModuleName.ProcedureName'"
$check13Ok = $false
$cmName = $null
$procName = $null
}
if ($cmName) {
$cmXml = Join-Path (Join-Path $script:configDir "CommonModules") "$cmName.xml"
if (-not (Test-Path $cmXml)) {
Report-Error "13. ${mdType}.${propLabel}: CommonModule '$cmName' not found (expected $cmXml)"
$check13Ok = $false
} else {
# Check BSL file for exported procedure
$bslPath = Join-Path (Join-Path (Join-Path $script:configDir "CommonModules") $cmName) "Ext/Module.bsl"
if (Test-Path $bslPath) {
$bslContent = [System.IO.File]::ReadAllText($bslPath, [System.Text.Encoding]::UTF8)
# Match: Procedure/Function ProcName(...) Export or Процедура/Функция ProcName(...) Экспорт
$exportPattern = "(?mi)^[\s]*(Procedure|Function|Процедура|Функция)\s+$([regex]::Escape($procName))\s*\(.*\)\s+(Export|Экспорт)"
if (-not [regex]::IsMatch($bslContent, $exportPattern)) {
Report-Warn "13. ${mdType}.${propLabel}: procedure '$procName' not found as exported in CommonModule '$cmName'"
$check13Ok = $false
}
} else {
Report-Warn "13. ${mdType}.${propLabel}: BSL file not found ($bslPath), cannot verify procedure"
}
}
}
}
if ($check13Ok) {
Report-OK "13. Method reference: $propLabel = '$methodRef'"
}
} else {
Report-OK "13. Method reference: N/A"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 14: DocumentJournal Column content ---
if ($mdType -eq "DocumentJournal" -and $childObjNode) {
$columns = $childObjNode.SelectNodes("md:Column", $ns)
$check14Ok = $true
$colCount = 0
$emptyRefCount = 0
foreach ($col in $columns) {
$colCount++
$colProps = $col.SelectSingleNode("md:Properties", $ns)
$colNameNode = if ($colProps) { $colProps.SelectSingleNode("md:Name", $ns) } else { $null }
$colName = if ($colNameNode) { $colNameNode.InnerText } else { "(unnamed)" }
$refs = if ($colProps) { $colProps.SelectSingleNode("md:References", $ns) } else { $null }
$hasItems = $false
if ($refs) {
$items = $refs.SelectNodes("xr:Item", $ns)
if ($items.Count -gt 0) { $hasItems = $true }
}
if (-not $hasItems) {
Report-Error "14. DocumentJournal Column '$colName': empty References (will fail on LoadConfigFromFiles)"
$check14Ok = $false
$emptyRefCount++
}
}
if ($check14Ok -and $colCount -gt 0) {
Report-OK "14. DocumentJournal Columns: $colCount column(s), all have References"
} elseif ($colCount -eq 0) {
Report-OK "14. DocumentJournal Columns: none"
}
} else {
Report-OK "14. DocumentJournal Columns: N/A"
}
# --- Final output ---
& $finalize
@@ -1,8 +1,9 @@
# meta-validate v1.0 — Validate 1C metadata object structure (Python port)
# meta-validate v1.1 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import subprocess
import sys
from lxml import etree
@@ -18,10 +19,32 @@ parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
object_path = args.ObjectPath
max_errors = args.MaxErrors
out_file = args.OutFile
# ── batch mode: pipe-separated paths ─────────────────────────
path_list = [p.strip() for p in args.ObjectPath.split('|') if p.strip()]
if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
if out_file:
base, ext = os.path.splitext(out_file)
obj_leaf = os.path.splitext(os.path.basename(single_path))[0]
cmd += ["-OutFile", f"{base}_{obj_leaf}{ext}"]
rc = subprocess.call(cmd)
if rc == 0:
batch_ok += 1
else:
batch_fail += 1
print()
print(f"=== Batch: {len(path_list)} objects, {batch_ok} passed, {batch_fail} failed ===")
sys.exit(1 if batch_fail > 0 else 0)
object_path = path_list[0]
# ── resolve path ─────────────────────────────────────────────
if not os.path.isabs(object_path):
@@ -59,6 +82,18 @@ if not os.path.exists(object_path):
resolved_path = os.path.abspath(object_path)
# ── detect config directory (for cross-object checks) ────────
config_dir = None
probe = os.path.dirname(resolved_path)
for _ in range(4):
if not probe:
break
if os.path.exists(os.path.join(probe, "Configuration.xml")):
config_dir = probe
break
probe = os.path.dirname(probe)
# ── output infrastructure ────────────────────────────────────
errors = 0
@@ -216,6 +251,15 @@ valid_property_values = {
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
"DataHistory": ["Use", "DontUse"],
"DependenceOnCalculationTypes": ["DontUse", "RequireCalculationTypes"],
}
# Properties forbidden per type (would cause LoadConfigFromFiles error)
forbidden_properties = {
"ChartOfCharacteristicTypes": ["CodeType"],
"ChartOfAccounts": ["Autonumbering", "Hierarchical"],
"ChartOfCalculationTypes": ["CheckUnique", "Autonumbering"],
"ExchangePlan": ["CodeType", "CheckUnique", "Autonumbering"],
}
# ── Namespaces ───────────────────────────────────────────────
@@ -626,6 +670,37 @@ if stopped:
finalize()
sys.exit(1)
# ── Check 7b: Reserved attribute names ───────────────────────
RESERVED_ATTR_NAMES = {
'Ref', 'DeletionMark', 'Code', 'Description', 'Date', 'Number', 'Posted',
'Parent', 'Owner', 'IsFolder', 'Predefined', 'PredefinedDataName',
'Recorder', 'Period', 'LineNumber', 'Active', 'Order', 'Type', 'OffBalance',
'Started', 'Completed', 'HeadTask', 'Executed', 'RoutePoint', 'BusinessProcess',
'ThisNode', 'SentNo', 'ReceivedNo', 'CalculationType', 'RegistrationPeriod',
'ReversingEntry', 'Account', 'ValueType', 'ActionPeriodIsBasic',
}
if child_obj_node is not None:
check7b_ok = True
for attr_node in find_all(child_obj_node, 'md:Attribute'):
attr_props = find(attr_node, 'md:Properties')
if attr_props is not None:
attr_name_node = find(attr_props, 'md:Name')
if attr_name_node is not None and inner_text(attr_name_node):
an = inner_text(attr_name_node)
if an in RESERVED_ATTR_NAMES:
report_warn(f"7b. Attribute '{an}' conflicts with a standard attribute name")
check7b_ok = False
if check7b_ok:
report_ok("7b. Reserved attribute names: no conflicts")
else:
report_ok("7b. Reserved attribute names: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 8: Name uniqueness ─────────────────────────────────
@@ -841,6 +916,88 @@ if props_node is not None:
check10_ok = False
check10_issues += 1
# AccountingRegister: ChartOfAccounts must not be empty
if md_type == 'AccountingRegister':
coa = find(props_node, 'md:ChartOfAccounts')
if coa is None or not text_of(coa):
report_error('10. AccountingRegister: empty ChartOfAccounts')
check10_ok = False
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
# CalculationRegister: ChartOfCalculationTypes must not be empty
if md_type == 'CalculationRegister':
coct = find(props_node, 'md:ChartOfCalculationTypes')
if coct is None or not text_of(coct):
report_error('10. CalculationRegister: empty ChartOfCalculationTypes')
check10_ok = False
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ChartOfCalculationTypes=ChartOfCalculationTypes.XXX"')
# BusinessProcess: Task should not be empty
if md_type == 'BusinessProcess':
task_prop = find(props_node, 'md:Task')
if task_prop is None or not text_of(task_prop):
report_warn('10. BusinessProcess: empty Task reference')
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "Task=Task.XXX"')
# DocumentJournal: RegisteredDocuments should not be empty
if md_type == 'DocumentJournal':
reg_docs = find(props_node, 'md:RegisteredDocuments')
has_reg_docs = False
if reg_docs is not None:
items = find_all(reg_docs, 'v8:Type')
if len(items) > 0:
has_reg_docs = True
if not has_reg_docs:
report_warn('10. DocumentJournal: no RegisteredDocuments specified')
check10_issues += 1
# ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0
if md_type == 'ChartOfAccounts':
max_ext_dim = find(props_node, 'md:MaxExtDimensionCount')
if max_ext_dim is not None:
try:
med_val = int(inner_text(max_ext_dim) or '0')
except ValueError:
med_val = 0
if med_val > 0:
edt = find(props_node, 'md:ExtDimensionTypes')
if edt is None or not text_of(edt):
report_warn('10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty')
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ExtDimensionTypes=ChartOfCharacteristicTypes.XXX"')
# Register: must have at least one registrar document
register_types = ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'InformationRegister')
if md_type in register_types and config_dir and obj_name != '(unknown)':
needs_registrar = True
# InformationRegister with WriteMode=Independent does not need a registrar
if md_type == 'InformationRegister':
write_mode = find(props_node, 'md:WriteMode')
if write_mode is None or inner_text(write_mode) != 'RecorderSubordinate':
needs_registrar = False
if needs_registrar:
reg_ref = f'{md_type}.{obj_name}'
docs_dir = os.path.join(config_dir, 'Documents')
has_registrar = False
if os.path.isdir(docs_dir):
for fname in os.listdir(docs_dir):
if not fname.endswith('.xml'):
continue
fpath = os.path.join(docs_dir, fname)
if not os.path.isfile(fpath):
continue
with open(fpath, 'r', encoding='utf-8-sig') as f:
content = f.read()
if reg_ref in content:
has_registrar = True
break
if not has_registrar:
report_warn(f"10. {md_type}: no registrar document found (none references '{reg_ref}' in RegisterRecords)")
check10_issues += 1
if check10_ok and check10_issues == 0:
report_ok("10. Cross-property consistency")
@@ -929,6 +1086,120 @@ elif md_type == "WebService" and child_obj_node is not None:
else:
report_ok("11. HTTPService/WebService: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 12: Forbidden properties per type ──────────────────
if props_node is not None and md_type in forbidden_properties:
forbidden = forbidden_properties[md_type]
check12_ok = True
for fp in forbidden:
fp_node = find(props_node, f"md:{fp}")
if fp_node is not None:
report_error(f"12. Forbidden property '{fp}' present in {md_type} (will fail on LoadConfigFromFiles)")
check12_ok = False
if check12_ok:
report_ok("12. Forbidden properties: none found")
else:
report_ok("12. Forbidden properties: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 13: Method reference validation ─────────────────────
if props_node is not None and md_type in ("EventSubscription", "ScheduledJob") and config_dir:
check13_ok = True
method_ref = None
prop_label = None
if md_type == "EventSubscription":
h_node = find(props_node, "md:Handler")
if h_node is not None:
method_ref = text_of(h_node)
prop_label = "Handler"
elif md_type == "ScheduledJob":
m_node = find(props_node, "md:MethodName")
if m_node is not None:
method_ref = text_of(m_node)
prop_label = "MethodName"
if method_ref:
parts = method_ref.split(".")
# Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy)
if len(parts) == 3 and parts[0] == "CommonModule":
cm_name = parts[1]
proc_name = parts[2]
elif len(parts) == 2:
cm_name = parts[0]
proc_name = parts[1]
else:
report_error(f"13. {md_type}.{prop_label} = '{method_ref}': expected format 'CommonModule.ModuleName.ProcedureName'")
check13_ok = False
cm_name = None
proc_name = None
if cm_name:
cm_xml = os.path.join(config_dir, "CommonModules", f"{cm_name}.xml")
if not os.path.exists(cm_xml):
report_error(f"13. {md_type}.{prop_label}: CommonModule '{cm_name}' not found (expected {cm_xml})")
check13_ok = False
else:
# Check BSL file for exported procedure
bsl_path = os.path.join(config_dir, "CommonModules", cm_name, "Ext", "Module.bsl")
if os.path.exists(bsl_path):
with open(bsl_path, "r", encoding="utf-8-sig") as f:
bsl_content = f.read()
export_pattern = rf"(?mi)^\s*(Procedure|Function|Процедура|Функция)\s+{re.escape(proc_name)}\s*\(.*\)\s+(Export|Экспорт)"
if not re.search(export_pattern, bsl_content):
report_warn(f"13. {md_type}.{prop_label}: procedure '{proc_name}' not found as exported in CommonModule '{cm_name}'")
check13_ok = False
else:
report_warn(f"13. {md_type}.{prop_label}: BSL file not found ({bsl_path}), cannot verify procedure")
if check13_ok:
report_ok(f"13. Method reference: {prop_label} = '{method_ref}'")
else:
report_ok("13. Method reference: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 14: DocumentJournal Column content ──────────────────
if md_type == "DocumentJournal" and child_obj_node is not None:
columns = find_all(child_obj_node, "md:Column")
check14_ok = True
col_count = 0
empty_ref_count = 0
for col in columns:
col_count += 1
col_props = find(col, "md:Properties")
col_name_node = find(col_props, "md:Name") if col_props is not None else None
col_name = inner_text(col_name_node) if col_name_node is not None else "(unnamed)"
refs = find(col_props, "md:References") if col_props is not None else None
has_items = False
if refs is not None:
items = find_all(refs, "xr:Item")
if len(items) > 0:
has_items = True
if not has_items:
report_error(f"14. DocumentJournal Column '{col_name}': empty References (will fail on LoadConfigFromFiles)")
check14_ok = False
empty_ref_count += 1
if check14_ok and col_count > 0:
report_ok(f"14. DocumentJournal Columns: {col_count} column(s), all have References")
elif col_count == 0:
report_ok("14. DocumentJournal Columns: none")
else:
report_ok("14. DocumentJournal Columns: N/A")
# ── Final output ──────────────────────────────────────────────
finalize()
+31 -11
View File
@@ -1,6 +1,6 @@
# Meta DSL — спецификация JSON-формата для объектов метаданных 1С
Версия: 2.0
Версия: 2.1
## Обзор
@@ -112,7 +112,7 @@ JSON DSL для описания объектов метаданных конф
### 4.1 Строковая форма
```
"ИмяРеквизита" → String (без квалификаторов)
"ИмяРеквизита" → String(10) по умолчанию
"ИмяРеквизита: Тип" → с типом
"ИмяРеквизита: Тип | req, index" → с флагами
```
@@ -130,6 +130,17 @@ JSON DSL для описания объектов метаданных конф
}
```
Тип можно задать единой строкой (`"type": "String(100)"`) или раздельными полями:
```json
{ "name": "Имя", "type": "String", "length": 100 }
{ "name": "Сумма", "type": "Number", "length": 15, "precision": 2 }
{ "name": "Остаток", "type": "Number", "length": 15, "precision": 2, "nonneg": true }
```
Раздельная форма эквивалентна `String(100)`, `Number(15,2)`, `Number(15,2,nonneg)`.
Если `type` уже содержит скобки — `length`/`precision` игнорируются.
### 4.3 Флаги
| Флаг | Действие | Применимость |
@@ -243,8 +254,13 @@ JSON DSL для описания объектов метаданных конф
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueType` | `String` | Type |
| `length` | — | Длина строки (если valueType=String) |
| `precision` | — | Точность числа (если valueType=Number) |
| `dataLockControlMode` | `Automatic` | DataLockControlMode |
`valueType` + `length`/`precision` работают аналогично раздельной форме типа (§4.2):
`"valueType": "String", "length": 100``String(100)`.
### 7.5 InformationRegister
| Поле JSON | Умолчание | XML элемент |
@@ -277,11 +293,13 @@ JSON DSL для описания объектов метаданных конф
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueTypes` | `[]` | Type (составной — массив `v8:Type`) |
| `valueType` | — | Алиас для `valueTypes` (принимает строку или массив) |
Без ChildObjects и модулей.
Без ChildObjects и модулей. Принимается как `valueTypes` (мн.ч.), так и `valueType` (ед.ч.).
```json
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
### 7.8 CommonModule
@@ -313,7 +331,7 @@ JSON DSL для описания объектов метаданных конф
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `methodName` | `""` | MethodName |
| `methodName` | `""` | MethodName (авто-префикс `CommonModule.`) |
| `description` | = synonym | Description |
| `key` | `""` | Key |
| `use` | `false` | Use |
@@ -323,6 +341,8 @@ JSON DSL для описания объектов метаданных конф
Без ChildObjects и модулей.
Формат `methodName`: `"МодульСервер.Процедура"` — при компиляции авто-дополняется до `CommonModule.МодульСервер.Процедура`. Если уже содержит `CommonModule.` — оставляется как есть.
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
```
@@ -333,12 +353,14 @@ JSON DSL для описания объектов метаданных конф
|-----------|----------|-------------|
| `source` | `[]` | Source (массив `v8:Type`, формат `cfg:XxxObject.Name`) |
| `event` | `BeforeWrite` | Event |
| `handler` | `""` | Handler |
| `handler` | `""` | Handler (авто-префикс `CommonModule.`) |
Без ChildObjects и модулей.
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing` и др.
Формат `handler`: `"МодульСервер.Процедура"` — при компиляции авто-дополняется до `CommonModule.МодульСервер.Процедура`. Если уже содержит `CommonModule.` — оставляется как есть.
```json
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
```
@@ -382,11 +404,8 @@ JSON DSL для описания объектов метаданных конф
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `codeType` | `String` | CodeType |
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
| `descriptionLength` | `100` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `distributedInfoBase` | `false` | DistributedInfoBase |
| `includeConfigurationExtensions` | `false` | IncludeConfigurationExtensions |
| `dataLockControlMode` | `Automatic` | DataLockControlMode |
@@ -419,7 +438,7 @@ JSON DSL для описания объектов метаданных конф
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
\* Если `valueTypes` не указан, по умолчанию: Boolean, String, Number(15,2), DateTime.
\* Если `valueTypes` не указан, по умолчанию: Boolean, String(100), Number(15,2), DateTime.
Модули: `Ext/ObjectModule.bsl` (пустой).
@@ -580,6 +599,7 @@ DSL для `columns` (§12).
| `autonumbering` | `true` | Autonumbering |
| `dataLockControlMode` | `Automatic` | DataLockControlMode |
| `fullTextSearch` | `Use` | FullTextSearch |
| `task` | `""` | Task (ссылка на Task.XXX) |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
@@ -587,7 +607,7 @@ DSL для `columns` (§12).
Дополнительно: `Ext/Flowchart.xml` (заглушка карты маршрута).
```json
{ "type": "BusinessProcess", "name": "Задание", "attributes": ["Описание: String(200)"] }
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
```
### 7.21 Task
@@ -904,7 +924,7 @@ DSL для `columns` (§12).
### Бизнес-процесс
```json
{ "type": "BusinessProcess", "name": "Задание", "attributes": ["Описание: String(200)"] }
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
```
### Задача