mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-15 07:49:01 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33a41c732 |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex-py"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
|
|
||||||
"owner": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": "./",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "github",
|
|
||||||
"repo": "Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"ref": "port-claude-code-py"
|
|
||||||
},
|
|
||||||
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "1c-skills",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
name: cf-init
|
|
||||||
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
|
||||||
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /cf-init — Создание пустой конфигурации 1С
|
|
||||||
|
|
||||||
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
|
||||||
|
|
||||||
## Параметры и команда
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `Name` | Имя конфигурации (обязат.) |
|
|
||||||
| `Synonym` | Синоним (= Name если не указан) |
|
|
||||||
| `OutputDir` | Каталог для создания (default: `src`) |
|
|
||||||
| `Version` | Версия конфигурации |
|
|
||||||
| `Vendor` | Поставщик |
|
|
||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Базовая конфигурация
|
|
||||||
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
|
||||||
|
|
||||||
# С версией и поставщиком
|
|
||||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
|
||||||
|
|
||||||
# Другой режим совместимости
|
|
||||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
|
||||||
```
|
|
||||||
|
|
||||||
## Верификация
|
|
||||||
|
|
||||||
```
|
|
||||||
/cf-init TestConfig -OutputDir test-tmp/cf
|
|
||||||
/cf-info test-tmp/cf — проверить созданное
|
|
||||||
/cf-validate test-tmp/cf — валидировать
|
|
||||||
```
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Name,
|
|
||||||
[string]$Synonym = $Name,
|
|
||||||
[string]$OutputDir = "src",
|
|
||||||
[string]$Version,
|
|
||||||
[string]$Vendor,
|
|
||||||
[string]$CompatibilityMode = "Version8_3_24"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve output dir ---
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
|
||||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Check existing ---
|
|
||||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
|
||||||
if (Test-Path $cfgFile) {
|
|
||||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Generate UUIDs ---
|
|
||||||
$uuidCfg = [guid]::NewGuid().ToString()
|
|
||||||
$uuidLang = [guid]::NewGuid().ToString()
|
|
||||||
# 7 ContainedObject ObjectIds
|
|
||||||
$co1 = [guid]::NewGuid().ToString()
|
|
||||||
$co2 = [guid]::NewGuid().ToString()
|
|
||||||
$co3 = [guid]::NewGuid().ToString()
|
|
||||||
$co4 = [guid]::NewGuid().ToString()
|
|
||||||
$co5 = [guid]::NewGuid().ToString()
|
|
||||||
$co6 = [guid]::NewGuid().ToString()
|
|
||||||
$co7 = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
|
||||||
$mobileFuncs = @(
|
|
||||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
|
||||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
|
||||||
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
|
||||||
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
|
||||||
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
|
||||||
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
|
||||||
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
|
||||||
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
|
||||||
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
|
||||||
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
|
||||||
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
|
||||||
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
|
||||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
|
||||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
|
||||||
)
|
|
||||||
|
|
||||||
$mobileXml = ""
|
|
||||||
foreach ($mf in $mobileFuncs) {
|
|
||||||
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Synonym XML ---
|
|
||||||
$synonymXml = ""
|
|
||||||
if ($Synonym) {
|
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Optional properties ---
|
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
|
||||||
$cfgXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<Configuration uuid="$uuidCfg">
|
|
||||||
<InternalInfo>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co1</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co2</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co3</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co4</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co5</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co6</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
|
||||||
<xr:ObjectId>$co7</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
</InternalInfo>
|
|
||||||
<Properties>
|
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
|
||||||
<Synonym>$synonymXml</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<NamePrefix/>
|
|
||||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
|
||||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
|
||||||
<UsePurposes>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
</UsePurposes>
|
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
|
||||||
<DefaultRoles/>
|
|
||||||
<Vendor>$vendorXml</Vendor>
|
|
||||||
<Version>$versionXml</Version>
|
|
||||||
<UpdateCatalogAddress/>
|
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
|
||||||
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
|
||||||
<AdditionalFullTextSearchDictionaries/>
|
|
||||||
<CommonSettingsStorage/>
|
|
||||||
<ReportsUserSettingsStorage/>
|
|
||||||
<ReportsVariantsStorage/>
|
|
||||||
<FormDataSettingsStorage/>
|
|
||||||
<DynamicListsUserSettingsStorage/>
|
|
||||||
<URLExternalDataStorage/>
|
|
||||||
<Content/>
|
|
||||||
<DefaultReportForm/>
|
|
||||||
<DefaultReportVariantForm/>
|
|
||||||
<DefaultReportSettingsForm/>
|
|
||||||
<DefaultReportAppearanceTemplate/>
|
|
||||||
<DefaultDynamicListSettingsForm/>
|
|
||||||
<DefaultSearchForm/>
|
|
||||||
<DefaultDataHistoryChangeHistoryForm/>
|
|
||||||
<DefaultDataHistoryVersionDataForm/>
|
|
||||||
<DefaultDataHistoryVersionDifferencesForm/>
|
|
||||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
|
||||||
<RequiredMobileApplicationPermissions/>
|
|
||||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
|
||||||
</UsedMobileApplicationFunctionalities>
|
|
||||||
<StandaloneConfigurationRestrictionRoles/>
|
|
||||||
<MobileApplicationURLs/>
|
|
||||||
<AllowedIncomingShareRequestTypes/>
|
|
||||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
|
||||||
<DefaultInterface/>
|
|
||||||
<DefaultStyle/>
|
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
|
||||||
<BriefInformation/>
|
|
||||||
<DetailedInformation/>
|
|
||||||
<Copyright/>
|
|
||||||
<VendorInformationAddress/>
|
|
||||||
<ConfigurationInformationAddress/>
|
|
||||||
<DataLockControlMode>Managed</DataLockControlMode>
|
|
||||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
|
||||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
|
||||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
|
||||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
|
||||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
|
||||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
|
||||||
<DefaultConstantsForm/>
|
|
||||||
</Properties>
|
|
||||||
<ChildObjects>
|
|
||||||
<Language>Русский</Language>
|
|
||||||
</ChildObjects>
|
|
||||||
</Configuration>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
# --- Languages/Русский.xml ---
|
|
||||||
$langXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<Language uuid="$uuidLang">
|
|
||||||
<Properties>
|
|
||||||
<Name>Русский</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>Русский</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<LanguageCode>ru</LanguageCode>
|
|
||||||
</Properties>
|
|
||||||
</Language>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
|
||||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
|
||||||
# via panelDef but not placed by default. Without this file the web client renders
|
|
||||||
# section icons without labels (icon-only mode).
|
|
||||||
$openPanelInst = [guid]::NewGuid().ToString()
|
|
||||||
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
|
||||||
$caiXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
|
||||||
<top>
|
|
||||||
<panel id="$openPanelInst">
|
|
||||||
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
|
||||||
</panel>
|
|
||||||
</top>
|
|
||||||
<left>
|
|
||||||
<panel id="$sectionsPanelInst">
|
|
||||||
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
|
||||||
</panel>
|
|
||||||
</left>
|
|
||||||
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
|
||||||
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
|
||||||
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
|
||||||
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
|
||||||
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
|
||||||
</ClientApplicationInterface>
|
|
||||||
"@
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
if (-not (Test-Path $OutputDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$langDir = Join-Path $OutputDir "Languages"
|
|
||||||
if (-not (Test-Path $langDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$extDir = Join-Path $OutputDir "Ext"
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Write files with UTF-8 BOM ---
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
|
||||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
|
||||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
|
||||||
Write-Host " Каталог: $OutputDir"
|
|
||||||
Write-Host " Configuration.xml: $cfgFile"
|
|
||||||
Write-Host " Languages: $langFile"
|
|
||||||
Write-Host " Ext/CAI: $caiFile"
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
|
||||||
import sys, os, argparse, uuid
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
def new_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
|
|
||||||
parser.add_argument('-Version', dest='Version', default='')
|
|
||||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
|
||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
output_dir = args.OutputDir
|
|
||||||
version = args.Version
|
|
||||||
vendor = args.Vendor
|
|
||||||
compat = args.CompatibilityMode
|
|
||||||
|
|
||||||
# --- Resolve output dir ---
|
|
||||||
if not os.path.isabs(output_dir):
|
|
||||||
output_dir = os.path.join(os.getcwd(), output_dir)
|
|
||||||
|
|
||||||
# --- Check existing ---
|
|
||||||
cfg_file = os.path.join(output_dir, "Configuration.xml")
|
|
||||||
if os.path.exists(cfg_file):
|
|
||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Generate UUIDs ---
|
|
||||||
uuid_cfg = new_uuid()
|
|
||||||
uuid_lang = new_uuid()
|
|
||||||
co = [new_uuid() for _ in range(7)]
|
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
|
||||||
mobile_funcs = [
|
|
||||||
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
|
||||||
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
|
||||||
("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"),
|
|
||||||
("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"),
|
|
||||||
("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"),
|
|
||||||
("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"),
|
|
||||||
("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"),
|
|
||||||
("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"),
|
|
||||||
("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"),
|
|
||||||
("OSBackup","true"), ("ApplicationUsageStatistics","false"),
|
|
||||||
("BarcodeScanning","false"), ("BackgroundAudioRecording","false"),
|
|
||||||
("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"),
|
|
||||||
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
|
||||||
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
|
||||||
]
|
|
||||||
|
|
||||||
mobile_xml = ""
|
|
||||||
for func_name, func_use in mobile_funcs:
|
|
||||||
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
|
|
||||||
|
|
||||||
# --- Synonym XML ---
|
|
||||||
synonym_xml = ""
|
|
||||||
if synonym:
|
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
|
||||||
version_xml = esc_xml(version) if version else ""
|
|
||||||
|
|
||||||
class_ids = [
|
|
||||||
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
|
||||||
"9fcd25a0-4822-11d4-9414-008048da11f9",
|
|
||||||
"e3687481-0a87-462c-a166-9f34594f9bba",
|
|
||||||
"9de14907-ec23-4a07-96f0-85521cb6b53b",
|
|
||||||
"51f2d5d8-ea4d-4064-8892-82951750031e",
|
|
||||||
"e68182ea-4237-4383-967f-90c1e3370bc7",
|
|
||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
|
||||||
]
|
|
||||||
|
|
||||||
contained_objects = ""
|
|
||||||
for i in range(7):
|
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
|
|
||||||
\t\t\t</xr:ContainedObject>\n"""
|
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
{contained_objects}\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<NamePrefix/>
|
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
|
||||||
\t\t\t<UsePurposes>
|
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
\t\t\t</UsePurposes>
|
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
|
||||||
\t\t\t<DefaultRoles/>
|
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
|
||||||
\t\t\t<UpdateCatalogAddress/>
|
|
||||||
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
|
||||||
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
|
||||||
\t\t\t<AdditionalFullTextSearchDictionaries/>
|
|
||||||
\t\t\t<CommonSettingsStorage/>
|
|
||||||
\t\t\t<ReportsUserSettingsStorage/>
|
|
||||||
\t\t\t<ReportsVariantsStorage/>
|
|
||||||
\t\t\t<FormDataSettingsStorage/>
|
|
||||||
\t\t\t<DynamicListsUserSettingsStorage/>
|
|
||||||
\t\t\t<URLExternalDataStorage/>
|
|
||||||
\t\t\t<Content/>
|
|
||||||
\t\t\t<DefaultReportForm/>
|
|
||||||
\t\t\t<DefaultReportVariantForm/>
|
|
||||||
\t\t\t<DefaultReportSettingsForm/>
|
|
||||||
\t\t\t<DefaultReportAppearanceTemplate/>
|
|
||||||
\t\t\t<DefaultDynamicListSettingsForm/>
|
|
||||||
\t\t\t<DefaultSearchForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
|
||||||
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
|
|
||||||
\t\t\t<RequiredMobileApplicationPermissions/>
|
|
||||||
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
|
||||||
\t\t\t</UsedMobileApplicationFunctionalities>
|
|
||||||
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
|
||||||
\t\t\t<MobileApplicationURLs/>
|
|
||||||
\t\t\t<AllowedIncomingShareRequestTypes/>
|
|
||||||
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
|
||||||
\t\t\t<DefaultInterface/>
|
|
||||||
\t\t\t<DefaultStyle/>
|
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
|
||||||
\t\t\t<BriefInformation/>
|
|
||||||
\t\t\t<DetailedInformation/>
|
|
||||||
\t\t\t<Copyright/>
|
|
||||||
\t\t\t<VendorInformationAddress/>
|
|
||||||
\t\t\t<ConfigurationInformationAddress/>
|
|
||||||
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
|
|
||||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
|
||||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
|
||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
|
||||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
|
||||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
|
||||||
\t\t\t<DefaultConstantsForm/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t<ChildObjects>
|
|
||||||
\t\t\t<Language>Русский</Language>
|
|
||||||
\t\t</ChildObjects>
|
|
||||||
\t</Configuration>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
# --- Languages/Русский.xml ---
|
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
\t<Language uuid="{uuid_lang}">
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>Русский</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>Русский</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<LanguageCode>ru</LanguageCode>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t</Language>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
|
||||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
|
||||||
# via panelDef but not placed by default. Without this file the web client renders
|
|
||||||
# section icons without labels (icon-only mode).
|
|
||||||
open_panel_inst = new_uuid()
|
|
||||||
sections_panel_inst = new_uuid()
|
|
||||||
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
|
||||||
\t<top>
|
|
||||||
\t\t<panel id="{open_panel_inst}">
|
|
||||||
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
|
||||||
\t\t</panel>
|
|
||||||
\t</top>
|
|
||||||
\t<left>
|
|
||||||
\t\t<panel id="{sections_panel_inst}">
|
|
||||||
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
|
||||||
\t\t</panel>
|
|
||||||
\t</left>
|
|
||||||
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
|
||||||
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
|
||||||
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
|
||||||
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
|
||||||
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
|
||||||
</ClientApplicationInterface>'''
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
lang_dir = os.path.join(output_dir, "Languages")
|
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
|
||||||
ext_dir = os.path.join(output_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Write files ---
|
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
|
||||||
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
|
||||||
write_utf8_bom(cai_file, cai_xml)
|
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
|
||||||
print(f" Каталог: {output_dir}")
|
|
||||||
print(f" Configuration.xml: {cfg_file}")
|
|
||||||
print(f" Languages: {lang_file}")
|
|
||||||
print(f" Ext/CAI: {cai_file}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: cfe-patch-method
|
|
||||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
|
||||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /cfe-patch-method — Генерация перехватчика метода
|
|
||||||
|
|
||||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
|
||||||
|
|
||||||
## Предусловие
|
|
||||||
|
|
||||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Описание | По умолчанию |
|
|
||||||
|----------|----------|--------------|
|
|
||||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
|
||||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
|
||||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
|
||||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
|
||||||
| `Context` | Директива контекста | `НаСервере` |
|
|
||||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
|
||||||
|
|
||||||
## Формат ModulePath
|
|
||||||
|
|
||||||
| ModulePath | Файл |
|
|
||||||
|------------|------|
|
|
||||||
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
|
||||||
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
|
||||||
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
|
||||||
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
|
||||||
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
|
||||||
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
|
||||||
|
|
||||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
|
||||||
|
|
||||||
## Типы перехвата
|
|
||||||
|
|
||||||
| InterceptorType | Декоратор | Назначение |
|
|
||||||
|-----------------|-----------|------------|
|
|
||||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
|
||||||
| `After` | `&После` | Код после вызова оригинального метода |
|
|
||||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Перехват &Перед на сервере
|
|
||||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
|
||||||
|
|
||||||
# Перехват &После на клиенте
|
|
||||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
|
||||||
|
|
||||||
# ИзменениеИКонтроль для функции
|
|
||||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
|
||||||
```
|
|
||||||
|
|
||||||
## Генерируемый код (Before)
|
|
||||||
|
|
||||||
```bsl
|
|
||||||
&НаСервере
|
|
||||||
&Перед("ПриЗаписи")
|
|
||||||
Процедура Расш1_ПриЗаписи()
|
|
||||||
// TODO: код перед вызовом оригинального метода
|
|
||||||
КонецПроцедуры
|
|
||||||
```
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ExtensionPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ModulePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$MethodName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[ValidateSet("Before","After","ModificationAndControl")]
|
|
||||||
[string]$InterceptorType,
|
|
||||||
|
|
||||||
[string]$Context = "НаСервере",
|
|
||||||
|
|
||||||
[switch]$IsFunction
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve extension path ---
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
|
||||||
}
|
|
||||||
if (Test-Path $ExtensionPath -PathType Leaf) {
|
|
||||||
$ExtensionPath = Split-Path $ExtensionPath -Parent
|
|
||||||
}
|
|
||||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
|
||||||
if (-not (Test-Path $cfgFile)) {
|
|
||||||
Write-Error "Configuration.xml not found in: $ExtensionPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Read NamePrefix from Configuration.xml ---
|
|
||||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$cfgDoc.PreserveWhitespace = $false
|
|
||||||
$cfgDoc.Load($cfgFile)
|
|
||||||
|
|
||||||
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
|
|
||||||
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
|
||||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
|
||||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
|
||||||
|
|
||||||
# --- Map ModulePath to file path ---
|
|
||||||
# ModulePath formats:
|
|
||||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
|
||||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
|
||||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
|
||||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
|
||||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
|
||||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
|
|
||||||
$typeDirMap = @{
|
|
||||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
|
||||||
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
|
||||||
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
|
|
||||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
|
||||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
|
||||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
|
||||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
|
||||||
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
|
||||||
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
|
|
||||||
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
|
|
||||||
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
|
|
||||||
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
|
||||||
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
|
|
||||||
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
|
|
||||||
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
|
|
||||||
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = $ModulePath.Split(".")
|
|
||||||
if ($parts.Count -lt 2) {
|
|
||||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$objType = $parts[0]
|
|
||||||
$objName = $parts[1]
|
|
||||||
|
|
||||||
if (-not $typeDirMap.ContainsKey($objType)) {
|
|
||||||
Write-Error "Unknown object type: $objType"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$dirName = $typeDirMap[$objType]
|
|
||||||
|
|
||||||
$bslFile = $null
|
|
||||||
if ($objType -eq "CommonModule") {
|
|
||||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
|
||||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
|
|
||||||
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
|
||||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
$formName = $parts[3]
|
|
||||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
|
|
||||||
} elseif ($parts.Count -ge 3) {
|
|
||||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
|
||||||
$moduleName = $parts[2]
|
|
||||||
$moduleFileName = switch ($moduleName) {
|
|
||||||
"ObjectModule" { "ObjectModule.bsl" }
|
|
||||||
"ManagerModule" { "ManagerModule.bsl" }
|
|
||||||
"RecordSetModule" { "RecordSetModule.bsl" }
|
|
||||||
"CommandModule" { "CommandModule.bsl" }
|
|
||||||
default { "$moduleName.bsl" }
|
|
||||||
}
|
|
||||||
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
|
|
||||||
} else {
|
|
||||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Map InterceptorType to decorator ---
|
|
||||||
$decorator = switch ($InterceptorType) {
|
|
||||||
"Before" { "&Перед" }
|
|
||||||
"After" { "&После" }
|
|
||||||
"ModificationAndControl" { "&ИзменениеИКонтроль" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Map Context to annotation ---
|
|
||||||
$contextAnnotation = switch ($Context) {
|
|
||||||
"НаСервере" { "&НаСервере" }
|
|
||||||
"НаКлиенте" { "&НаКлиенте" }
|
|
||||||
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
|
|
||||||
default { "&$Context" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Procedure name ---
|
|
||||||
$procName = "${namePrefix}${MethodName}"
|
|
||||||
|
|
||||||
# --- Generate BSL code ---
|
|
||||||
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
|
|
||||||
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
|
||||||
|
|
||||||
$bodyLines = @()
|
|
||||||
switch ($InterceptorType) {
|
|
||||||
"Before" {
|
|
||||||
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
|
|
||||||
}
|
|
||||||
"After" {
|
|
||||||
$bodyLines += "`t// TODO: код после вызова оригинального метода"
|
|
||||||
}
|
|
||||||
"ModificationAndControl" {
|
|
||||||
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
|
|
||||||
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($IsFunction) {
|
|
||||||
$bodyLines += "`t"
|
|
||||||
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
|
|
||||||
}
|
|
||||||
|
|
||||||
$bslCode = @()
|
|
||||||
$bslCode += "$contextAnnotation"
|
|
||||||
$bslCode += "${decorator}(`"$MethodName`")"
|
|
||||||
$bslCode += "$keyword ${procName}()"
|
|
||||||
$bslCode += $bodyLines
|
|
||||||
$bslCode += "$endKeyword"
|
|
||||||
|
|
||||||
$bslText = ($bslCode -join "`r`n") + "`r`n"
|
|
||||||
|
|
||||||
# --- Check form borrowing for .Form. paths ---
|
|
||||||
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
|
||||||
$formName = $parts[3]
|
|
||||||
$dirName = $typeDirMap[$objType]
|
|
||||||
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
|
|
||||||
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
|
||||||
|
|
||||||
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
|
|
||||||
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
|
|
||||||
Write-Host " Run /cfe-borrow first:"
|
|
||||||
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Check if file exists and append ---
|
|
||||||
$bslDir = Split-Path $bslFile -Parent
|
|
||||||
if (-not (Test-Path $bslDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
if (Test-Path $bslFile) {
|
|
||||||
# Append to existing file
|
|
||||||
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
|
|
||||||
$separator = "`r`n"
|
|
||||||
if ($existing -and -not $existing.EndsWith("`n")) {
|
|
||||||
$separator = "`r`n`r`n"
|
|
||||||
}
|
|
||||||
$newContent = $existing + $separator + $bslText
|
|
||||||
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
|
|
||||||
Write-Host "[OK] Добавлен перехватчик в существующий файл"
|
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
|
|
||||||
Write-Host "[OK] Создан файл модуля"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host " Файл: $bslFile"
|
|
||||||
Write-Host " Декоратор: $decorator(`"$MethodName`")"
|
|
||||||
Write-Host " Процедура: ${procName}()"
|
|
||||||
Write-Host " Контекст: $contextAnnotation"
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate method interceptor for 1C extension (CFE)",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-ExtensionPath", required=True)
|
|
||||||
parser.add_argument("-ModulePath", required=True)
|
|
||||||
parser.add_argument("-MethodName", required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"-InterceptorType",
|
|
||||||
required=True,
|
|
||||||
choices=["Before", "After", "ModificationAndControl"],
|
|
||||||
)
|
|
||||||
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
|
|
||||||
parser.add_argument("-IsFunction", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
|
||||||
module_path = args.ModulePath
|
|
||||||
method_name = args.MethodName
|
|
||||||
interceptor_type = args.InterceptorType
|
|
||||||
context = args.Context
|
|
||||||
is_function = args.IsFunction
|
|
||||||
|
|
||||||
# --- Resolve extension path ---
|
|
||||||
if not os.path.isabs(extension_path):
|
|
||||||
extension_path = os.path.join(os.getcwd(), extension_path)
|
|
||||||
if os.path.isfile(extension_path):
|
|
||||||
extension_path = os.path.dirname(extension_path)
|
|
||||||
|
|
||||||
cfg_file = os.path.join(extension_path, "Configuration.xml")
|
|
||||||
if not os.path.isfile(cfg_file):
|
|
||||||
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Read NamePrefix from Configuration.xml ---
|
|
||||||
tree = ET.parse(cfg_file)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
props_node = root.find(".//md:Configuration/md:Properties", ns)
|
|
||||||
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
|
|
||||||
if props_node is not None:
|
|
||||||
prefix_node = props_node.find("md:NamePrefix", ns)
|
|
||||||
if prefix_node is not None and prefix_node.text:
|
|
||||||
name_prefix = prefix_node.text
|
|
||||||
|
|
||||||
# --- Map ModulePath to file path ---
|
|
||||||
# ModulePath formats:
|
|
||||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
|
||||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
|
||||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
|
||||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
|
||||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
|
||||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
|
|
||||||
type_dir_map = {
|
|
||||||
"Catalog": "Catalogs",
|
|
||||||
"Document": "Documents",
|
|
||||||
"Enum": "Enums",
|
|
||||||
"CommonModule": "CommonModules",
|
|
||||||
"Report": "Reports",
|
|
||||||
"DataProcessor": "DataProcessors",
|
|
||||||
"ExchangePlan": "ExchangePlans",
|
|
||||||
"ChartOfAccounts": "ChartsOfAccounts",
|
|
||||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
|
||||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcess": "BusinessProcesses",
|
|
||||||
"Task": "Tasks",
|
|
||||||
"InformationRegister": "InformationRegisters",
|
|
||||||
"AccumulationRegister": "AccumulationRegisters",
|
|
||||||
"AccountingRegister": "AccountingRegisters",
|
|
||||||
"CalculationRegister": "CalculationRegisters",
|
|
||||||
"Catalogs": "Catalogs",
|
|
||||||
"Documents": "Documents",
|
|
||||||
"Enums": "Enums",
|
|
||||||
"CommonModules": "CommonModules",
|
|
||||||
"Reports": "Reports",
|
|
||||||
"DataProcessors": "DataProcessors",
|
|
||||||
"ExchangePlans": "ExchangePlans",
|
|
||||||
"ChartsOfAccounts": "ChartsOfAccounts",
|
|
||||||
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
|
||||||
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcesses": "BusinessProcesses",
|
|
||||||
"Tasks": "Tasks",
|
|
||||||
"InformationRegisters": "InformationRegisters",
|
|
||||||
"AccumulationRegisters": "AccumulationRegisters",
|
|
||||||
"AccountingRegisters": "AccountingRegisters",
|
|
||||||
"CalculationRegisters": "CalculationRegisters",
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = module_path.split(".")
|
|
||||||
if len(parts) < 2:
|
|
||||||
print(
|
|
||||||
f"Invalid ModulePath format: {module_path}. "
|
|
||||||
"Expected: Type.Name.Module or CommonModule.Name",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
obj_type = parts[0]
|
|
||||||
obj_name = parts[1]
|
|
||||||
|
|
||||||
if obj_type not in type_dir_map:
|
|
||||||
print(f"Unknown object type: {obj_type}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
dir_name = type_dir_map[obj_type]
|
|
||||||
|
|
||||||
bsl_file = None
|
|
||||||
if obj_type == "CommonModule":
|
|
||||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
|
||||||
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
|
|
||||||
elif len(parts) >= 4 and parts[2] == "Form":
|
|
||||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
|
||||||
form_name = parts[3]
|
|
||||||
bsl_file = os.path.join(
|
|
||||||
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
|
|
||||||
)
|
|
||||||
elif len(parts) >= 3:
|
|
||||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
|
||||||
module_name = parts[2]
|
|
||||||
module_file_map = {
|
|
||||||
"ObjectModule": "ObjectModule.bsl",
|
|
||||||
"ManagerModule": "ManagerModule.bsl",
|
|
||||||
"RecordSetModule": "RecordSetModule.bsl",
|
|
||||||
"CommandModule": "CommandModule.bsl",
|
|
||||||
}
|
|
||||||
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
|
|
||||||
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Invalid ModulePath format: {module_path}. "
|
|
||||||
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Map InterceptorType to decorator ---
|
|
||||||
decorator_map = {
|
|
||||||
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
|
|
||||||
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
|
|
||||||
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
|
|
||||||
}
|
|
||||||
decorator = decorator_map[interceptor_type]
|
|
||||||
|
|
||||||
# --- Map Context to annotation ---
|
|
||||||
context_map = {
|
|
||||||
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
|
|
||||||
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
|
|
||||||
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
|
|
||||||
}
|
|
||||||
context_annotation = context_map.get(context, f"&{context}")
|
|
||||||
|
|
||||||
# --- Procedure name ---
|
|
||||||
proc_name = f"{name_prefix}{method_name}"
|
|
||||||
|
|
||||||
# --- Generate BSL code ---
|
|
||||||
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
|
|
||||||
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
|
|
||||||
|
|
||||||
body_lines = []
|
|
||||||
if interceptor_type == "Before":
|
|
||||||
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
|
|
||||||
elif interceptor_type == "After":
|
|
||||||
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
|
|
||||||
elif interceptor_type == "ModificationAndControl":
|
|
||||||
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
|
|
||||||
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
|
|
||||||
|
|
||||||
if is_function:
|
|
||||||
body_lines.append("\t")
|
|
||||||
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
|
|
||||||
|
|
||||||
bsl_code = [
|
|
||||||
context_annotation,
|
|
||||||
f'{decorator}("{method_name}")',
|
|
||||||
f"{keyword} {proc_name}()",
|
|
||||||
]
|
|
||||||
bsl_code.extend(body_lines)
|
|
||||||
bsl_code.append(end_keyword)
|
|
||||||
|
|
||||||
bsl_text = "\r\n".join(bsl_code) + "\r\n"
|
|
||||||
|
|
||||||
# --- Check form borrowing for .Form. paths ---
|
|
||||||
if len(parts) >= 4 and parts[2] == "Form":
|
|
||||||
form_name = parts[3]
|
|
||||||
form_meta_file = os.path.join(
|
|
||||||
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
|
|
||||||
)
|
|
||||||
form_xml_file = os.path.join(
|
|
||||||
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
|
|
||||||
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
|
|
||||||
print(" Run /cfe-borrow first:")
|
|
||||||
print(
|
|
||||||
f" /cfe-borrow -ExtensionPath {extension_path} "
|
|
||||||
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# --- Check if file exists and append ---
|
|
||||||
bsl_dir = os.path.dirname(bsl_file)
|
|
||||||
if not os.path.isdir(bsl_dir):
|
|
||||||
os.makedirs(bsl_dir, exist_ok=True)
|
|
||||||
|
|
||||||
if os.path.isfile(bsl_file):
|
|
||||||
# Append to existing file
|
|
||||||
with open(bsl_file, "r", encoding="utf-8-sig", newline="") as f:
|
|
||||||
existing = f.read()
|
|
||||||
|
|
||||||
separator = "\r\n"
|
|
||||||
if existing and not existing.endswith("\n"):
|
|
||||||
separator = "\r\n\r\n"
|
|
||||||
new_content = existing + separator + bsl_text
|
|
||||||
|
|
||||||
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
|
|
||||||
f.write(new_content)
|
|
||||||
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
|
|
||||||
else:
|
|
||||||
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
|
|
||||||
f.write(bsl_text)
|
|
||||||
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
|
|
||||||
|
|
||||||
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
|
|
||||||
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
|
|
||||||
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
|
|
||||||
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# db-create v1.0 — Create 1C information base
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Создание информационной базы 1С
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Создаёт новую информационную базу 1С (файловую или серверную).
|
|
||||||
Поддерживает создание из шаблона и добавление в список баз.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UseTemplate
|
|
||||||
Путь к файлу шаблона (.cf или .dt)
|
|
||||||
|
|
||||||
.PARAMETER AddToList
|
|
||||||
Добавить в список баз 1С
|
|
||||||
|
|
||||||
.PARAMETER ListName
|
|
||||||
Имя базы в списке
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UseTemplate,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AddToList,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$ListName
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate template ---
|
|
||||||
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
|
|
||||||
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("CREATEINFOBASE")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "File=`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Template ---
|
|
||||||
if ($UseTemplate) {
|
|
||||||
$arguments += "/UseTemplate", "`"$UseTemplate`""
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Add to list ---
|
|
||||||
if ($AddToList) {
|
|
||||||
if ($ListName) {
|
|
||||||
$arguments += "/AddToList", "`"$ListName`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/AddToList"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "create_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-create v1.0 — Create 1C information base
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Create 1C information base",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UseTemplate", default="")
|
|
||||||
parser.add_argument("-AddToList", action="store_true")
|
|
||||||
parser.add_argument("-ListName", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate template ---
|
|
||||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
|
||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["CREATEINFOBASE"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
|
||||||
else:
|
|
||||||
arguments.append(f'File="{args.InfoBasePath}"')
|
|
||||||
|
|
||||||
# --- Template ---
|
|
||||||
if args.UseTemplate:
|
|
||||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
|
||||||
|
|
||||||
# --- Add to list ---
|
|
||||||
if args.AddToList:
|
|
||||||
if args.ListName:
|
|
||||||
arguments.extend(["/AddToList", args.ListName])
|
|
||||||
else:
|
|
||||||
arguments.append("/AddToList")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
|
||||||
else:
|
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
|
||||||
else:
|
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
|
||||||
Поддерживает выгрузку расширений.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER OutputFile
|
|
||||||
Путь к выходному CF-файлу
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для выгрузки
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Выгрузить все расширения
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$OutputFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
$outDir = Split-Path $OutputFile -Parent
|
|
||||||
if ($outDir -and -not (Test-Path $outDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/DumpCfg", "`"$OutputFile`""
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C configuration to CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.isdir(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
|
|
||||||
- Full: полная выгрузка всей конфигурации
|
|
||||||
- Changes: инкрементальная выгрузка изменённых объектов
|
|
||||||
- Partial: выгрузка конкретных объектов из списка
|
|
||||||
- UpdateInfo: обновление только ConfigDumpInfo.xml
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER ConfigDir
|
|
||||||
Каталог для выгрузки конфигурации
|
|
||||||
|
|
||||||
.PARAMETER Mode
|
|
||||||
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
|
|
||||||
|
|
||||||
.PARAMETER Objects
|
|
||||||
Имена объектов метаданных через запятую (для режима Partial)
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для выгрузки
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Выгрузить все расширения
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$ConfigDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
|
||||||
[string]$Mode = "Changes",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Objects,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
|
||||||
[string]$Format = "Hierarchical"
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
|
||||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Create output dir if needed ---
|
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
|
||||||
Write-Host "Created output directory: $ConfigDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
|
|
||||||
switch ($Mode) {
|
|
||||||
"Full" {
|
|
||||||
Write-Host "Executing full configuration dump..."
|
|
||||||
}
|
|
||||||
"Changes" {
|
|
||||||
Write-Host "Executing incremental configuration dump..."
|
|
||||||
$arguments += "-update"
|
|
||||||
$arguments += "-force"
|
|
||||||
}
|
|
||||||
"Partial" {
|
|
||||||
Write-Host "Executing partial configuration dump..."
|
|
||||||
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
||||||
|
|
||||||
$listFile = Join-Path $tempDir "dump_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
|
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$listFile`""
|
|
||||||
Write-Host "Objects to dump: $($objectList.Count)"
|
|
||||||
foreach ($obj in $objectList) { Write-Host " $obj" }
|
|
||||||
}
|
|
||||||
"UpdateInfo" {
|
|
||||||
Write-Host "Updating ConfigDumpInfo.xml..."
|
|
||||||
$arguments += "-configDumpInfoOnly"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
|
||||||
Write-Host "Configuration dumped to: $ConfigDir"
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C configuration to XML files",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Mode",
|
|
||||||
default="Changes",
|
|
||||||
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
|
||||||
help="Dump mode (default: Changes)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="Dump format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if args.Mode == "Partial" and not args.Objects:
|
|
||||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create output dir if needed ---
|
|
||||||
if not os.path.exists(args.ConfigDir):
|
|
||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
if args.Mode == "Full":
|
|
||||||
print("Executing full configuration dump...")
|
|
||||||
elif args.Mode == "Changes":
|
|
||||||
print("Executing incremental configuration dump...")
|
|
||||||
arguments.append("-update")
|
|
||||||
arguments.append("-force")
|
|
||||||
elif args.Mode == "Partial":
|
|
||||||
print("Executing partial configuration dump...")
|
|
||||||
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
|
|
||||||
|
|
||||||
list_file = os.path.join(temp_dir, "dump_list.txt")
|
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(object_list))
|
|
||||||
|
|
||||||
arguments += ["-listFile", list_file]
|
|
||||||
print(f"Objects to dump: {len(object_list)}")
|
|
||||||
for obj in object_list:
|
|
||||||
print(f" {obj}")
|
|
||||||
elif args.Mode == "UpdateInfo":
|
|
||||||
print("Updating ConfigDumpInfo.xml...")
|
|
||||||
arguments.append("-configDumpInfoOnly")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments += ["-Extension", args.Extension]
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Dump completed successfully")
|
|
||||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка конфигурации 1С из CF-файла
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
|
||||||
Поддерживает загрузку расширений.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER InputFile
|
|
||||||
Путь к CF-файлу для загрузки
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Загрузить как расширение
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Загрузить все расширения из архива
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$InputFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if (-not (Test-Path $InputFile)) {
|
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/LoadCfg", "`"$InputFile`""
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C configuration from CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/LoadCfg", args.InputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка изменений из Git в базу 1С
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Определяет изменённые файлы конфигурации по данным Git и выполняет
|
|
||||||
частичную загрузку в информационную базу.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER ConfigDir
|
|
||||||
Каталог XML-выгрузки конфигурации (git-репозиторий)
|
|
||||||
|
|
||||||
.PARAMETER Source
|
|
||||||
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
|
|
||||||
|
|
||||||
.PARAMETER CommitRange
|
|
||||||
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для загрузки
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Загрузить все расширения
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
|
||||||
|
|
||||||
.PARAMETER DryRun
|
|
||||||
Только показать что будет загружено (без загрузки)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$ConfigDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
|
|
||||||
[string]$Source = "All",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$CommitRange,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
|
||||||
[string]$Format = "Hierarchical",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$DryRun,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$UpdateDB
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
|
||||||
function Get-ObjectXmlFromSubFile {
|
|
||||||
param([string]$RelativePath)
|
|
||||||
|
|
||||||
$parts = $RelativePath -split '[\\/]'
|
|
||||||
if ($parts.Count -ge 2) {
|
|
||||||
return "$($parts[0])/$($parts[1]).xml"
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
|
||||||
if (-not $DryRun) {
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
|
||||||
if (-not $DryRun) {
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
|
||||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate Commit mode ---
|
|
||||||
if ($Source -eq "Commit" -and -not $CommitRange) {
|
|
||||||
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Check git ---
|
|
||||||
try {
|
|
||||||
$null = git --version 2>&1
|
|
||||||
} catch {
|
|
||||||
Write-Host "Error: git not found in PATH" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
|
||||||
$changedFiles = @()
|
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
|
||||||
|
|
||||||
Push-Location $ConfigDir
|
|
||||||
try {
|
|
||||||
switch ($Source) {
|
|
||||||
"Staged" {
|
|
||||||
Write-Host "Getting staged changes..."
|
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
|
||||||
"Unstaged" {
|
|
||||||
Write-Host "Getting unstaged changes..."
|
|
||||||
$raw = git diff --name-only --relative 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
|
||||||
"Commit" {
|
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
|
||||||
"All" {
|
|
||||||
Write-Host "Getting all uncommitted changes..."
|
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git diff --name-only --relative 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
|
||||||
|
|
||||||
if ($changedFiles.Count -eq 0) {
|
|
||||||
Write-Host "No changes found"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Git changes detected: $($changedFiles.Count) files"
|
|
||||||
|
|
||||||
# --- Filter and map to config files ---
|
|
||||||
$configFiles = @()
|
|
||||||
|
|
||||||
foreach ($file in $changedFiles) {
|
|
||||||
$file = $file.Trim().Replace('\', '/')
|
|
||||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
|
||||||
|
|
||||||
# Skip service files
|
|
||||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
|
||||||
|
|
||||||
$fullPath = Join-Path $ConfigDir $file
|
|
||||||
|
|
||||||
if ($file -match '\.xml$') {
|
|
||||||
# XML file — add directly if exists
|
|
||||||
if (Test-Path $fullPath) {
|
|
||||||
if ($configFiles -notcontains $file) {
|
|
||||||
$configFiles += $file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
|
||||||
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
|
|
||||||
if ($objectXml) {
|
|
||||||
$fullXmlPath = Join-Path $ConfigDir $objectXml
|
|
||||||
if (Test-Path $fullXmlPath) {
|
|
||||||
if ($configFiles -notcontains $objectXml) {
|
|
||||||
$configFiles += $objectXml
|
|
||||||
}
|
|
||||||
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
|
|
||||||
$configFiles += $file
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add all files from Ext/ directory of the object
|
|
||||||
$parts = $file -split '[\\/]'
|
|
||||||
if ($parts.Count -ge 2) {
|
|
||||||
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
|
|
||||||
if (Test-Path $extDir) {
|
|
||||||
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
|
|
||||||
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
|
|
||||||
if ($configFiles -notcontains $extRelPath) {
|
|
||||||
$configFiles += $extRelPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configFiles.Count -eq 0) {
|
|
||||||
Write-Host "No configuration files found in changes"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Files for loading: $($configFiles.Count)"
|
|
||||||
foreach ($f in $configFiles) { Write-Host " $f" }
|
|
||||||
|
|
||||||
# --- DryRun: stop here ---
|
|
||||||
if ($DryRun) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "DryRun mode - no changes applied"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
|
||||||
$arguments += "-listFile", "`"$listFile`""
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
$arguments += "-partial"
|
|
||||||
$arguments += "-updateConfigDumpInfo"
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$arguments += "/UpdateDBCfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Executing partial configuration load..."
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
Write-Host ""
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
|
||||||
if len(parts) >= 2:
|
|
||||||
return f"{parts[0]}/{parts[1]}.xml"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def run_git(config_dir, git_args):
|
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
|
||||||
result = subprocess.run(
|
|
||||||
["git"] + git_args,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
encoding="utf-8",
|
|
||||||
cwd=config_dir,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load Git changes into 1C database",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Source",
|
|
||||||
default="All",
|
|
||||||
choices=["All", "Staged", "Unstaged", "Commit"],
|
|
||||||
help="Change source (default: All)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD")
|
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to load")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="File format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
|
||||||
v8path = None
|
|
||||||
if not args.DryRun:
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
|
||||||
if not args.DryRun:
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if not os.path.exists(args.ConfigDir):
|
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate Commit mode ---
|
|
||||||
if args.Source == "Commit" and not args.CommitRange:
|
|
||||||
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Check git ---
|
|
||||||
try:
|
|
||||||
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
print("Error: git not found in PATH", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
|
||||||
changed_files = []
|
|
||||||
|
|
||||||
if args.Source == "Staged":
|
|
||||||
print("Getting staged changes...")
|
|
||||||
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"])
|
|
||||||
elif args.Source == "Unstaged":
|
|
||||||
print("Getting unstaged changes...")
|
|
||||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"])
|
|
||||||
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
|
|
||||||
elif args.Source == "Commit":
|
|
||||||
print(f"Getting changes from {args.CommitRange}...")
|
|
||||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative", args.CommitRange])
|
|
||||||
elif args.Source == "All":
|
|
||||||
print("Getting all uncommitted changes...")
|
|
||||||
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"])
|
|
||||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"])
|
|
||||||
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
|
|
||||||
|
|
||||||
# Deduplicate and filter blanks
|
|
||||||
changed_files = list(dict.fromkeys(f for f in changed_files if f.strip()))
|
|
||||||
|
|
||||||
if len(changed_files) == 0:
|
|
||||||
print("No changes found")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
print(f"Git changes detected: {len(changed_files)} files")
|
|
||||||
|
|
||||||
# --- Filter and map to config files ---
|
|
||||||
config_files = []
|
|
||||||
|
|
||||||
for file in changed_files:
|
|
||||||
file = file.strip().replace("\\", "/")
|
|
||||||
if not file:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip service files
|
|
||||||
if file == "ConfigDumpInfo.xml":
|
|
||||||
continue
|
|
||||||
|
|
||||||
full_path = os.path.join(args.ConfigDir, file)
|
|
||||||
|
|
||||||
if file.endswith(".xml"):
|
|
||||||
# XML file — add directly if exists
|
|
||||||
if os.path.exists(full_path):
|
|
||||||
if file not in config_files:
|
|
||||||
config_files.append(file)
|
|
||||||
else:
|
|
||||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
|
||||||
object_xml = get_object_xml_from_subfile(file)
|
|
||||||
if object_xml:
|
|
||||||
full_xml_path = os.path.join(args.ConfigDir, object_xml)
|
|
||||||
if os.path.exists(full_xml_path):
|
|
||||||
if object_xml not in config_files:
|
|
||||||
config_files.append(object_xml)
|
|
||||||
if os.path.exists(full_path) and file not in config_files:
|
|
||||||
config_files.append(file)
|
|
||||||
|
|
||||||
# Add all files from Ext/ directory of the object
|
|
||||||
parts = re.split(r"[\\/]", file)
|
|
||||||
if len(parts) >= 2:
|
|
||||||
ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext")
|
|
||||||
if os.path.isdir(ext_dir):
|
|
||||||
for root, dirs, files in os.walk(ext_dir):
|
|
||||||
for fname in files:
|
|
||||||
abs_path = os.path.join(root, fname)
|
|
||||||
rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/")
|
|
||||||
if rel_path not in config_files:
|
|
||||||
config_files.append(rel_path)
|
|
||||||
|
|
||||||
if len(config_files) == 0:
|
|
||||||
print("No configuration files found in changes")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
print(f"Files for loading: {len(config_files)}")
|
|
||||||
for f in config_files:
|
|
||||||
print(f" {f}")
|
|
||||||
|
|
||||||
# --- DryRun: stop here ---
|
|
||||||
if args.DryRun:
|
|
||||||
print("")
|
|
||||||
print("DryRun mode - no changes applied")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(config_files))
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
|
||||||
arguments += ["-listFile", list_file]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
arguments.append("-partial")
|
|
||||||
arguments.append("-updateConfigDumpInfo")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments += ["-Extension", args.Extension]
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if args.UpdateDB:
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print("")
|
|
||||||
print("Executing partial configuration load...")
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
print("")
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Load completed successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
# db-load-xml v1.3 — Load 1C configuration from XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Загружает конфигурацию в информационную базу из XML-файлов.
|
|
||||||
Поддерживает полную и частичную загрузку.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER ConfigDir
|
|
||||||
Каталог XML-исходников конфигурации
|
|
||||||
|
|
||||||
.PARAMETER Mode
|
|
||||||
Режим загрузки: Full или Partial (по умолчанию Full)
|
|
||||||
|
|
||||||
.PARAMETER Files
|
|
||||||
Относительные пути файлов через запятую (для режима Partial)
|
|
||||||
|
|
||||||
.PARAMETER ListFile
|
|
||||||
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для загрузки
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Загрузить все расширения
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$ConfigDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Full", "Partial")]
|
|
||||||
[string]$Mode = "Full",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Files,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$ListFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
|
||||||
[string]$Format = "Hierarchical",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$UpdateDB,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$StrictLog
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
|
||||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
|
||||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
|
||||||
|
|
||||||
if ($Mode -eq "Full") {
|
|
||||||
Write-Host "Executing full configuration load..."
|
|
||||||
} else {
|
|
||||||
Write-Host "Executing partial configuration load..."
|
|
||||||
|
|
||||||
# Build list file
|
|
||||||
$generatedListFile = $null
|
|
||||||
if ($ListFile) {
|
|
||||||
# Use provided list file
|
|
||||||
if (-not (Test-Path $ListFile)) {
|
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$generatedListFile = $ListFile
|
|
||||||
} else {
|
|
||||||
# Generate from -Files parameter
|
|
||||||
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
||||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
|
||||||
|
|
||||||
Write-Host "Files to load: $($fileList.Count)"
|
|
||||||
foreach ($f in $fileList) { Write-Host " $f" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$generatedListFile`""
|
|
||||||
$arguments += "-partial"
|
|
||||||
$arguments += "-updateConfigDumpInfo"
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$arguments += "/UpdateDBCfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Read log ---
|
|
||||||
$logContent = $null
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
$fatalLogPatterns = @(
|
|
||||||
'Неверное свойство объекта метаданных',
|
|
||||||
'не входит в состав объекта метаданных',
|
|
||||||
'Неизвестное имя типа',
|
|
||||||
'Неизвестный объект метаданных',
|
|
||||||
'Ни один из документов не является регистратором для регистра',
|
|
||||||
'Неверное значение перечисления',
|
|
||||||
'не может быть приведен к типу'
|
|
||||||
)
|
|
||||||
$silentFailures = @()
|
|
||||||
if ($logContent) {
|
|
||||||
foreach ($line in ($logContent -split "`r?`n")) {
|
|
||||||
foreach ($pat in $fatalLogPatterns) {
|
|
||||||
if ($line -match [regex]::Escape($pat)) {
|
|
||||||
$silentFailures += $line.Trim()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
|
||||||
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
|
||||||
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($silentFailures.Count -gt 0) {
|
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
|
||||||
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
|
|
||||||
Write-Host $msg -ForegroundColor Yellow
|
|
||||||
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
|
||||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-xml v1.3 — Load 1C configuration from XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C configuration from XML files",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Mode",
|
|
||||||
default="Full",
|
|
||||||
choices=["Full", "Partial"],
|
|
||||||
help="Load mode (default: Full)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
|
||||||
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
|
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to load")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="File format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
|
||||||
parser.add_argument(
|
|
||||||
"-StrictLog",
|
|
||||||
action="store_true",
|
|
||||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if not os.path.exists(args.ConfigDir):
|
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
|
||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
|
||||||
|
|
||||||
if args.Mode == "Full":
|
|
||||||
print("Executing full configuration load...")
|
|
||||||
else:
|
|
||||||
print("Executing partial configuration load...")
|
|
||||||
|
|
||||||
# Build list file
|
|
||||||
generated_list_file = None
|
|
||||||
if args.ListFile:
|
|
||||||
# Use provided list file
|
|
||||||
if not os.path.isfile(args.ListFile):
|
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
generated_list_file = args.ListFile
|
|
||||||
else:
|
|
||||||
# Generate from -Files parameter
|
|
||||||
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(file_list))
|
|
||||||
|
|
||||||
print(f"Files to load: {len(file_list)}")
|
|
||||||
for fl in file_list:
|
|
||||||
print(f" {fl}")
|
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
|
||||||
arguments.append("-partial")
|
|
||||||
arguments.append("-updateConfigDumpInfo")
|
|
||||||
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments += ["-Extension", args.Extension]
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if args.UpdateDB:
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Read log ---
|
|
||||||
log_content = ""
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
except Exception:
|
|
||||||
log_content = ""
|
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
fatal_log_patterns = [
|
|
||||||
"Неверное свойство объекта метаданных",
|
|
||||||
"не входит в состав объекта метаданных",
|
|
||||||
"Неизвестное имя типа",
|
|
||||||
"Неизвестный объект метаданных",
|
|
||||||
"Ни один из документов не является регистратором для регистра",
|
|
||||||
"Неверное значение перечисления",
|
|
||||||
"не может быть приведен к типу",
|
|
||||||
]
|
|
||||||
silent_failures = []
|
|
||||||
if log_content:
|
|
||||||
for line in log_content.splitlines():
|
|
||||||
for pat in fatal_log_patterns:
|
|
||||||
if pat in line:
|
|
||||||
silent_failures.append(line.strip())
|
|
||||||
break
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
|
||||||
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
|
||||||
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Load completed successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
|
|
||||||
if silent_failures:
|
|
||||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
|
||||||
print(
|
|
||||||
f"[warning] log contains {len(silent_failures)} rejection(s) — "
|
|
||||||
f"platform loaded config but dropped properties/refs{suffix}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
for f in silent_failures:
|
|
||||||
print(f" {f}", file=sys.stderr)
|
|
||||||
if args.StrictLog and exit_code == 0:
|
|
||||||
exit_code = 1
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# db-run v1.0 — Launch 1C:Enterprise
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Запуск 1С:Предприятие
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
|
||||||
Запуск в фоне — не ждёт завершения процесса.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER Execute
|
|
||||||
Путь к внешней обработке для запуска
|
|
||||||
|
|
||||||
.PARAMETER CParam
|
|
||||||
Параметр запуска (/C)
|
|
||||||
|
|
||||||
.PARAMETER URL
|
|
||||||
Навигационная ссылка (e1cib/...)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Execute,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$CParam,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$URL
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Build arguments as single string ---
|
|
||||||
# Note: Start-Process without -NoNewWindow uses ShellExecute.
|
|
||||||
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
|
|
||||||
# re-joins elements. Single string avoids this.
|
|
||||||
$argString = "ENTERPRISE"
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$argString += " /F `"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $argString += " /N`"$UserName`"" }
|
|
||||||
if ($Password) { $argString += " /P`"$Password`"" }
|
|
||||||
|
|
||||||
# --- Optional params ---
|
|
||||||
if ($Execute) {
|
|
||||||
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
|
|
||||||
if ($ext -eq ".erf") {
|
|
||||||
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
|
|
||||||
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
|
|
||||||
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
|
|
||||||
$Execute = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($Execute) {
|
|
||||||
$argString += " /Execute `"$Execute`""
|
|
||||||
}
|
|
||||||
if ($CParam) {
|
|
||||||
$argString += " /C `"$CParam`""
|
|
||||||
}
|
|
||||||
if ($URL) {
|
|
||||||
$argString += " /URL `"$URL`""
|
|
||||||
}
|
|
||||||
|
|
||||||
$argString += " /DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
|
||||||
Write-Host "Running: 1cv8.exe $argString"
|
|
||||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
|
||||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-run v1.0 — Launch 1C:Enterprise
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Launch 1C:Enterprise",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-Execute", default="")
|
|
||||||
parser.add_argument("-CParam", default="")
|
|
||||||
parser.add_argument("-URL", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["ENTERPRISE"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
# --- Optional params ---
|
|
||||||
execute = args.Execute
|
|
||||||
if execute:
|
|
||||||
ext = os.path.splitext(execute)[1].lower()
|
|
||||||
if ext == ".erf":
|
|
||||||
print("[WARN] /Execute does not support ERF files (external reports).")
|
|
||||||
print(f" Open the report via File -> Open: {execute}")
|
|
||||||
print(" Launching database without /Execute.")
|
|
||||||
execute = ""
|
|
||||||
|
|
||||||
if execute:
|
|
||||||
arguments.extend(["/Execute", execute])
|
|
||||||
if args.CParam:
|
|
||||||
arguments.extend(["/C", args.CParam])
|
|
||||||
if args.URL:
|
|
||||||
arguments.extend(["/URL", args.URL])
|
|
||||||
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
subprocess.Popen([v8path] + arguments)
|
|
||||||
print("1C:Enterprise launched")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# db-update v1.0 — Update 1C database configuration
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Обновление конфигурации базы данных 1С
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Применяет изменения основной конфигурации к конфигурации базы данных.
|
|
||||||
Поддерживает динамическое обновление, обновление расширений.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для обновления
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Обновить все расширения
|
|
||||||
|
|
||||||
.PARAMETER Dynamic
|
|
||||||
Динамическое обновление: "+" включить, "-" отключить
|
|
||||||
|
|
||||||
.PARAMETER Server
|
|
||||||
Обновление на стороне сервера
|
|
||||||
|
|
||||||
.PARAMETER WarningsAsErrors
|
|
||||||
Предупреждения считать ошибками
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("+", "-")]
|
|
||||||
[string]$Dynamic,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$Server,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$WarningsAsErrors
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/UpdateDBCfg"
|
|
||||||
|
|
||||||
# --- Options ---
|
|
||||||
if ($Dynamic) {
|
|
||||||
$arguments += "-Dynamic$Dynamic"
|
|
||||||
}
|
|
||||||
if ($Server) {
|
|
||||||
$arguments += "-Server"
|
|
||||||
}
|
|
||||||
if ($WarningsAsErrors) {
|
|
||||||
$arguments += "-WarningsAsErrors"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "update_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-update v1.0 — Update 1C database configuration
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Update 1C database configuration",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
|
||||||
parser.add_argument("-Server", action="store_true")
|
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Options ---
|
|
||||||
if args.Dynamic:
|
|
||||||
arguments.append(f"-Dynamic{args.Dynamic}")
|
|
||||||
if args.Server:
|
|
||||||
arguments.append("-Server")
|
|
||||||
if args.WarningsAsErrors:
|
|
||||||
arguments.append("-WarningsAsErrors")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
|
|
||||||
Общий скрипт для epf-build и erf-build.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER SourceFile
|
|
||||||
Путь к корневому XML-файлу исходников
|
|
||||||
|
|
||||||
.PARAMETER OutputFile
|
|
||||||
Путь к выходному EPF/ERF-файлу
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$SourceFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$OutputFile
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
|
||||||
$autoCreatedBase = $null
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
$sourceDir = Split-Path $SourceFile -Parent
|
|
||||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
|
||||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
|
||||||
Write-Host "No database specified. Creating temporary stub database..."
|
|
||||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
|
||||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
|
||||||
if ($stubProc.ExitCode -ne 0) {
|
|
||||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$InfoBasePath = $autoBasePath
|
|
||||||
$autoCreatedBase = $autoBasePath
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate source file ---
|
|
||||||
if (-not (Test-Path $SourceFile)) {
|
|
||||||
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
$outDir = Split-Path $OutputFile -Parent
|
|
||||||
if ($outDir -and -not (Test-Path $outDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "build_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
|
||||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Build external data processor or report (EPF/ERF) from XML sources",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
|
||||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
|
||||||
auto_created_base = None
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
|
||||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
|
||||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
|
||||||
print("No database specified. Creating temporary stub database...")
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
|
|
||||||
capture_output=False,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
print("Error: failed to create stub database", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
args.InfoBasePath = auto_base_path
|
|
||||||
auto_created_base = auto_base_path
|
|
||||||
|
|
||||||
# --- Validate source file ---
|
|
||||||
if not os.path.isfile(args.SourceFile):
|
|
||||||
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.exists(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "build_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Build completed successfully: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
if auto_created_base and os.path.exists(auto_created_base):
|
|
||||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
|
|
||||||
Общий скрипт для epf-dump и erf-dump.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER InputFile
|
|
||||||
Путь к EPF/ERF-файлу
|
|
||||||
|
|
||||||
.PARAMETER OutputDir
|
|
||||||
Каталог для выгрузки исходников
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$InputFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$OutputDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
|
||||||
[string]$Format = "Hierarchical"
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate database connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
|
|
||||||
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if (-not (Test-Path $InputFile)) {
|
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
if (-not (Test-Path $OutputDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump external data processor or report (EPF/ERF) to XML sources",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
|
|
||||||
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="Dump format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate database connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
if not os.path.exists(args.OutputDir):
|
|
||||||
os.makedirs(args.OutputDir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
name: epf-init
|
|
||||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
|
|
||||||
argument-hint: <Name> [Synonym]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /epf-init — Создание новой обработки
|
|
||||||
|
|
||||||
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/epf-init <Name> [Synonym] [SrcDir]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-----------|:------------:|--------------|-------------------------------------|
|
|
||||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дальнейшие шаги
|
|
||||||
|
|
||||||
- Добавить форму: `/form-add`
|
|
||||||
- Добавить макет: `/template-add`
|
|
||||||
- Добавить справку: `/help-add`
|
|
||||||
- Собрать EPF: `/epf-build`
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Name,
|
|
||||||
|
|
||||||
[string]$Synonym = $Name,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$xml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<ExternalDataProcessor uuid="$uuid1">
|
|
||||||
<InternalInfo>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
|
||||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
|
|
||||||
<xr:TypeId>$uuid3</xr:TypeId>
|
|
||||||
<xr:ValueId>$uuid4</xr:ValueId>
|
|
||||||
</xr:GeneratedType>
|
|
||||||
</InternalInfo>
|
|
||||||
<Properties>
|
|
||||||
<Name>$Name</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<DefaultForm/>
|
|
||||||
<AuxiliaryForm/>
|
|
||||||
</Properties>
|
|
||||||
<ChildObjects/>
|
|
||||||
</ExternalDataProcessor>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
|
||||||
$processorDir = Join-Path $SrcDir $Name
|
|
||||||
|
|
||||||
if (Test-Path $rootFile) {
|
|
||||||
Write-Error "Файл уже существует: $rootFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $SrcDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$extDir = Join-Path $processorDir "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана обработка: $rootFile"
|
|
||||||
Write-Host " Каталог: $processorDir"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
"""Generates minimal XML source files for a 1C external data processor."""
|
|
||||||
import sys, os, argparse, uuid
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
def new_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
uuid1 = new_uuid()
|
|
||||||
uuid2 = new_uuid()
|
|
||||||
uuid3 = new_uuid()
|
|
||||||
uuid4 = new_uuid()
|
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
\t<ExternalDataProcessor uuid="{uuid1}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
|
|
||||||
\t\t\t</xr:ContainedObject>
|
|
||||||
\t\t\t<xr:GeneratedType name="ExternalDataProcessorObject.{name}" category="Object">
|
|
||||||
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
|
|
||||||
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
|
|
||||||
\t\t\t</xr:GeneratedType>
|
|
||||||
\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<DefaultForm/>
|
|
||||||
\t\t\t<AuxiliaryForm/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t<ChildObjects/>
|
|
||||||
\t</ExternalDataProcessor>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
root_file = os.path.join(src_dir, f"{name}.xml")
|
|
||||||
processor_dir = os.path.join(src_dir, name)
|
|
||||||
|
|
||||||
if os.path.exists(root_file):
|
|
||||||
print(f"Файл уже существует: {root_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
os.makedirs(src_dir, exist_ok=True)
|
|
||||||
ext_dir = os.path.join(processor_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
module_bsl = """\
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти"""
|
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
|
||||||
write_utf8_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
print(f"[OK] Создана обработка: {root_file}")
|
|
||||||
print(f" Каталог: {processor_dir}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: erf-init
|
|
||||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
|
|
||||||
argument-hint: <Name> [Synonym] [--with-skd]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /erf-init — Создание нового отчёта
|
|
||||||
|
|
||||||
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-----------|:------------:|--------------|---------------------------------------|
|
|
||||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
|
||||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дальнейшие шаги
|
|
||||||
|
|
||||||
- Добавить форму: `/form-add`
|
|
||||||
- Добавить макет: `/template-add`
|
|
||||||
- Добавить справку: `/help-add`
|
|
||||||
- Собрать ERF: `/erf-build`
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
# erf-init v1.1 — Init 1C external report scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Name,
|
|
||||||
|
|
||||||
[string]$Synonym = $Name,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src",
|
|
||||||
|
|
||||||
[switch]$WithSKD
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# --- Формируем Properties ---
|
|
||||||
|
|
||||||
$mainDCSValue = ""
|
|
||||||
$childObjectsContent = ""
|
|
||||||
|
|
||||||
if ($WithSKD) {
|
|
||||||
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
$childObjectsContent = @"
|
|
||||||
|
|
||||||
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
|
|
||||||
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainDCSElement = if ($mainDCSValue) {
|
|
||||||
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
|
|
||||||
} else {
|
|
||||||
"<MainDataCompositionSchema/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$childObjectsXml = if ($childObjectsContent) {
|
|
||||||
"<ChildObjects>$childObjectsContent</ChildObjects>"
|
|
||||||
} else {
|
|
||||||
"<ChildObjects/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$xml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<ExternalReport uuid="$uuid1">
|
|
||||||
<InternalInfo>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
|
||||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
|
|
||||||
<xr:TypeId>$uuid3</xr:TypeId>
|
|
||||||
<xr:ValueId>$uuid4</xr:ValueId>
|
|
||||||
</xr:GeneratedType>
|
|
||||||
</InternalInfo>
|
|
||||||
<Properties>
|
|
||||||
<Name>$Name</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<DefaultForm/>
|
|
||||||
<AuxiliaryForm/>
|
|
||||||
$mainDCSElement
|
|
||||||
<DefaultSettingsForm/>
|
|
||||||
<AuxiliarySettingsForm/>
|
|
||||||
<DefaultVariantForm/>
|
|
||||||
<VariantsStorage/>
|
|
||||||
<SettingsStorage/>
|
|
||||||
</Properties>
|
|
||||||
$childObjectsXml
|
|
||||||
</ExternalReport>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
|
||||||
$reportDir = Join-Path $SrcDir $Name
|
|
||||||
|
|
||||||
if (Test-Path $rootFile) {
|
|
||||||
Write-Error "Файл уже существует: $rootFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $SrcDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$extDir = Join-Path $reportDir "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
|
||||||
|
|
||||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
|
||||||
Write-Host " Каталог: $reportDir"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
|
|
||||||
# --- СКД-макет (если --WithSKD) ---
|
|
||||||
|
|
||||||
if ($WithSKD) {
|
|
||||||
$templatesDir = Join-Path $reportDir "Templates"
|
|
||||||
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
|
|
||||||
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
|
|
||||||
|
|
||||||
$skdUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$skdMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<Template uuid="$skdUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$skdName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>Основная схема компоновки данных</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<TemplateType>DataCompositionSchema</TemplateType>
|
|
||||||
</Properties>
|
|
||||||
</Template>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
|
||||||
|
|
||||||
$skdContent = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
||||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
|
||||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
|
||||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
||||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
|
||||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<dataSource>
|
|
||||||
<name>ИсточникДанных1</name>
|
|
||||||
<dataSourceType>Local</dataSourceType>
|
|
||||||
</dataSource>
|
|
||||||
</DataCompositionSchema>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
|
||||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
|
||||||
|
|
||||||
Write-Host " СКД: $skdMetaPath"
|
|
||||||
Write-Host " Тело: $skdFilePath"
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# erf-init v1.1 — Init 1C external report scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
"""Generates minimal XML source files for a 1C external report."""
|
|
||||||
import sys, os, argparse, uuid
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
def new_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
|
||||||
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
uuid1 = new_uuid()
|
|
||||||
uuid2 = new_uuid()
|
|
||||||
uuid3 = new_uuid()
|
|
||||||
uuid4 = new_uuid()
|
|
||||||
|
|
||||||
# --- Properties ---
|
|
||||||
main_dcs_value = ""
|
|
||||||
child_objects_content = ""
|
|
||||||
|
|
||||||
if args.WithSKD:
|
|
||||||
main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
child_objects_content = f"\n\t\t\t<Template>ОсновнаяСхемаКомпоновкиДанных</Template>\n"
|
|
||||||
|
|
||||||
main_dcs_element = f"<MainDataCompositionSchema>{main_dcs_value}</MainDataCompositionSchema>" if main_dcs_value else "<MainDataCompositionSchema/>"
|
|
||||||
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
\t<ExternalReport uuid="{uuid1}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
|
|
||||||
\t\t\t</xr:ContainedObject>
|
|
||||||
\t\t\t<xr:GeneratedType name="ExternalReportObject.{name}" category="Object">
|
|
||||||
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
|
|
||||||
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
|
|
||||||
\t\t\t</xr:GeneratedType>
|
|
||||||
\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<DefaultForm/>
|
|
||||||
\t\t\t<AuxiliaryForm/>
|
|
||||||
\t\t\t{main_dcs_element}
|
|
||||||
\t\t\t<DefaultSettingsForm/>
|
|
||||||
\t\t\t<AuxiliarySettingsForm/>
|
|
||||||
\t\t\t<DefaultVariantForm/>
|
|
||||||
\t\t\t<VariantsStorage/>
|
|
||||||
\t\t\t<SettingsStorage/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t{child_objects_xml}
|
|
||||||
\t</ExternalReport>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
root_file = os.path.join(src_dir, f"{name}.xml")
|
|
||||||
report_dir = os.path.join(src_dir, name)
|
|
||||||
|
|
||||||
if os.path.exists(root_file):
|
|
||||||
print(f"Файл уже существует: {root_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
os.makedirs(src_dir, exist_ok=True)
|
|
||||||
ext_dir = os.path.join(report_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
module_bsl = """\
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти"""
|
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
|
||||||
write_utf8_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
print(f"[OK] Создан отчёт: {root_file}")
|
|
||||||
print(f" Каталог: {report_dir}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
|
|
||||||
# --- СКД-макет ---
|
|
||||||
if args.WithSKD:
|
|
||||||
templates_dir = os.path.join(report_dir, "Templates")
|
|
||||||
skd_name = "ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml")
|
|
||||||
skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext")
|
|
||||||
os.makedirs(skd_ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
skd_uuid = new_uuid()
|
|
||||||
|
|
||||||
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
\t<Template uuid="{skd_uuid}">
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{skd_name}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>Основная схема компоновки данных</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<TemplateType>DataCompositionSchema</TemplateType>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t</Template>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
write_utf8_bom(skd_meta_path, skd_meta_xml)
|
|
||||||
|
|
||||||
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
||||||
\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
|
||||||
\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
|
||||||
\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
||||||
\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"
|
|
||||||
\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
|
||||||
\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
\t<dataSource>
|
|
||||||
\t\t<name>ИсточникДанных1</name>
|
|
||||||
\t\t<dataSourceType>Local</dataSourceType>
|
|
||||||
\t</dataSource>
|
|
||||||
</DataCompositionSchema>'''
|
|
||||||
|
|
||||||
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
|
||||||
write_utf8_bom(skd_file_path, skd_content)
|
|
||||||
|
|
||||||
print(f" СКД: {skd_meta_path}")
|
|
||||||
print(f" Тело: {skd_file_path}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
# form-add v1.5 — Add managed form to 1C config object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ObjectPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$Synonym = $FormName,
|
|
||||||
|
|
||||||
[string]$Purpose = "Object",
|
|
||||||
|
|
||||||
[switch]$SetDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Detect XML format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 1: Определение типа объекта ---
|
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) {
|
|
||||||
$ObjectPath = Join-Path (Get-Location).Path $ObjectPath
|
|
||||||
}
|
|
||||||
if (Test-Path $ObjectPath -PathType Container) {
|
|
||||||
$dirName = Split-Path $ObjectPath -Leaf
|
|
||||||
$candidate = Join-Path $ObjectPath "$dirName.xml"
|
|
||||||
$sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml"
|
|
||||||
if (Test-Path $candidate) { $ObjectPath = $candidate }
|
|
||||||
elseif (Test-Path $sibling) { $ObjectPath = $sibling }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $ObjectPath)) {
|
|
||||||
Write-Error "Файл объекта не найден: $ObjectPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
|
||||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($objectXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
|
|
||||||
# Определяем тип объекта по корневому тегу внутри MetaDataObject
|
|
||||||
$metaDataObject = $xmlDoc.SelectSingleNode("//md:MetaDataObject", $nsMgr)
|
|
||||||
if (-not $metaDataObject) {
|
|
||||||
# Пробуем без namespace (fallback)
|
|
||||||
$metaDataObject = $xmlDoc.DocumentElement
|
|
||||||
}
|
|
||||||
|
|
||||||
$supportedTypes = @(
|
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task"
|
|
||||||
)
|
|
||||||
|
|
||||||
$objectType = $null
|
|
||||||
$objectNode = $null
|
|
||||||
foreach ($t in $supportedTypes) {
|
|
||||||
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
|
|
||||||
if ($node) {
|
|
||||||
$objectType = $t
|
|
||||||
$objectNode = $node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $objectType) {
|
|
||||||
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Имя объекта из Properties/Name
|
|
||||||
$objectName = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:Name", $nsMgr).InnerText
|
|
||||||
if (-not $objectName) {
|
|
||||||
Write-Error "Не удалось определить имя объекта из Properties/Name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "=== form-add ==="
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Object: $objectType.$objectName"
|
|
||||||
|
|
||||||
# --- Фаза 2: Валидация Purpose ---
|
|
||||||
|
|
||||||
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
|
|
||||||
# Нормализация
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" { }
|
|
||||||
"List" { }
|
|
||||||
"Choice" { }
|
|
||||||
"Record" { }
|
|
||||||
default {
|
|
||||||
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
|
|
||||||
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
|
||||||
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" {
|
|
||||||
# допустимо для всех типов
|
|
||||||
}
|
|
||||||
"List" {
|
|
||||||
if ($objectType -eq "DataProcessor") {
|
|
||||||
Write-Error "Purpose=List недопустим для DataProcessor"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Choice" {
|
|
||||||
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Choice недопустим для $objectType"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Record" {
|
|
||||||
if ($objectType -ne "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Record допустим только для InformationRegister"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 3: Создание файлов ---
|
|
||||||
|
|
||||||
$objectDir = [System.IO.Path]::ChangeExtension($objectXmlFull.Path, $null).TrimEnd('.')
|
|
||||||
$formsDir = Join-Path $objectDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
|
|
||||||
if (Test-Path $formMetaPath) {
|
|
||||||
Write-Error "Форма уже существует: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
$formExtDir = Join-Path $formDir "Ext"
|
|
||||||
$formModuleDir = Join-Path $formExtDir "Form"
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# --- 3a. Метаданные формы ---
|
|
||||||
|
|
||||||
$formUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# ExtendedPresentation — only for DataProcessor, Report, ExternalDataProcessor, ExternalReport forms
|
|
||||||
$extPresentationLine = ""
|
|
||||||
if ($objectType -in $processorLikeTypes) {
|
|
||||||
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
|
||||||
<Form uuid="$formUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$FormName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<FormType>Managed</FormType>
|
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
<UsePurposes>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
|
||||||
</UsePurposes>$extPresentationLine
|
|
||||||
</Properties>
|
|
||||||
</Form>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
|
||||||
|
|
||||||
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
|
|
||||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|
||||||
# Динамический список
|
|
||||||
# MainTable: тип.имя
|
|
||||||
$mainTable = "$objectType.$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="Список" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:DynamicList</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<Settings xsi:type="DynamicList">
|
|
||||||
<MainTable>$mainTable</MainTable>
|
|
||||||
</Settings>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} elseif ($Purpose -eq "Record") {
|
|
||||||
# Запись регистра сведений
|
|
||||||
$mainAttrName = "Запись"
|
|
||||||
$mainAttrType = "InformationRegisterRecordManager.$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="$mainAttrName" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<SavedData>true</SavedData>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} else {
|
|
||||||
# Object — форма объекта
|
|
||||||
$mainAttrName = "Объект"
|
|
||||||
|
|
||||||
# Маппинг типа объекта на тип реквизита
|
|
||||||
$attrTypeMap = @{
|
|
||||||
"Document" = "DocumentObject"
|
|
||||||
"Catalog" = "CatalogObject"
|
|
||||||
"DataProcessor" = "DataProcessorObject"
|
|
||||||
"Report" = "ReportObject"
|
|
||||||
"ExternalDataProcessor" = "ExternalDataProcessorObject"
|
|
||||||
"ExternalReport" = "ExternalReportObject"
|
|
||||||
"ChartOfAccounts" = "ChartOfAccountsObject"
|
|
||||||
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
|
|
||||||
"ExchangePlan" = "ExchangePlanObject"
|
|
||||||
"BusinessProcess" = "BusinessProcessObject"
|
|
||||||
"Task" = "TaskObject"
|
|
||||||
"InformationRegister" = "InformationRegisterRecordManager"
|
|
||||||
"AccumulationRegister" = "AccumulationRegisterRecordSet"
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
$savedDataLine = ""
|
|
||||||
if ($objectType -notin $processorLikeTypes) {
|
|
||||||
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="$mainAttrName" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>$savedDataLine
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $formXmlPath) {
|
|
||||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
|
||||||
|
|
||||||
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОбработчикиСобытийФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиКомандФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиОповещений
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
if (Test-Path $modulePath) {
|
|
||||||
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 4: Регистрация в родительском объекте ---
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:${objectType}/md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $ObjectPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Form>$FormName</Form>
|
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$formElem.InnerText = $FormName
|
|
||||||
|
|
||||||
# Ищем первый <Template> для вставки перед ним
|
|
||||||
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
|
||||||
# Ищем первую <TabularSection> для вставки перед ней (если нет Template)
|
|
||||||
$firstTabular = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr)
|
|
||||||
|
|
||||||
# Определяем точку вставки: перед Template, перед TabularSection, или в конец
|
|
||||||
$insertBefore = $null
|
|
||||||
if ($firstTemplate) {
|
|
||||||
$insertBefore = $firstTemplate
|
|
||||||
} elseif ($firstTabular) {
|
|
||||||
$insertBefore = $firstTabular
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($insertBefore) {
|
|
||||||
# Вставить перед найденным элементом, с переносом строки
|
|
||||||
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
|
|
||||||
$childObjects.InsertBefore($whitespace, $formElem) | Out-Null
|
|
||||||
# Переставляем: whitespace перед formElem — неправильный порядок
|
|
||||||
# Правильно: formElem, затем whitespace перед insertBefore
|
|
||||||
# InsertBefore возвращает вставленный узел, порядок: ... formElem whitespace insertBefore ...
|
|
||||||
# На самом деле нам нужно: ... \n\t\t\tformElem \n\t\t\tinsertBefore
|
|
||||||
# Удалим и вставим правильно
|
|
||||||
$childObjects.RemoveChild($whitespace) | Out-Null
|
|
||||||
$childObjects.RemoveChild($formElem) | Out-Null
|
|
||||||
|
|
||||||
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
|
|
||||||
# Whitespace нужен ДО formElem (перенос строки + отступ)
|
|
||||||
# Но перед insertBefore уже должен быть whitespace от предыдущего элемента
|
|
||||||
# Нам нужно добавить whitespace ПОСЛЕ formElem (перед insertBefore)
|
|
||||||
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
|
|
||||||
} else {
|
|
||||||
# Добавить в конец ChildObjects
|
|
||||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
} else {
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- SetDefault ---
|
|
||||||
|
|
||||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
|
||||||
$isFirstFormForPurpose = $false
|
|
||||||
$defaultPropName = $null
|
|
||||||
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
|
||||||
|
|
||||||
# Определяем имя свойства для DefaultForm
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" {
|
|
||||||
if ($objectType -in $processorLikeTypes) {
|
|
||||||
$defaultPropName = "DefaultForm"
|
|
||||||
} else {
|
|
||||||
$defaultPropName = "DefaultObjectForm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"List" { $defaultPropName = "DefaultListForm" }
|
|
||||||
"Choice" { $defaultPropName = "DefaultChoiceForm" }
|
|
||||||
"Record" { $defaultPropName = "DefaultRecordForm" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем, установлено ли уже значение
|
|
||||||
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
|
||||||
if ($defaultNode) {
|
|
||||||
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultUpdated = $false
|
|
||||||
if ($SetDefault -or $isFirstFormForPurpose) {
|
|
||||||
if ($defaultNode) {
|
|
||||||
$defaultNode.InnerText = $defaultValue
|
|
||||||
$defaultUpdated = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
# --- Фаза 5: Вывод ---
|
|
||||||
|
|
||||||
# Относительные пути для вывода
|
|
||||||
$basePath = Split-Path $objectXmlFull.Path -Parent
|
|
||||||
# Определяем корень (ищем родительский каталог типа Documents, Catalogs и т.д.)
|
|
||||||
$relFormMeta = $formMetaPath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
$relFormXml = $formXmlPath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
$relModule = $modulePath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
|
|
||||||
$objFileName = [System.IO.Path]::GetFileName($ObjectPath)
|
|
||||||
$objDirName = Split-Path $ObjectPath -Parent
|
|
||||||
$objBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath)
|
|
||||||
|
|
||||||
Write-Host "Created:"
|
|
||||||
Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
|
|
||||||
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
|
||||||
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
|
||||||
if ($defaultUpdated) {
|
|
||||||
Write-Host "${defaultPropName}: $defaultValue"
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# form-add v1.5 — Add managed form to 1C config object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {
|
|
||||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def detect_format_version(d):
|
|
||||||
while d:
|
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
|
||||||
if os.path.isfile(cfg_path):
|
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
|
||||||
head = f.read(2000)
|
|
||||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return "2.17"
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add managed form to 1C config object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectPath", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-Purpose", default="Object")
|
|
||||||
parser.add_argument("-SetDefault", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
|
||||||
form_name = args.FormName
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else form_name
|
|
||||||
purpose = args.Purpose
|
|
||||||
set_default = args.SetDefault
|
|
||||||
|
|
||||||
# --- Phase 1: Determine object type ---
|
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
|
||||||
if not os.path.isabs(object_path):
|
|
||||||
object_path = os.path.join(os.getcwd(), object_path)
|
|
||||||
if os.path.isdir(object_path):
|
|
||||||
dir_name = os.path.basename(object_path.rstrip("/\\"))
|
|
||||||
candidate = os.path.join(object_path, dir_name + ".xml")
|
|
||||||
sibling = os.path.join(os.path.dirname(object_path.rstrip("/\\")), dir_name + ".xml")
|
|
||||||
if os.path.isfile(candidate):
|
|
||||||
object_path = candidate
|
|
||||||
elif os.path.isfile(sibling):
|
|
||||||
object_path = sibling
|
|
||||||
if not os.path.isfile(object_path):
|
|
||||||
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
|
||||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(object_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
supported_types = [
|
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task",
|
|
||||||
]
|
|
||||||
|
|
||||||
object_type = None
|
|
||||||
object_node = None
|
|
||||||
for t in supported_types:
|
|
||||||
node = root.find(f".//md:{t}", NSMAP)
|
|
||||||
if node is not None:
|
|
||||||
object_type = t
|
|
||||||
object_node = node
|
|
||||||
break
|
|
||||||
|
|
||||||
if object_type is None:
|
|
||||||
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Object name from Properties/Name
|
|
||||||
name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP)
|
|
||||||
if name_node is None or not name_node.text:
|
|
||||||
print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
object_name = name_node.text
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=== form-add ===")
|
|
||||||
print()
|
|
||||||
print(f"Object: {object_type}.{object_name}")
|
|
||||||
|
|
||||||
# --- Phase 2: Validate Purpose ---
|
|
||||||
|
|
||||||
# Normalize: capitalize first letter, lowercase rest
|
|
||||||
purpose = purpose[0].upper() + purpose[1:].lower()
|
|
||||||
|
|
||||||
valid_purposes = ["Object", "List", "Choice", "Record"]
|
|
||||||
if purpose not in valid_purposes:
|
|
||||||
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task"]
|
|
||||||
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
|
||||||
|
|
||||||
if purpose == "List":
|
|
||||||
if object_type == "DataProcessor":
|
|
||||||
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Choice":
|
|
||||||
if object_type in processor_like_types or object_type == "InformationRegister":
|
|
||||||
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
|
||||||
if object_type != "InformationRegister":
|
|
||||||
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Phase 3: Create files ---
|
|
||||||
|
|
||||||
object_dir = os.path.splitext(object_xml_full)[0]
|
|
||||||
forms_dir = os.path.join(object_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
|
|
||||||
if os.path.exists(form_meta_path):
|
|
||||||
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
form_ext_dir = os.path.join(form_dir, "Ext")
|
|
||||||
form_module_dir = os.path.join(form_ext_dir, "Form")
|
|
||||||
|
|
||||||
os.makedirs(form_module_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 3a. Form metadata ---
|
|
||||||
|
|
||||||
form_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
form_meta_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
f' version="{format_version}">\n'
|
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
|
||||||
'\t\t<Properties>\n'
|
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
|
||||||
'\t\t\t<Synonym>\n'
|
|
||||||
'\t\t\t\t<v8:item>\n'
|
|
||||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
|
||||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
|
||||||
'\t\t\t\t</v8:item>\n'
|
|
||||||
'\t\t\t</Synonym>\n'
|
|
||||||
'\t\t\t<Comment/>\n'
|
|
||||||
'\t\t\t<FormType>Managed</FormType>\n'
|
|
||||||
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
|
|
||||||
'\t\t\t<UsePurposes>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t</UsePurposes>\n'
|
|
||||||
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
|
||||||
+ '\t\t</Properties>\n'
|
|
||||||
'\t</Form>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
|
||||||
|
|
||||||
form_ns_decl = (
|
|
||||||
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
||||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if purpose in ("List", "Choice"):
|
|
||||||
# Dynamic list
|
|
||||||
main_table = f"{object_type}.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<Settings xsi:type="DynamicList">\n'
|
|
||||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
|
||||||
'\t\t\t</Settings>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
|
||||||
# Information register record
|
|
||||||
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
|
|
||||||
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Object — object form
|
|
||||||
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
|
|
||||||
|
|
||||||
attr_type_map = {
|
|
||||||
"Document": "DocumentObject",
|
|
||||||
"Catalog": "CatalogObject",
|
|
||||||
"DataProcessor": "DataProcessorObject",
|
|
||||||
"Report": "ReportObject",
|
|
||||||
"ExternalDataProcessor": "ExternalDataProcessorObject",
|
|
||||||
"ExternalReport": "ExternalReportObject",
|
|
||||||
"ChartOfAccounts": "ChartOfAccountsObject",
|
|
||||||
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
|
|
||||||
"ExchangePlan": "ExchangePlanObject",
|
|
||||||
"BusinessProcess": "BusinessProcessObject",
|
|
||||||
"Task": "TaskObject",
|
|
||||||
"InformationRegister": "InformationRegisterRecordManager",
|
|
||||||
"AccumulationRegister": "AccumulationRegisterRecordSet",
|
|
||||||
}
|
|
||||||
|
|
||||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
saved_data_line = ''
|
|
||||||
if object_type not in processor_like_types:
|
|
||||||
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
f'{saved_data_line}'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(form_xml_path):
|
|
||||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
|
||||||
else:
|
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
|
||||||
|
|
||||||
module_path = os.path.join(form_module_dir, "Module.bsl")
|
|
||||||
|
|
||||||
module_bsl = (
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(module_path):
|
|
||||||
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
|
||||||
else:
|
|
||||||
write_text_with_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
# --- Phase 4: Register in parent object ---
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Form>$FormName</Form>
|
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
# Find first <Template> to insert before it
|
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
|
||||||
# Find first <TabularSection> to insert before it (if no Template)
|
|
||||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
|
||||||
|
|
||||||
# Determine insertion point: before Template, before TabularSection, or at end
|
|
||||||
insert_before = None
|
|
||||||
if first_template is not None:
|
|
||||||
insert_before = first_template
|
|
||||||
elif first_tabular is not None:
|
|
||||||
insert_before = first_tabular
|
|
||||||
|
|
||||||
if insert_before is not None:
|
|
||||||
# Insert before the found element
|
|
||||||
idx = list(child_objects).index(insert_before)
|
|
||||||
child_objects.insert(idx, form_elem)
|
|
||||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
|
||||||
form_elem.tail = "\n\t\t\t"
|
|
||||||
else:
|
|
||||||
# Add to end of ChildObjects
|
|
||||||
children = list(child_objects)
|
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
|
||||||
# Empty ChildObjects (self-closing)
|
|
||||||
child_objects.text = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
else:
|
|
||||||
if len(children) > 0:
|
|
||||||
last_child = children[-1]
|
|
||||||
old_tail = last_child.tail
|
|
||||||
last_child.tail = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
|
|
||||||
# --- SetDefault ---
|
|
||||||
|
|
||||||
is_first_form_for_purpose = False
|
|
||||||
default_prop_name = None
|
|
||||||
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
|
||||||
|
|
||||||
# Determine property name for DefaultForm
|
|
||||||
if purpose == "Object":
|
|
||||||
if object_type in processor_like_types:
|
|
||||||
default_prop_name = "DefaultForm"
|
|
||||||
else:
|
|
||||||
default_prop_name = "DefaultObjectForm"
|
|
||||||
elif purpose == "List":
|
|
||||||
default_prop_name = "DefaultListForm"
|
|
||||||
elif purpose == "Choice":
|
|
||||||
default_prop_name = "DefaultChoiceForm"
|
|
||||||
elif purpose == "Record":
|
|
||||||
default_prop_name = "DefaultRecordForm"
|
|
||||||
|
|
||||||
# Check if value is already set
|
|
||||||
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
|
||||||
if default_node is not None:
|
|
||||||
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
|
|
||||||
|
|
||||||
default_updated = False
|
|
||||||
if set_default or is_first_form_for_purpose:
|
|
||||||
if default_node is not None:
|
|
||||||
default_node.text = default_value
|
|
||||||
default_updated = True
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, object_xml_full)
|
|
||||||
|
|
||||||
# --- Phase 5: Output ---
|
|
||||||
|
|
||||||
obj_dir_name = os.path.dirname(object_path)
|
|
||||||
obj_base_name = os.path.splitext(os.path.basename(object_path))[0]
|
|
||||||
|
|
||||||
print("Created:")
|
|
||||||
print(f" Metadata: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}.xml")
|
|
||||||
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
|
||||||
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
|
||||||
print()
|
|
||||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
|
||||||
if default_updated:
|
|
||||||
print(f"{default_prop_name}: {default_value}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# form-remove v1.2 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias("ProcessorName")]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$formsDir = Join-Path $processorDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
|
|
||||||
if (-not (Test-Path $formMetaPath)) {
|
|
||||||
Write-Error "Метаданные формы не найдены: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Удаление файлов ---
|
|
||||||
|
|
||||||
if (Test-Path $formDir) {
|
|
||||||
Remove-Item -Path $formDir -Recurse -Force
|
|
||||||
Write-Host "[OK] Удалён каталог: $formDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
Remove-Item -Path $formMetaPath -Force
|
|
||||||
Write-Host "[OK] Удалён файл: $formMetaPath"
|
|
||||||
|
|
||||||
# --- Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
# Удалить <Form>FormName</Form> из ChildObjects
|
|
||||||
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
|
|
||||||
foreach ($node in $formNodes) {
|
|
||||||
if ($node.InnerText -eq $FormName) {
|
|
||||||
$parent = $node.ParentNode
|
|
||||||
# Удалить предшествующий whitespace
|
|
||||||
$prev = $node.PreviousSibling
|
|
||||||
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$parent.RemoveChild($prev) | Out-Null
|
|
||||||
}
|
|
||||||
$parent.RemoveChild($node) | Out-Null
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Очистить DefaultForm если указывала на эту форму
|
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
|
||||||
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") {
|
|
||||||
$defaultForm.InnerText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# remove-form v1.1 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
form_name = args.FormName
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, object_name)
|
|
||||||
forms_dir = os.path.join(processor_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
|
|
||||||
if not os.path.exists(form_meta_path):
|
|
||||||
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Delete files ---
|
|
||||||
|
|
||||||
if os.path.isdir(form_dir):
|
|
||||||
shutil.rmtree(form_dir)
|
|
||||||
print(f"[OK] Удалён каталог: {form_dir}")
|
|
||||||
|
|
||||||
os.remove(form_meta_path)
|
|
||||||
print(f"[OK] Удалён файл: {form_meta_path}")
|
|
||||||
|
|
||||||
# --- Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Remove <Form>FormName</Form> from ChildObjects
|
|
||||||
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
|
|
||||||
if node.text and node.text.strip() == form_name:
|
|
||||||
parent = node.getparent()
|
|
||||||
prev = node.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Whitespace is in prev.tail
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = ""
|
|
||||||
else:
|
|
||||||
# First child — whitespace is in parent.text
|
|
||||||
if parent.text and parent.text.strip() == "":
|
|
||||||
parent.text = ""
|
|
||||||
parent.remove(node)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Clear DefaultForm if it pointed to removed form
|
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
|
||||||
if default_form is not None and default_form.text:
|
|
||||||
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
|
|
||||||
default_form.text = ""
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# help-add v1.4 — Add built-in help to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[string]$Lang = "ru",
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$objectDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$extDir = Join-Path $objectDir "Ext"
|
|
||||||
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$helpXmlPath = Join-Path $extDir "Help.xml"
|
|
||||||
if (Test-Path $helpXmlPath) {
|
|
||||||
Write-Error "Справка уже существует: $helpXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# --- 1. Help.xml ---
|
|
||||||
|
|
||||||
$helpXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
|
||||||
<Page>$Lang</Page>
|
|
||||||
</Help>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
|
||||||
|
|
||||||
$helpDir = Join-Path $extDir "Help"
|
|
||||||
New-Item -ItemType Directory -Path $helpDir -Force | Out-Null
|
|
||||||
|
|
||||||
$helpHtmlPath = Join-Path $helpDir "$Lang.html"
|
|
||||||
|
|
||||||
$helpHtml = @"
|
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>$ObjectName</h1>
|
|
||||||
<p>Описание.</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom)
|
|
||||||
|
|
||||||
# --- 3. Проверка IncludeHelpInContents в метаданных форм ---
|
|
||||||
|
|
||||||
$formsDir = Join-Path $objectDir "Forms"
|
|
||||||
if (Test-Path $formsDir) {
|
|
||||||
$formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File
|
|
||||||
foreach ($formMeta in $formMetaFiles) {
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($formMeta.FullName)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr)
|
|
||||||
if (-not $includeHelp) {
|
|
||||||
# Добавить после <FormType>
|
|
||||||
$formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr)
|
|
||||||
if ($formType) {
|
|
||||||
$newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$newElem.InnerText = "false"
|
|
||||||
$parent = $formType.ParentNode
|
|
||||||
$nextSibling = $formType.NextSibling
|
|
||||||
# Вставить перенос + табуляцию + элемент
|
|
||||||
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
if ($nextSibling) {
|
|
||||||
$parent.InsertBefore($ws, $nextSibling) | Out-Null
|
|
||||||
$parent.InsertBefore($newElem, $ws) | Out-Null
|
|
||||||
} else {
|
|
||||||
$parent.AppendChild($ws) | Out-Null
|
|
||||||
$parent.AppendChild($newElem) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана справка: $ObjectName"
|
|
||||||
Write-Host " Метаданные: $helpXmlPath"
|
|
||||||
Write-Host " Страница: $helpHtmlPath"
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-help v1.4 — Add built-in help to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def detect_format_version(d):
|
|
||||||
while d:
|
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
|
||||||
if os.path.isfile(cfg_path):
|
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
|
||||||
head = f.read(2000)
|
|
||||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return "2.17"
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", required=True)
|
|
||||||
parser.add_argument("-Lang", default="ru")
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
lang = args.Lang
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
format_version = detect_format_version(os.path.abspath(src_dir))
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
object_dir = os.path.join(src_dir, object_name)
|
|
||||||
ext_dir = os.path.join(object_dir, "Ext")
|
|
||||||
|
|
||||||
if not os.path.isdir(ext_dir):
|
|
||||||
print(f"Каталог объекта не найден: {ext_dir}. Проверьте путь ObjectName (например Catalogs/МойСправочник).", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
help_xml_path = os.path.join(ext_dir, "Help.xml")
|
|
||||||
if os.path.exists(help_xml_path):
|
|
||||||
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- 1. Help.xml ---
|
|
||||||
|
|
||||||
help_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
f' version="{format_version}">\n'
|
|
||||||
f'\t<Page>{lang}</Page>\n'
|
|
||||||
'</Help>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(help_xml_path, help_xml)
|
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
|
||||||
|
|
||||||
help_dir = os.path.join(ext_dir, "Help")
|
|
||||||
os.makedirs(help_dir, exist_ok=True)
|
|
||||||
|
|
||||||
help_html_path = os.path.join(help_dir, f"{lang}.html")
|
|
||||||
|
|
||||||
help_html = (
|
|
||||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
|
|
||||||
'<html>\n'
|
|
||||||
'<head>\n'
|
|
||||||
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
|
|
||||||
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
|
|
||||||
'</head>\n'
|
|
||||||
'<body>\n'
|
|
||||||
f' <h1>{object_name}</h1>\n'
|
|
||||||
' <p>Описание.</p>\n'
|
|
||||||
'</body>\n'
|
|
||||||
'</html>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(help_html_path, help_html)
|
|
||||||
|
|
||||||
# --- 3. Check IncludeHelpInContents in form metadata ---
|
|
||||||
|
|
||||||
forms_dir = os.path.join(object_dir, "Forms")
|
|
||||||
if os.path.isdir(forms_dir):
|
|
||||||
for entry in os.listdir(forms_dir):
|
|
||||||
if not entry.endswith(".xml"):
|
|
||||||
continue
|
|
||||||
form_meta_full = os.path.join(forms_dir, entry)
|
|
||||||
if not os.path.isfile(form_meta_full):
|
|
||||||
continue
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
form_tree = etree.parse(form_meta_full, parser_xml)
|
|
||||||
form_root = form_tree.getroot()
|
|
||||||
|
|
||||||
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
|
|
||||||
if include_help is not None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add after <FormType>
|
|
||||||
form_type = form_root.find(".//md:FormType", NSMAP)
|
|
||||||
if form_type is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parent = form_type.getparent()
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
|
|
||||||
new_elem.text = "false"
|
|
||||||
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
|
|
||||||
parent.remove(new_elem)
|
|
||||||
|
|
||||||
# Find index of FormType in parent
|
|
||||||
form_type_idx = list(parent).index(form_type)
|
|
||||||
|
|
||||||
# Insert after FormType
|
|
||||||
parent.insert(form_type_idx + 1, new_elem)
|
|
||||||
|
|
||||||
# Whitespace handling: copy FormType's tail as new_elem's tail,
|
|
||||||
# and set FormType's tail to include newline + indent
|
|
||||||
new_elem.tail = form_type.tail
|
|
||||||
form_type.tail = "\n\t\t\t"
|
|
||||||
|
|
||||||
save_xml_with_bom(form_tree, form_meta_full)
|
|
||||||
|
|
||||||
print(f" IncludeHelpInContents добавлен: {entry}")
|
|
||||||
|
|
||||||
print(f"[OK] Создана справка: {object_name}")
|
|
||||||
print(f" Метаданные: {help_xml_path}")
|
|
||||||
print(f" Страница: {help_html_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
---
|
|
||||||
name: meta-compile
|
|
||||||
description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
|
|
||||||
argument-hint: <JsonPath> <OutputDir>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /meta-compile — генерация объектов метаданных из JSON DSL
|
|
||||||
|
|
||||||
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
|
|
||||||
|
|
||||||
## Порядок работы
|
|
||||||
|
|
||||||
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
|
|
||||||
2. Запусти скрипт meta-compile
|
|
||||||
3. Если нужно изменить созданный объект — `/meta-edit`
|
|
||||||
4. Если нужно проверить — `/meta-validate`
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
|
|
||||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
|
|
||||||
|
|
||||||
## JSON DSL
|
|
||||||
|
|
||||||
### Общая структура
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
|
|
||||||
```
|
|
||||||
|
|
||||||
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
|
|
||||||
|
|
||||||
### Shorthand реквизитов
|
|
||||||
|
|
||||||
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
|
|
||||||
|
|
||||||
```
|
|
||||||
"ИмяРеквизита" → String(10) по умолчанию
|
|
||||||
"ИмяРеквизита: Тип" → с типом
|
|
||||||
"ИмяРеквизита: Тип | req, index" → с флагами
|
|
||||||
```
|
|
||||||
|
|
||||||
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
|
|
||||||
|
|
||||||
Составной тип: `"Значение: 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": "Catalog", "name": "Организации",
|
|
||||||
"descriptionLength": 100,
|
|
||||||
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### С табличной частью
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Регистровый паттерн (измерения + ресурсы)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Batch — несколько объектов в одном файле
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
|
||||||
{ "type": "Catalog", "name": "Валюты" },
|
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
|
|
||||||
## Catalog
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
|
|
||||||
| `limitLevelCount` | `false` | LimitLevelCount |
|
|
||||||
| `levelCount` | `2` | LevelCount |
|
|
||||||
| `foldersOnTop` | `true` | FoldersOnTop |
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `codeType` | `String` | CodeType |
|
|
||||||
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
|
|
||||||
| `codeSeries` | `WholeCatalog` | CodeSeries |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
|
|
||||||
| `subordinationUse` | `ToItems` | SubordinationUse |
|
|
||||||
| `quickChoice` | `false` | QuickChoice |
|
|
||||||
| `choiceMode` | `BothWays` | ChoiceMode |
|
|
||||||
| `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)"] }
|
|
||||||
```
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Процессы и сервисные: 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` (задача должна существовать)
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
# Регистры и планы: 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` | `DontUse` | DependenceOnCalculationTypes |
|
|
||||||
| `actionPeriodUse` | `false` | ActionPeriodUse |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
|
|
||||||
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
|
|
||||||
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Веб-сервисы: 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
# Свойства объекта и complex properties
|
|
||||||
|
|
||||||
Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString).
|
|
||||||
|
|
||||||
## modify-property
|
|
||||||
|
|
||||||
Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`):
|
|
||||||
```powershell
|
|
||||||
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
|
|
||||||
-Operation modify-property -Value "Hierarchical=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complex properties
|
|
||||||
|
|
||||||
Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
|
||||||
|
|
||||||
| Свойство | Объекты | Inline-значение |
|
|
||||||
|----------|---------|-----------------|
|
|
||||||
| Owners | Catalog, ChartOfCharacteristicTypes | `Catalog.XXX` |
|
|
||||||
| RegisterRecords | Document | `AccumulationRegister.XXX` |
|
|
||||||
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
|
|
||||||
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
|
|
||||||
|
|
||||||
### add-owner / add-registerRecord / add-basedOn
|
|
||||||
|
|
||||||
Полное имя метаданных `MetaType.Name`:
|
|
||||||
```powershell
|
|
||||||
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
|
||||||
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
|
|
||||||
-Operation add-basedOn -Value "Document.ЗаказКлиента"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-inputByString
|
|
||||||
|
|
||||||
Пути полей (префикс `MetaType.Name.` добавляется автоматически):
|
|
||||||
```powershell
|
|
||||||
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
|
|
||||||
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
-Operation remove-owner -Value "Catalog.Контрагенты"
|
|
||||||
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
|
|
||||||
### set-owners / set-registerRecords / set-basedOn / set-inputByString
|
|
||||||
|
|
||||||
Заменяют **весь список** (в отличие от add/remove):
|
|
||||||
```powershell
|
|
||||||
-Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты"
|
|
||||||
-Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров"
|
|
||||||
-Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
name: mxl-compile
|
|
||||||
description: Компиляция табличного документа (MXL) из JSON-определения. Используй когда нужно создать макет печатной формы
|
|
||||||
argument-hint: <JsonPath> <OutputPath>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /mxl-compile — Компилятор макета из DSL
|
|
||||||
|
|
||||||
Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace).
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/mxl-compile <JsonPath> <OutputPath>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|------------|:------------:|------------------------------------|
|
|
||||||
| JsonPath | да | Путь к JSON-определению макета |
|
|
||||||
| OutputPath | да | Путь для генерации Template.xml |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рабочий процесс
|
|
||||||
|
|
||||||
1. Claude пишет JSON-определение (Write tool) → файл `.json`
|
|
||||||
2. Claude вызывает `/mxl-compile` для генерации Template.xml
|
|
||||||
3. Claude вызывает `/mxl-validate` для проверки корректности
|
|
||||||
4. Claude вызывает `/mxl-info` для верификации структуры
|
|
||||||
|
|
||||||
**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров.
|
|
||||||
|
|
||||||
## JSON-схема DSL
|
|
||||||
|
|
||||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON).
|
|
||||||
|
|
||||||
Краткая структура:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ columns, page, defaultWidth, columnWidths,
|
|
||||||
fonts: { name: { face, size, bold, italic, underline, strikeout } },
|
|
||||||
styles: { name: { font, align, valign, border, borderWidth, wrap, format } },
|
|
||||||
areas: [{ name, rows: [{ height, rowStyle, cells: [
|
|
||||||
{ col, span, rowspan, style, param, detail, text, template }
|
|
||||||
]}]}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключевые правила:
|
|
||||||
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
|
|
||||||
- `col` — 1-based позиция колонки
|
|
||||||
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
|
|
||||||
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
|
|
||||||
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$JsonPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$OutputPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $JsonPath)) {
|
|
||||||
Write-Error "File not found: $JsonPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
|
||||||
$def = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $def.columns) {
|
|
||||||
Write-Error "Required field 'columns' is missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if (-not $def.areas) {
|
|
||||||
Write-Error "Required field 'areas' is missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalColumns = [int]$def.columns
|
|
||||||
$defaultWidth = if ($def.defaultWidth) { [int]$def.defaultWidth } else { 10 }
|
|
||||||
|
|
||||||
# --- 2. Build font palette ---
|
|
||||||
|
|
||||||
$fontMap = [ordered]@{} # name -> 0-based index
|
|
||||||
$fontEntries = @() # array of hashtables
|
|
||||||
|
|
||||||
function Add-Font {
|
|
||||||
param([string]$name, $fontDef)
|
|
||||||
$face = if ($fontDef.face) { $fontDef.face } else { "Arial" }
|
|
||||||
$size = if ($fontDef.size) { [int]$fontDef.size } else { 10 }
|
|
||||||
$bold = if ($fontDef.bold -eq $true) { "true" } else { "false" }
|
|
||||||
$italic = if ($fontDef.italic -eq $true) { "true" } else { "false" }
|
|
||||||
$underline = if ($fontDef.underline -eq $true) { "true" } else { "false" }
|
|
||||||
$strikeout = if ($fontDef.strikeout -eq $true) { "true" } else { "false" }
|
|
||||||
|
|
||||||
$idx = $script:fontEntries.Count
|
|
||||||
$script:fontMap[$name] = $idx
|
|
||||||
$script:fontEntries += @{
|
|
||||||
Face = $face
|
|
||||||
Size = $size
|
|
||||||
Bold = $bold
|
|
||||||
Italic = $italic
|
|
||||||
Underline = $underline
|
|
||||||
Strikeout = $strikeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add user-defined fonts
|
|
||||||
$hasDefault = $false
|
|
||||||
if ($def.fonts) {
|
|
||||||
foreach ($prop in $def.fonts.PSObject.Properties) {
|
|
||||||
if ($prop.Name -eq "default") { $hasDefault = $true }
|
|
||||||
Add-Font -name $prop.Name -fontDef $prop.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure default font exists
|
|
||||||
if (-not $hasDefault) {
|
|
||||||
$defaultDef = New-Object PSObject -Property @{ face = "Arial"; size = 10 }
|
|
||||||
Add-Font -name "default" -fontDef $defaultDef
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Determine line palette ---
|
|
||||||
|
|
||||||
$hasThinBorders = $false
|
|
||||||
$hasThickBorders = $false
|
|
||||||
|
|
||||||
# Scan styles for border usage
|
|
||||||
if ($def.styles) {
|
|
||||||
foreach ($prop in $def.styles.PSObject.Properties) {
|
|
||||||
$s = $prop.Value
|
|
||||||
if ($s.border -and $s.border -ne "none") {
|
|
||||||
if ($s.borderWidth -eq "thick") {
|
|
||||||
$hasThickBorders = $true
|
|
||||||
} else {
|
|
||||||
$hasThinBorders = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$thinLineIndex = -1
|
|
||||||
$thickLineIndex = -1
|
|
||||||
$lineCount = 0
|
|
||||||
if ($hasThinBorders) {
|
|
||||||
$thinLineIndex = $lineCount; $lineCount++
|
|
||||||
}
|
|
||||||
if ($hasThickBorders) {
|
|
||||||
$thickLineIndex = $lineCount; $lineCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Parse column width specs ---
|
|
||||||
|
|
||||||
function Parse-ColumnSpec {
|
|
||||||
param([string]$spec)
|
|
||||||
$cols = @()
|
|
||||||
foreach ($part in $spec -split ',') {
|
|
||||||
$part = $part.Trim()
|
|
||||||
if ($part -match '^(\d+)-(\d+)$') {
|
|
||||||
$from = [int]$Matches[1]
|
|
||||||
$to = [int]$Matches[2]
|
|
||||||
for ($i = $from; $i -le $to; $i++) { $cols += $i }
|
|
||||||
} else {
|
|
||||||
$cols += [int]$part
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $cols
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4a. Auto-calculate defaultWidth from page format ---
|
|
||||||
|
|
||||||
$pageTargets = @{
|
|
||||||
"A4-landscape" = 780
|
|
||||||
"A4-portrait" = 540
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($def.page) {
|
|
||||||
$pageName = "$($def.page)"
|
|
||||||
$targetWidth = $null
|
|
||||||
|
|
||||||
if ($pageName -match '^\d+$') {
|
|
||||||
$targetWidth = [int]$pageName
|
|
||||||
} elseif ($pageTargets.ContainsKey($pageName)) {
|
|
||||||
$targetWidth = $pageTargets[$pageName]
|
|
||||||
} else {
|
|
||||||
Write-Warning "Unknown page format '$pageName'. Known: $($pageTargets.Keys -join ', '), or a number."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($targetWidth) {
|
|
||||||
$totalUnits = 0.0
|
|
||||||
$absoluteSum = 0
|
|
||||||
$specifiedCols = @{}
|
|
||||||
|
|
||||||
if ($def.columnWidths) {
|
|
||||||
foreach ($prop in $def.columnWidths.PSObject.Properties) {
|
|
||||||
$val = "$($prop.Value)"
|
|
||||||
$cols = Parse-ColumnSpec $prop.Name
|
|
||||||
foreach ($c in $cols) {
|
|
||||||
$specifiedCols[[int]$c] = $true
|
|
||||||
if ($val -match '^([0-9.]+)x$') {
|
|
||||||
$totalUnits += [double]$Matches[1]
|
|
||||||
} else {
|
|
||||||
$absoluteSum += [int]$val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if (-not $specifiedCols.ContainsKey($c)) {
|
|
||||||
$totalUnits += 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($totalUnits -gt 0) {
|
|
||||||
$defaultWidth = [int][math]::Round(($targetWidth - $absoluteSum) / $totalUnits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build column width map: 1-based col -> width
|
|
||||||
$colWidthMap = @{}
|
|
||||||
if ($def.columnWidths) {
|
|
||||||
foreach ($prop in $def.columnWidths.PSObject.Properties) {
|
|
||||||
$val = "$($prop.Value)"
|
|
||||||
if ($val -match '^([0-9.]+)x$') {
|
|
||||||
$width = [int][math]::Round([double]$Matches[1] * $defaultWidth)
|
|
||||||
} else {
|
|
||||||
$width = [int]$val
|
|
||||||
}
|
|
||||||
$columns = Parse-ColumnSpec $prop.Name
|
|
||||||
foreach ($c in $columns) {
|
|
||||||
$colWidthMap[$c] = $width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Style resolver ---
|
|
||||||
|
|
||||||
function Resolve-Style {
|
|
||||||
param([string]$styleName, [string]$fillType)
|
|
||||||
|
|
||||||
$fontIdx = $fontMap["default"]
|
|
||||||
$lb = -1; $tb = -1; $rb = -1; $bb = -1
|
|
||||||
$ha = ""; $va = ""; $nf = ""
|
|
||||||
$wrap = $false
|
|
||||||
|
|
||||||
if ($styleName -and $def.styles) {
|
|
||||||
$style = $def.styles.$styleName
|
|
||||||
if ($style) {
|
|
||||||
# Font
|
|
||||||
if ($style.font -and $fontMap.Contains($style.font)) {
|
|
||||||
$fontIdx = $fontMap[$style.font]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Borders
|
|
||||||
if ($style.border -and $style.border -ne "none") {
|
|
||||||
$lineIdx = if ($style.borderWidth -eq "thick") { $thickLineIndex } else { $thinLineIndex }
|
|
||||||
foreach ($side in ($style.border -split ',')) {
|
|
||||||
switch ($side.Trim()) {
|
|
||||||
"all" { $lb = $lineIdx; $tb = $lineIdx; $rb = $lineIdx; $bb = $lineIdx }
|
|
||||||
"left" { $lb = $lineIdx }
|
|
||||||
"top" { $tb = $lineIdx }
|
|
||||||
"right" { $rb = $lineIdx }
|
|
||||||
"bottom" { $bb = $lineIdx }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alignment
|
|
||||||
if ($style.align) {
|
|
||||||
switch ($style.align) {
|
|
||||||
"left" { $ha = "Left" }
|
|
||||||
"center" { $ha = "Center" }
|
|
||||||
"right" { $ha = "Right" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($style.valign) {
|
|
||||||
switch ($style.valign) {
|
|
||||||
"top" { $va = "Top" }
|
|
||||||
"center" { $va = "Center" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wrap
|
|
||||||
if ($style.wrap -eq $true) { $wrap = $true }
|
|
||||||
|
|
||||||
# Number format
|
|
||||||
if ($style.format) { $nf = $style.format }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @{
|
|
||||||
FontIdx = $fontIdx
|
|
||||||
LB = $lb; TB = $tb; RB = $rb; BB = $bb
|
|
||||||
HA = $ha; VA = $va
|
|
||||||
Wrap = $wrap
|
|
||||||
FillType = $fillType
|
|
||||||
NumberFormat = $nf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Format palette builder ---
|
|
||||||
|
|
||||||
$formatRegistry = [ordered]@{} # key -> hashtable with properties
|
|
||||||
$formatOrder = @() # ordered keys for index assignment
|
|
||||||
|
|
||||||
function Get-FormatKey {
|
|
||||||
param(
|
|
||||||
[int]$fontIdx = -1,
|
|
||||||
[int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1,
|
|
||||||
[string]$ha = "", [string]$va = "",
|
|
||||||
[bool]$wrap = $false,
|
|
||||||
[string]$fillType = "",
|
|
||||||
[string]$numberFormat = "",
|
|
||||||
[int]$width = -1,
|
|
||||||
[int]$height = -1
|
|
||||||
)
|
|
||||||
return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|nf=$numberFormat|w=$width|h=$height"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Register-Format {
|
|
||||||
param([string]$key, [hashtable]$props)
|
|
||||||
if (-not $script:formatRegistry.Contains($key)) {
|
|
||||||
$script:formatRegistry[$key] = $props
|
|
||||||
$script:formatOrder += $key
|
|
||||||
}
|
|
||||||
# Return 1-based index
|
|
||||||
$idx = 0
|
|
||||||
foreach ($k in $script:formatRegistry.Keys) {
|
|
||||||
$idx++
|
|
||||||
if ($k -eq $key) { return $idx }
|
|
||||||
}
|
|
||||||
return $idx
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6a. Default width format
|
|
||||||
$defaultFormatKey = Get-FormatKey -width $defaultWidth
|
|
||||||
$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth }
|
|
||||||
|
|
||||||
# 6b. Column width formats
|
|
||||||
$colFormatMap = @{} # 1-based col -> format index
|
|
||||||
foreach ($col in ($colWidthMap.Keys | Sort-Object)) {
|
|
||||||
$w = $colWidthMap[$col]
|
|
||||||
$key = Get-FormatKey -width $w
|
|
||||||
$idx = Register-Format -key $key -props @{ Width = $w }
|
|
||||||
$colFormatMap[[int]$col] = $idx
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6c. Scan areas for row heights and cell formats
|
|
||||||
# We need to do two passes: first collect all formats, then generate XML
|
|
||||||
|
|
||||||
# Helper: escape XML special characters
|
|
||||||
function Esc-Xml {
|
|
||||||
param([string]$s)
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: determine fillType from cell content
|
|
||||||
function Get-FillType {
|
|
||||||
param($cell)
|
|
||||||
if ($cell.param) { return "Parameter" }
|
|
||||||
if ($cell.template) { return "Template" }
|
|
||||||
if ($cell.text) { return "Text" }
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: register a cell format and return its index
|
|
||||||
function Register-CellFormat {
|
|
||||||
param($styleName, [string]$fillType)
|
|
||||||
$resolved = Resolve-Style -styleName $styleName -fillType $fillType
|
|
||||||
$key = Get-FormatKey -fontIdx $resolved.FontIdx `
|
|
||||||
-lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB `
|
|
||||||
-ha $resolved.HA -va $resolved.VA `
|
|
||||||
-wrap $resolved.Wrap -fillType $resolved.FillType `
|
|
||||||
-numberFormat $resolved.NumberFormat
|
|
||||||
$props = @{
|
|
||||||
FontIdx = $resolved.FontIdx
|
|
||||||
LB = $resolved.LB; TB = $resolved.TB
|
|
||||||
RB = $resolved.RB; BB = $resolved.BB
|
|
||||||
HA = $resolved.HA; VA = $resolved.VA
|
|
||||||
Wrap = $resolved.Wrap
|
|
||||||
FillType = $resolved.FillType
|
|
||||||
NumberFormat = $resolved.NumberFormat
|
|
||||||
}
|
|
||||||
return Register-Format -key $key -props $props
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pre-register all formats from areas
|
|
||||||
foreach ($area in $def.areas) {
|
|
||||||
foreach ($row in $area.rows) {
|
|
||||||
# Skip empty row placeholder
|
|
||||||
if ($row.empty) { continue }
|
|
||||||
|
|
||||||
# Row height format
|
|
||||||
if ($row.height) {
|
|
||||||
$hKey = Get-FormatKey -height ([int]$row.height)
|
|
||||||
Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# rowStyle gap-fill format (no content → no fillType)
|
|
||||||
if ($row.rowStyle) {
|
|
||||||
Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Explicit cell formats
|
|
||||||
if ($row.cells) {
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
|
|
||||||
$ft = Get-FillType $cell
|
|
||||||
Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Generate XML ---
|
|
||||||
|
|
||||||
$xml = New-Object System.Text.StringBuilder 4096
|
|
||||||
|
|
||||||
function X {
|
|
||||||
param([string]$text)
|
|
||||||
$script:xml.AppendLine($text) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7a. Header
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
|
|
||||||
|
|
||||||
# 7b. Language settings
|
|
||||||
X "`t<languageSettings>"
|
|
||||||
X "`t`t<currentLanguage>ru</currentLanguage>"
|
|
||||||
X "`t`t<defaultLanguage>ru</defaultLanguage>"
|
|
||||||
X "`t`t<languageInfo>"
|
|
||||||
X "`t`t`t<id>ru</id>"
|
|
||||||
X "`t`t`t<code>Русский</code>"
|
|
||||||
X "`t`t`t<description>Русский</description>"
|
|
||||||
X "`t`t</languageInfo>"
|
|
||||||
X "`t</languageSettings>"
|
|
||||||
|
|
||||||
# 7c. Columns
|
|
||||||
X "`t<columns>"
|
|
||||||
X "`t`t<size>$totalColumns</size>"
|
|
||||||
|
|
||||||
# Emit columnsItem for columns with non-default widths
|
|
||||||
foreach ($col in ($colFormatMap.Keys | Sort-Object)) {
|
|
||||||
$fmtIdx = $colFormatMap[$col]
|
|
||||||
$colIdx = $col - 1 # Convert to 0-based
|
|
||||||
X "`t`t<columnsItem>"
|
|
||||||
X "`t`t`t<index>$colIdx</index>"
|
|
||||||
X "`t`t`t<column>"
|
|
||||||
X "`t`t`t`t<formatIndex>$fmtIdx</formatIndex>"
|
|
||||||
X "`t`t`t</column>"
|
|
||||||
X "`t`t</columnsItem>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t</columns>"
|
|
||||||
|
|
||||||
# 7d. Rows — main generation loop
|
|
||||||
$globalRow = 0
|
|
||||||
$merges = @()
|
|
||||||
$namedItems = @()
|
|
||||||
$totalRowCount = 0
|
|
||||||
|
|
||||||
foreach ($area in $def.areas) {
|
|
||||||
$areaStartRow = $globalRow
|
|
||||||
$areaName = $area.name
|
|
||||||
$activeRowspans = @() # @{ColStart=1-based; ColEnd=1-based; EndLocalRow=int}
|
|
||||||
$localRow = 0
|
|
||||||
|
|
||||||
foreach ($row in $area.rows) {
|
|
||||||
# Empty row placeholder: emit N empty rows
|
|
||||||
if ($row.empty) {
|
|
||||||
$count = [int]$row.empty
|
|
||||||
for ($ei = 0; $ei -lt $count; $ei++) {
|
|
||||||
X "`t<rowsItem>"
|
|
||||||
X "`t`t<index>$globalRow</index>"
|
|
||||||
X "`t`t<row>"
|
|
||||||
X "`t`t`t<empty>true</empty>"
|
|
||||||
X "`t`t</row>"
|
|
||||||
X "`t</rowsItem>"
|
|
||||||
$globalRow++; $localRow++
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build set of columns occupied by rowspans from previous rows
|
|
||||||
$rowspanOccupied = @{} # 1-based col -> $true
|
|
||||||
foreach ($rs in $activeRowspans) {
|
|
||||||
if ($localRow -gt $rs.StartLocalRow -and $localRow -le $rs.EndLocalRow) {
|
|
||||||
for ($c = $rs.ColStart; $c -le $rs.ColEnd; $c++) {
|
|
||||||
$rowspanOccupied[$c] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rowHasContent = $false
|
|
||||||
$rowCells = @() # array of { Col(0-based), FormatIdx, Content }
|
|
||||||
|
|
||||||
# Determine row height format
|
|
||||||
$rowFormatIdx = 0
|
|
||||||
if ($row.height) {
|
|
||||||
$hKey = Get-FormatKey -height ([int]$row.height)
|
|
||||||
# Find format index for this key
|
|
||||||
$rIdx = 0
|
|
||||||
foreach ($k in $formatRegistry.Keys) {
|
|
||||||
$rIdx++
|
|
||||||
if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($row.cells -and $row.cells.Count -gt 0) {
|
|
||||||
$rowHasContent = $true
|
|
||||||
|
|
||||||
# Build set of occupied columns (1-based): explicit cells + rowspan from above
|
|
||||||
$occupiedCols = @{}
|
|
||||||
foreach ($rsk in $rowspanOccupied.Keys) { $occupiedCols[$rsk] = $true }
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$colStart = [int]$cell.col
|
|
||||||
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
|
|
||||||
for ($c = $colStart; $c -lt ($colStart + $colSpan); $c++) {
|
|
||||||
$occupiedCols[$c] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate explicit cells
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$colStart = [int]$cell.col
|
|
||||||
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
|
|
||||||
$rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 }
|
|
||||||
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
|
|
||||||
$ft = Get-FillType $cell
|
|
||||||
$fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft
|
|
||||||
|
|
||||||
$cellInfo = @{
|
|
||||||
Col = $colStart - 1 # 0-based
|
|
||||||
FormatIdx = $fmtIdx
|
|
||||||
Param = $cell.param
|
|
||||||
Detail = $cell.detail
|
|
||||||
Text = $cell.text
|
|
||||||
Template = $cell.template
|
|
||||||
}
|
|
||||||
$rowCells += $cellInfo
|
|
||||||
|
|
||||||
# Track rowspan for subsequent rows
|
|
||||||
if ($rowspan -gt 1) {
|
|
||||||
$activeRowspans += @{
|
|
||||||
ColStart = $colStart
|
|
||||||
ColEnd = $colStart + $colSpan - 1
|
|
||||||
StartLocalRow = $localRow
|
|
||||||
EndLocalRow = $localRow + $rowspan - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect merge (horizontal, vertical, or both)
|
|
||||||
if ($colSpan -gt 1 -or $rowspan -gt 1) {
|
|
||||||
$merge = @{ R = $globalRow; C = $colStart - 1; W = $colSpan - 1 }
|
|
||||||
if ($rowspan -gt 1) { $merge.H = $rowspan - 1 }
|
|
||||||
$merges += $merge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate gap-fill cells for rowStyle
|
|
||||||
if ($row.rowStyle) {
|
|
||||||
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if (-not $occupiedCols.ContainsKey($c)) {
|
|
||||||
$rowCells += @{
|
|
||||||
Col = $c - 1 # 0-based
|
|
||||||
FormatIdx = $gapFmtIdx
|
|
||||||
Param = $null
|
|
||||||
Detail = $null
|
|
||||||
Text = $null
|
|
||||||
Template = $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sort cells by column
|
|
||||||
$rowCells = $rowCells | Sort-Object { $_.Col }
|
|
||||||
|
|
||||||
} elseif ($row.rowStyle) {
|
|
||||||
# Row with only rowStyle, no explicit cells — fill non-rowspan columns
|
|
||||||
$rowHasContent = $true
|
|
||||||
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if ($rowspanOccupied.ContainsKey($c)) { continue }
|
|
||||||
$rowCells += @{
|
|
||||||
Col = $c - 1
|
|
||||||
FormatIdx = $gapFmtIdx
|
|
||||||
Param = $null
|
|
||||||
Detail = $null
|
|
||||||
Text = $null
|
|
||||||
Template = $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Emit rowsItem
|
|
||||||
X "`t<rowsItem>"
|
|
||||||
X "`t`t<index>$globalRow</index>"
|
|
||||||
X "`t`t<row>"
|
|
||||||
|
|
||||||
if ($rowFormatIdx -gt 0) {
|
|
||||||
X "`t`t`t<formatIndex>$rowFormatIdx</formatIndex>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $rowHasContent) {
|
|
||||||
X "`t`t`t<empty>true</empty>"
|
|
||||||
} else {
|
|
||||||
foreach ($cellInfo in $rowCells) {
|
|
||||||
X "`t`t`t<c>"
|
|
||||||
X "`t`t`t`t<i>$($cellInfo.Col)</i>"
|
|
||||||
X "`t`t`t`t<c>"
|
|
||||||
X "`t`t`t`t`t<f>$($cellInfo.FormatIdx)</f>"
|
|
||||||
|
|
||||||
if ($cellInfo.Param) {
|
|
||||||
X "`t`t`t`t`t<parameter>$($cellInfo.Param)</parameter>"
|
|
||||||
if ($cellInfo.Detail) {
|
|
||||||
X "`t`t`t`t`t<detailParameter>$($cellInfo.Detail)</detailParameter>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cellInfo.Text) {
|
|
||||||
X "`t`t`t`t`t<tl>"
|
|
||||||
X "`t`t`t`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Text)</v8:content>"
|
|
||||||
X "`t`t`t`t`t`t</v8:item>"
|
|
||||||
X "`t`t`t`t`t</tl>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cellInfo.Template) {
|
|
||||||
X "`t`t`t`t`t<tl>"
|
|
||||||
X "`t`t`t`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Template)</v8:content>"
|
|
||||||
X "`t`t`t`t`t`t</v8:item>"
|
|
||||||
X "`t`t`t`t`t</tl>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t`t`t`t</c>"
|
|
||||||
X "`t`t`t</c>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t`t</row>"
|
|
||||||
X "`t</rowsItem>"
|
|
||||||
|
|
||||||
$localRow++
|
|
||||||
$globalRow++
|
|
||||||
}
|
|
||||||
|
|
||||||
$areaEndRow = $globalRow - 1
|
|
||||||
$namedItems += @{
|
|
||||||
Name = $areaName
|
|
||||||
BeginRow = $areaStartRow
|
|
||||||
EndRow = $areaEndRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalRowCount = $globalRow
|
|
||||||
|
|
||||||
# 7e. Scalar metadata
|
|
||||||
X "`t<templateMode>true</templateMode>"
|
|
||||||
X "`t<defaultFormatIndex>$defaultFormatIndex</defaultFormatIndex>"
|
|
||||||
X "`t<height>$totalRowCount</height>"
|
|
||||||
X "`t<vgRows>$totalRowCount</vgRows>"
|
|
||||||
|
|
||||||
# 7f. Merges
|
|
||||||
foreach ($m in $merges) {
|
|
||||||
X "`t<merge>"
|
|
||||||
X "`t`t<r>$($m.R)</r>"
|
|
||||||
X "`t`t<c>$($m.C)</c>"
|
|
||||||
if ($m.H) { X "`t`t<h>$($m.H)</h>" }
|
|
||||||
X "`t`t<w>$($m.W)</w>"
|
|
||||||
X "`t</merge>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7g. Named items
|
|
||||||
foreach ($ni in $namedItems) {
|
|
||||||
X "`t<namedItem xsi:type=`"NamedItemCells`">"
|
|
||||||
X "`t`t<name>$($ni.Name)</name>"
|
|
||||||
X "`t`t<area>"
|
|
||||||
X "`t`t`t<type>Rows</type>"
|
|
||||||
X "`t`t`t<beginRow>$($ni.BeginRow)</beginRow>"
|
|
||||||
X "`t`t`t<endRow>$($ni.EndRow)</endRow>"
|
|
||||||
X "`t`t`t<beginColumn>-1</beginColumn>"
|
|
||||||
X "`t`t`t<endColumn>-1</endColumn>"
|
|
||||||
X "`t`t</area>"
|
|
||||||
X "`t</namedItem>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7h. Line palette
|
|
||||||
if ($hasThinBorders) {
|
|
||||||
X "`t<line width=`"1`" gap=`"false`">"
|
|
||||||
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
|
|
||||||
X "`t</line>"
|
|
||||||
}
|
|
||||||
if ($hasThickBorders) {
|
|
||||||
X "`t<line width=`"2`" gap=`"false`">"
|
|
||||||
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
|
|
||||||
X "`t</line>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7i. Font palette
|
|
||||||
foreach ($fe in $fontEntries) {
|
|
||||||
X "`t<font faceName=`"$($fe.Face)`" height=`"$($fe.Size)`" bold=`"$($fe.Bold)`" italic=`"$($fe.Italic)`" underline=`"$($fe.Underline)`" strikeout=`"$($fe.Strikeout)`" kind=`"Absolute`" scale=`"100`"/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7j. Format palette
|
|
||||||
foreach ($key in $formatRegistry.Keys) {
|
|
||||||
$fmt = $formatRegistry[$key]
|
|
||||||
X "`t<format>"
|
|
||||||
|
|
||||||
if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) {
|
|
||||||
X "`t`t<font>$($fmt.FontIdx)</font>"
|
|
||||||
}
|
|
||||||
if ($fmt.LB -ne $null -and $fmt.LB -ge 0) {
|
|
||||||
X "`t`t<leftBorder>$($fmt.LB)</leftBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.TB -ne $null -and $fmt.TB -ge 0) {
|
|
||||||
X "`t`t<topBorder>$($fmt.TB)</topBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.RB -ne $null -and $fmt.RB -ge 0) {
|
|
||||||
X "`t`t<rightBorder>$($fmt.RB)</rightBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.BB -ne $null -and $fmt.BB -ge 0) {
|
|
||||||
X "`t`t<bottomBorder>$($fmt.BB)</bottomBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.Width) {
|
|
||||||
X "`t`t<width>$($fmt.Width)</width>"
|
|
||||||
}
|
|
||||||
if ($fmt.Height) {
|
|
||||||
X "`t`t<height>$($fmt.Height)</height>"
|
|
||||||
}
|
|
||||||
if ($fmt.HA) {
|
|
||||||
X "`t`t<horizontalAlignment>$($fmt.HA)</horizontalAlignment>"
|
|
||||||
}
|
|
||||||
if ($fmt.VA) {
|
|
||||||
X "`t`t<verticalAlignment>$($fmt.VA)</verticalAlignment>"
|
|
||||||
}
|
|
||||||
if ($fmt.Wrap -eq $true) {
|
|
||||||
X "`t`t<textPlacement>Wrap</textPlacement>"
|
|
||||||
}
|
|
||||||
if ($fmt.FillType) {
|
|
||||||
X "`t`t<fillType>$($fmt.FillType)</fillType>"
|
|
||||||
}
|
|
||||||
if ($fmt.NumberFormat) {
|
|
||||||
X "`t`t<format>"
|
|
||||||
X "`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t<v8:content>$(Esc-Xml $fmt.NumberFormat)</v8:content>"
|
|
||||||
X "`t`t`t</v8:item>"
|
|
||||||
X "`t`t</format>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t</format>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7k. Close document
|
|
||||||
X '</document>'
|
|
||||||
|
|
||||||
# --- 8. Write output ---
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
|
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc)
|
|
||||||
|
|
||||||
# --- 9. Summary ---
|
|
||||||
|
|
||||||
Write-Host "[OK] Compiled: $OutputPath"
|
|
||||||
if ($def.page) {
|
|
||||||
Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth"
|
|
||||||
}
|
|
||||||
Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns"
|
|
||||||
Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)"
|
|
||||||
Write-Host " Merges: $($merges.Count)"
|
|
||||||
@@ -1,636 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
|
|
||||||
parser.add_argument('-JsonPath', type=str, required=True)
|
|
||||||
parser.add_argument('-OutputPath', type=str, required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
json_path = args.JsonPath
|
|
||||||
if not os.path.exists(json_path):
|
|
||||||
print(f"File not found: {json_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
defn = json.load(f)
|
|
||||||
|
|
||||||
if not defn.get('columns'):
|
|
||||||
print("Required field 'columns' is missing", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not defn.get('areas'):
|
|
||||||
print("Required field 'areas' is missing", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
total_columns = int(defn['columns'])
|
|
||||||
default_width = int(defn['defaultWidth']) if defn.get('defaultWidth') else 10
|
|
||||||
|
|
||||||
# --- 2. Build font palette ---
|
|
||||||
font_map = {} # name -> 0-based index
|
|
||||||
font_entries = [] # list of dicts
|
|
||||||
|
|
||||||
def add_font(name, font_def):
|
|
||||||
face = font_def.get('face', 'Arial') if font_def else 'Arial'
|
|
||||||
size = int(font_def.get('size', 10)) if font_def else 10
|
|
||||||
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
|
|
||||||
italic = 'true' if font_def and font_def.get('italic') is True else 'false'
|
|
||||||
underline = 'true' if font_def and font_def.get('underline') is True else 'false'
|
|
||||||
strikeout = 'true' if font_def and font_def.get('strikeout') is True else 'false'
|
|
||||||
|
|
||||||
idx = len(font_entries)
|
|
||||||
font_map[name] = idx
|
|
||||||
font_entries.append({
|
|
||||||
'Face': face,
|
|
||||||
'Size': size,
|
|
||||||
'Bold': bold,
|
|
||||||
'Italic': italic,
|
|
||||||
'Underline': underline,
|
|
||||||
'Strikeout': strikeout,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Add user-defined fonts
|
|
||||||
has_default = False
|
|
||||||
if defn.get('fonts'):
|
|
||||||
for fname, fdef in defn['fonts'].items():
|
|
||||||
if fname == 'default':
|
|
||||||
has_default = True
|
|
||||||
add_font(fname, fdef)
|
|
||||||
|
|
||||||
# Ensure default font exists
|
|
||||||
if not has_default:
|
|
||||||
add_font('default', {'face': 'Arial', 'size': 10})
|
|
||||||
|
|
||||||
# --- 3. Determine line palette ---
|
|
||||||
has_thin_borders = False
|
|
||||||
has_thick_borders = False
|
|
||||||
|
|
||||||
if defn.get('styles'):
|
|
||||||
for sname, sval in defn['styles'].items():
|
|
||||||
if sval.get('border') and sval['border'] != 'none':
|
|
||||||
if sval.get('borderWidth') == 'thick':
|
|
||||||
has_thick_borders = True
|
|
||||||
else:
|
|
||||||
has_thin_borders = True
|
|
||||||
|
|
||||||
thin_line_index = -1
|
|
||||||
thick_line_index = -1
|
|
||||||
line_count = 0
|
|
||||||
if has_thin_borders:
|
|
||||||
thin_line_index = line_count
|
|
||||||
line_count += 1
|
|
||||||
if has_thick_borders:
|
|
||||||
thick_line_index = line_count
|
|
||||||
line_count += 1
|
|
||||||
|
|
||||||
# --- 4. Parse column width specs ---
|
|
||||||
def parse_column_spec(spec):
|
|
||||||
cols = []
|
|
||||||
for part in spec.split(','):
|
|
||||||
part = part.strip()
|
|
||||||
m = re.match(r'^(\d+)-(\d+)$', part)
|
|
||||||
if m:
|
|
||||||
from_col = int(m.group(1))
|
|
||||||
to_col = int(m.group(2))
|
|
||||||
for i in range(from_col, to_col + 1):
|
|
||||||
cols.append(i)
|
|
||||||
else:
|
|
||||||
cols.append(int(part))
|
|
||||||
return cols
|
|
||||||
|
|
||||||
# --- 4a. Auto-calculate defaultWidth from page format ---
|
|
||||||
page_targets = {
|
|
||||||
'A4-landscape': 780,
|
|
||||||
'A4-portrait': 540,
|
|
||||||
}
|
|
||||||
|
|
||||||
page_name = None
|
|
||||||
target_width = None
|
|
||||||
if defn.get('page'):
|
|
||||||
page_name = str(defn['page'])
|
|
||||||
|
|
||||||
if re.match(r'^\d+$', page_name):
|
|
||||||
target_width = int(page_name)
|
|
||||||
elif page_name in page_targets:
|
|
||||||
target_width = page_targets[page_name]
|
|
||||||
else:
|
|
||||||
print(f"WARNING: Unknown page format '{page_name}'. Known: {', '.join(page_targets.keys())}, or a number.", file=sys.stderr)
|
|
||||||
|
|
||||||
if target_width:
|
|
||||||
total_units = 0.0
|
|
||||||
absolute_sum = 0
|
|
||||||
specified_cols = {}
|
|
||||||
|
|
||||||
if defn.get('columnWidths'):
|
|
||||||
for prop_name, prop_value in defn['columnWidths'].items():
|
|
||||||
val = str(prop_value)
|
|
||||||
cols = parse_column_spec(prop_name)
|
|
||||||
for c in cols:
|
|
||||||
specified_cols[int(c)] = True
|
|
||||||
m = re.match(r'^([0-9.]+)x$', val)
|
|
||||||
if m:
|
|
||||||
total_units += float(m.group(1))
|
|
||||||
else:
|
|
||||||
absolute_sum += int(val)
|
|
||||||
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c not in specified_cols:
|
|
||||||
total_units += 1.0
|
|
||||||
|
|
||||||
if total_units > 0:
|
|
||||||
default_width = round((target_width - absolute_sum) / total_units)
|
|
||||||
|
|
||||||
# Build column width map: 1-based col -> width
|
|
||||||
col_width_map = {}
|
|
||||||
if defn.get('columnWidths'):
|
|
||||||
for prop_name, prop_value in defn['columnWidths'].items():
|
|
||||||
val = str(prop_value)
|
|
||||||
m = re.match(r'^([0-9.]+)x$', val)
|
|
||||||
if m:
|
|
||||||
width = round(float(m.group(1)) * default_width)
|
|
||||||
else:
|
|
||||||
width = int(val)
|
|
||||||
columns = parse_column_spec(prop_name)
|
|
||||||
for c in columns:
|
|
||||||
col_width_map[c] = width
|
|
||||||
|
|
||||||
# --- 5. Style resolver ---
|
|
||||||
def resolve_style(style_name, fill_type):
|
|
||||||
font_idx = font_map.get('default', 0)
|
|
||||||
lb = -1; tb = -1; rb = -1; bb = -1
|
|
||||||
ha = ''; va = ''; nf = ''
|
|
||||||
wrap = False
|
|
||||||
|
|
||||||
if style_name and defn.get('styles'):
|
|
||||||
style = defn['styles'].get(style_name)
|
|
||||||
if style:
|
|
||||||
# Font
|
|
||||||
if style.get('font') and style['font'] in font_map:
|
|
||||||
font_idx = font_map[style['font']]
|
|
||||||
|
|
||||||
# Borders
|
|
||||||
if style.get('border') and style['border'] != 'none':
|
|
||||||
line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index
|
|
||||||
for side in style['border'].split(','):
|
|
||||||
side = side.strip()
|
|
||||||
if side == 'all':
|
|
||||||
lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx
|
|
||||||
elif side == 'left':
|
|
||||||
lb = line_idx
|
|
||||||
elif side == 'top':
|
|
||||||
tb = line_idx
|
|
||||||
elif side == 'right':
|
|
||||||
rb = line_idx
|
|
||||||
elif side == 'bottom':
|
|
||||||
bb = line_idx
|
|
||||||
|
|
||||||
# Alignment
|
|
||||||
if style.get('align'):
|
|
||||||
align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'}
|
|
||||||
ha = align_map.get(style['align'], '')
|
|
||||||
if style.get('valign'):
|
|
||||||
valign_map = {'top': 'Top', 'center': 'Center'}
|
|
||||||
va = valign_map.get(style['valign'], '')
|
|
||||||
|
|
||||||
# Wrap
|
|
||||||
if style.get('wrap') is True:
|
|
||||||
wrap = True
|
|
||||||
|
|
||||||
# Number format
|
|
||||||
if style.get('format'):
|
|
||||||
nf = style['format']
|
|
||||||
|
|
||||||
return {
|
|
||||||
'FontIdx': font_idx,
|
|
||||||
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
|
|
||||||
'HA': ha, 'VA': va,
|
|
||||||
'Wrap': wrap,
|
|
||||||
'FillType': fill_type,
|
|
||||||
'NumberFormat': nf,
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Format palette builder ---
|
|
||||||
format_registry = {} # key -> props
|
|
||||||
format_order = [] # ordered keys for index assignment
|
|
||||||
|
|
||||||
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
|
|
||||||
wrap=False, fill_type='', number_format='', width=-1, height=-1):
|
|
||||||
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
|
|
||||||
|
|
||||||
def register_format(key, props):
|
|
||||||
if key not in format_registry:
|
|
||||||
format_registry[key] = props
|
|
||||||
format_order.append(key)
|
|
||||||
# Return 1-based index
|
|
||||||
return format_order.index(key) + 1
|
|
||||||
|
|
||||||
# 6a. Default width format
|
|
||||||
default_format_key = get_format_key(width=default_width)
|
|
||||||
default_format_index = register_format(default_format_key, {'Width': default_width})
|
|
||||||
|
|
||||||
# 6b. Column width formats
|
|
||||||
col_format_map = {} # 1-based col -> format index
|
|
||||||
for col in sorted(col_width_map):
|
|
||||||
w = col_width_map[col]
|
|
||||||
key = get_format_key(width=w)
|
|
||||||
idx = register_format(key, {'Width': w})
|
|
||||||
col_format_map[int(col)] = idx
|
|
||||||
|
|
||||||
# 6c. Helper: determine fillType from cell content
|
|
||||||
def get_fill_type(cell):
|
|
||||||
if cell.get('param'):
|
|
||||||
return 'Parameter'
|
|
||||||
if cell.get('template'):
|
|
||||||
return 'Template'
|
|
||||||
if cell.get('text'):
|
|
||||||
return 'Text'
|
|
||||||
return ''
|
|
||||||
|
|
||||||
# Helper: register a cell format and return its index
|
|
||||||
def register_cell_format(style_name, fill_type):
|
|
||||||
resolved = resolve_style(style_name, fill_type)
|
|
||||||
key = get_format_key(
|
|
||||||
font_idx=resolved['FontIdx'],
|
|
||||||
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
|
|
||||||
ha=resolved['HA'], va=resolved['VA'],
|
|
||||||
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
|
|
||||||
number_format=resolved['NumberFormat'])
|
|
||||||
props = {
|
|
||||||
'FontIdx': resolved['FontIdx'],
|
|
||||||
'LB': resolved['LB'], 'TB': resolved['TB'],
|
|
||||||
'RB': resolved['RB'], 'BB': resolved['BB'],
|
|
||||||
'HA': resolved['HA'], 'VA': resolved['VA'],
|
|
||||||
'Wrap': resolved['Wrap'],
|
|
||||||
'FillType': resolved['FillType'],
|
|
||||||
'NumberFormat': resolved['NumberFormat'],
|
|
||||||
}
|
|
||||||
return register_format(key, props)
|
|
||||||
|
|
||||||
# Pre-register all formats from areas
|
|
||||||
for area in defn['areas']:
|
|
||||||
for row in area.get('rows', []):
|
|
||||||
# Skip list-of-values shorthand rows (treated as empty rows like PS1)
|
|
||||||
if isinstance(row, list):
|
|
||||||
continue
|
|
||||||
# Skip empty row placeholder
|
|
||||||
if row.get('empty'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Row height format
|
|
||||||
if row.get('height'):
|
|
||||||
h_key = get_format_key(height=int(row['height']))
|
|
||||||
register_format(h_key, {'Height': int(row['height'])})
|
|
||||||
|
|
||||||
# rowStyle gap-fill format
|
|
||||||
if row.get('rowStyle'):
|
|
||||||
register_cell_format(row['rowStyle'], '')
|
|
||||||
|
|
||||||
# Explicit cell formats
|
|
||||||
if row.get('cells'):
|
|
||||||
for cell in row['cells']:
|
|
||||||
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
|
|
||||||
ft = get_fill_type(cell)
|
|
||||||
register_cell_format(cell_style, ft)
|
|
||||||
|
|
||||||
# --- 7. Generate XML ---
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# 7a. Header
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">')
|
|
||||||
|
|
||||||
# 7b. Language settings
|
|
||||||
lines.append('\t<languageSettings>')
|
|
||||||
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
|
|
||||||
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
|
|
||||||
lines.append('\t\t<languageInfo>')
|
|
||||||
lines.append('\t\t\t<id>ru</id>')
|
|
||||||
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
|
|
||||||
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
|
|
||||||
lines.append('\t\t</languageInfo>')
|
|
||||||
lines.append('\t</languageSettings>')
|
|
||||||
|
|
||||||
# 7c. Columns
|
|
||||||
lines.append('\t<columns>')
|
|
||||||
lines.append(f'\t\t<size>{total_columns}</size>')
|
|
||||||
|
|
||||||
# Emit columnsItem for columns with non-default widths
|
|
||||||
for col in sorted(col_format_map.keys()):
|
|
||||||
fmt_idx = col_format_map[col]
|
|
||||||
col_idx = col - 1 # Convert to 0-based
|
|
||||||
lines.append('\t\t<columnsItem>')
|
|
||||||
lines.append(f'\t\t\t<index>{col_idx}</index>')
|
|
||||||
lines.append('\t\t\t<column>')
|
|
||||||
lines.append(f'\t\t\t\t<formatIndex>{fmt_idx}</formatIndex>')
|
|
||||||
lines.append('\t\t\t</column>')
|
|
||||||
lines.append('\t\t</columnsItem>')
|
|
||||||
|
|
||||||
lines.append('\t</columns>')
|
|
||||||
|
|
||||||
# 7d. Rows -- main generation loop
|
|
||||||
global_row = 0
|
|
||||||
merges = []
|
|
||||||
named_items = []
|
|
||||||
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
|
|
||||||
|
|
||||||
for area in defn['areas']:
|
|
||||||
area_start_row = global_row
|
|
||||||
area_name = area.get('name', '')
|
|
||||||
active_rowspans = []
|
|
||||||
local_row = 0
|
|
||||||
|
|
||||||
for row in area.get('rows', []):
|
|
||||||
# List-of-values shorthand: treat as row with no properties (like PS1)
|
|
||||||
if isinstance(row, list):
|
|
||||||
row = {}
|
|
||||||
# Empty row placeholder: emit N empty rows
|
|
||||||
if row.get('empty'):
|
|
||||||
count = int(row['empty'])
|
|
||||||
for ei in range(count):
|
|
||||||
lines.append('\t<rowsItem>')
|
|
||||||
lines.append(f'\t\t<index>{global_row}</index>')
|
|
||||||
lines.append('\t\t<row>')
|
|
||||||
lines.append('\t\t\t<empty>true</empty>')
|
|
||||||
lines.append('\t\t</row>')
|
|
||||||
lines.append('\t</rowsItem>')
|
|
||||||
global_row += 1
|
|
||||||
local_row += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Build set of columns occupied by rowspans from previous rows
|
|
||||||
rowspan_occupied = {}
|
|
||||||
for rs in active_rowspans:
|
|
||||||
if local_row > rs['StartLocalRow'] and local_row <= rs['EndLocalRow']:
|
|
||||||
for c in range(rs['ColStart'], rs['ColEnd'] + 1):
|
|
||||||
rowspan_occupied[c] = True
|
|
||||||
|
|
||||||
row_has_content = False
|
|
||||||
row_cells = []
|
|
||||||
|
|
||||||
# Determine row height format
|
|
||||||
row_format_idx = 0
|
|
||||||
if row.get('height'):
|
|
||||||
h_key = get_format_key(height=int(row['height']))
|
|
||||||
if h_key in format_registry:
|
|
||||||
row_format_idx = format_order.index(h_key) + 1
|
|
||||||
|
|
||||||
if row.get('cells') and len(row['cells']) > 0:
|
|
||||||
row_has_content = True
|
|
||||||
|
|
||||||
# Build set of occupied columns (1-based)
|
|
||||||
occupied_cols = dict(rowspan_occupied)
|
|
||||||
for cell in row['cells']:
|
|
||||||
col_start = int(cell['col'])
|
|
||||||
col_span = int(cell.get('span', 1))
|
|
||||||
for c in range(col_start, col_start + col_span):
|
|
||||||
occupied_cols[c] = True
|
|
||||||
|
|
||||||
# Generate explicit cells
|
|
||||||
for cell in row['cells']:
|
|
||||||
col_start = int(cell['col'])
|
|
||||||
col_span = int(cell.get('span', 1))
|
|
||||||
rowspan = int(cell.get('rowspan', 1))
|
|
||||||
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
|
|
||||||
ft = get_fill_type(cell)
|
|
||||||
fmt_idx = register_cell_format(cell_style, ft)
|
|
||||||
|
|
||||||
cell_info = {
|
|
||||||
'Col': col_start - 1, # 0-based
|
|
||||||
'FormatIdx': fmt_idx,
|
|
||||||
'Param': cell.get('param'),
|
|
||||||
'Detail': cell.get('detail'),
|
|
||||||
'Text': cell.get('text'),
|
|
||||||
'Template': cell.get('template'),
|
|
||||||
}
|
|
||||||
row_cells.append(cell_info)
|
|
||||||
|
|
||||||
# Track rowspan for subsequent rows
|
|
||||||
if rowspan > 1:
|
|
||||||
active_rowspans.append({
|
|
||||||
'ColStart': col_start,
|
|
||||||
'ColEnd': col_start + col_span - 1,
|
|
||||||
'StartLocalRow': local_row,
|
|
||||||
'EndLocalRow': local_row + rowspan - 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Collect merge
|
|
||||||
if col_span > 1 or rowspan > 1:
|
|
||||||
merge = {'R': global_row, 'C': col_start - 1, 'W': col_span - 1}
|
|
||||||
if rowspan > 1:
|
|
||||||
merge['H'] = rowspan - 1
|
|
||||||
merges.append(merge)
|
|
||||||
|
|
||||||
# Generate gap-fill cells for rowStyle
|
|
||||||
if row.get('rowStyle'):
|
|
||||||
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c not in occupied_cols:
|
|
||||||
row_cells.append({
|
|
||||||
'Col': c - 1,
|
|
||||||
'FormatIdx': gap_fmt_idx,
|
|
||||||
'Param': None,
|
|
||||||
'Detail': None,
|
|
||||||
'Text': None,
|
|
||||||
'Template': None,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort cells by column
|
|
||||||
row_cells.sort(key=lambda x: x['Col'])
|
|
||||||
|
|
||||||
elif row.get('rowStyle'):
|
|
||||||
# Row with only rowStyle, no explicit cells
|
|
||||||
row_has_content = True
|
|
||||||
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c in rowspan_occupied:
|
|
||||||
continue
|
|
||||||
row_cells.append({
|
|
||||||
'Col': c - 1,
|
|
||||||
'FormatIdx': gap_fmt_idx,
|
|
||||||
'Param': None,
|
|
||||||
'Detail': None,
|
|
||||||
'Text': None,
|
|
||||||
'Template': None,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Emit rowsItem
|
|
||||||
lines.append('\t<rowsItem>')
|
|
||||||
lines.append(f'\t\t<index>{global_row}</index>')
|
|
||||||
lines.append('\t\t<row>')
|
|
||||||
|
|
||||||
if row_format_idx > 0:
|
|
||||||
lines.append(f'\t\t\t<formatIndex>{row_format_idx}</formatIndex>')
|
|
||||||
|
|
||||||
if not row_has_content:
|
|
||||||
lines.append('\t\t\t<empty>true</empty>')
|
|
||||||
else:
|
|
||||||
for cell_info in row_cells:
|
|
||||||
lines.append('\t\t\t<c>')
|
|
||||||
lines.append(f'\t\t\t\t<i>{cell_info["Col"]}</i>')
|
|
||||||
lines.append('\t\t\t\t<c>')
|
|
||||||
lines.append(f'\t\t\t\t\t<f>{cell_info["FormatIdx"]}</f>')
|
|
||||||
|
|
||||||
if cell_info['Param']:
|
|
||||||
lines.append(f'\t\t\t\t\t<parameter>{cell_info["Param"]}</parameter>')
|
|
||||||
if cell_info['Detail']:
|
|
||||||
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
|
|
||||||
|
|
||||||
if cell_info['Text']:
|
|
||||||
lines.append('\t\t\t\t\t<tl>')
|
|
||||||
lines.append('\t\t\t\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Text"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t</tl>')
|
|
||||||
|
|
||||||
if cell_info['Template']:
|
|
||||||
lines.append('\t\t\t\t\t<tl>')
|
|
||||||
lines.append('\t\t\t\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Template"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t</tl>')
|
|
||||||
|
|
||||||
lines.append('\t\t\t\t</c>')
|
|
||||||
lines.append('\t\t\t</c>')
|
|
||||||
|
|
||||||
lines.append('\t\t</row>')
|
|
||||||
lines.append('\t</rowsItem>')
|
|
||||||
|
|
||||||
local_row += 1
|
|
||||||
global_row += 1
|
|
||||||
|
|
||||||
area_end_row = global_row - 1
|
|
||||||
named_items.append({
|
|
||||||
'Name': area_name,
|
|
||||||
'BeginRow': area_start_row,
|
|
||||||
'EndRow': area_end_row,
|
|
||||||
})
|
|
||||||
|
|
||||||
total_row_count = global_row
|
|
||||||
|
|
||||||
# 7e. Scalar metadata
|
|
||||||
lines.append(f'\t<templateMode>true</templateMode>')
|
|
||||||
lines.append(f'\t<defaultFormatIndex>{default_format_index}</defaultFormatIndex>')
|
|
||||||
lines.append(f'\t<height>{total_row_count}</height>')
|
|
||||||
lines.append(f'\t<vgRows>{total_row_count}</vgRows>')
|
|
||||||
|
|
||||||
# 7f. Merges
|
|
||||||
for m in merges:
|
|
||||||
lines.append('\t<merge>')
|
|
||||||
lines.append(f'\t\t<r>{m["R"]}</r>')
|
|
||||||
lines.append(f'\t\t<c>{m["C"]}</c>')
|
|
||||||
if m.get('H'):
|
|
||||||
lines.append(f'\t\t<h>{m["H"]}</h>')
|
|
||||||
lines.append(f'\t\t<w>{m["W"]}</w>')
|
|
||||||
lines.append('\t</merge>')
|
|
||||||
|
|
||||||
# 7g. Named items
|
|
||||||
for ni in named_items:
|
|
||||||
lines.append('\t<namedItem xsi:type="NamedItemCells">')
|
|
||||||
lines.append(f'\t\t<name>{ni["Name"]}</name>')
|
|
||||||
lines.append('\t\t<area>')
|
|
||||||
lines.append('\t\t\t<type>Rows</type>')
|
|
||||||
lines.append(f'\t\t\t<beginRow>{ni["BeginRow"]}</beginRow>')
|
|
||||||
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
|
|
||||||
lines.append('\t\t\t<beginColumn>-1</beginColumn>')
|
|
||||||
lines.append('\t\t\t<endColumn>-1</endColumn>')
|
|
||||||
lines.append('\t\t</area>')
|
|
||||||
lines.append('\t</namedItem>')
|
|
||||||
|
|
||||||
# 7h. Line palette
|
|
||||||
if has_thin_borders:
|
|
||||||
lines.append('\t<line width="1" gap="false">')
|
|
||||||
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
|
|
||||||
lines.append('\t</line>')
|
|
||||||
if has_thick_borders:
|
|
||||||
lines.append('\t<line width="2" gap="false">')
|
|
||||||
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
|
|
||||||
lines.append('\t</line>')
|
|
||||||
|
|
||||||
# 7i. Font palette
|
|
||||||
for fe in font_entries:
|
|
||||||
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
|
|
||||||
|
|
||||||
# 7j. Format palette
|
|
||||||
for key in format_order:
|
|
||||||
fmt = format_registry[key]
|
|
||||||
lines.append('\t<format>')
|
|
||||||
|
|
||||||
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
|
|
||||||
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
|
|
||||||
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
|
|
||||||
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
|
|
||||||
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
|
|
||||||
if fmt.get('Width'):
|
|
||||||
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
|
|
||||||
if fmt.get('Height'):
|
|
||||||
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
|
|
||||||
if fmt.get('HA'):
|
|
||||||
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
|
|
||||||
if fmt.get('VA'):
|
|
||||||
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
|
|
||||||
if fmt.get('Wrap') is True:
|
|
||||||
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
|
|
||||||
if fmt.get('FillType'):
|
|
||||||
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
|
|
||||||
if fmt.get('NumberFormat'):
|
|
||||||
lines.append('\t\t<format>')
|
|
||||||
lines.append('\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t<v8:content>{esc_xml(fmt["NumberFormat"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t</format>')
|
|
||||||
|
|
||||||
lines.append('\t</format>')
|
|
||||||
|
|
||||||
# 7k. Close document
|
|
||||||
lines.append('</document>')
|
|
||||||
|
|
||||||
# --- 8. Write output ---
|
|
||||||
out_path = args.OutputPath
|
|
||||||
if not os.path.isabs(out_path):
|
|
||||||
out_path = os.path.join(os.getcwd(), out_path)
|
|
||||||
|
|
||||||
out_dir = os.path.dirname(out_path)
|
|
||||||
if out_dir and not os.path.exists(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
content = '\n'.join(lines) + '\n'
|
|
||||||
write_utf8_bom(out_path, content)
|
|
||||||
|
|
||||||
# --- 9. Summary ---
|
|
||||||
print(f"[OK] Compiled: {args.OutputPath}")
|
|
||||||
if defn.get('page'):
|
|
||||||
print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}")
|
|
||||||
print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}")
|
|
||||||
print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}")
|
|
||||||
print(f" Merges: {len(merges)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: mxl-decompile
|
|
||||||
description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета
|
|
||||||
argument-hint: <TemplatePath> [OutputPath]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /mxl-decompile — Декомпилятор макета в DSL
|
|
||||||
|
|
||||||
Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/mxl-decompile <TemplatePath> [OutputPath]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|--------------|:------------:|-----------------------------------------|
|
|
||||||
| TemplatePath | да | Путь к Template.xml |
|
|
||||||
| OutputPath | нет | Путь для JSON (если не указан — stdout) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рабочий процесс
|
|
||||||
|
|
||||||
Декомпиляция существующего макета для анализа или доработки:
|
|
||||||
|
|
||||||
1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml
|
|
||||||
2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили)
|
|
||||||
3. Claude вызывает `/mxl-compile` для генерации нового Template.xml
|
|
||||||
4. Claude вызывает `/mxl-validate` для проверки
|
|
||||||
|
|
||||||
## JSON-схема DSL
|
|
||||||
|
|
||||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool).
|
|
||||||
|
|
||||||
## Генерация имён
|
|
||||||
|
|
||||||
Скрипт автоматически генерирует осмысленные имена:
|
|
||||||
|
|
||||||
- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам
|
|
||||||
- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств
|
|
||||||
|
|
||||||
## Детектирование `rowStyle`
|
|
||||||
|
|
||||||
Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода.
|
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias('Path')]
|
|
||||||
[string]$TemplatePath,
|
|
||||||
|
|
||||||
[string]$OutputPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and parse XML ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $TemplatePath)) {
|
|
||||||
Write-Error "File not found: $TemplatePath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $false
|
|
||||||
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
|
|
||||||
|
|
||||||
$root = $xmlDoc.DocumentElement
|
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
|
|
||||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
|
|
||||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
|
||||||
|
|
||||||
# --- 2. Extract font palette ---
|
|
||||||
|
|
||||||
$rawFonts = @()
|
|
||||||
foreach ($fNode in $root.SelectNodes("d:font", $ns)) {
|
|
||||||
$rawFonts += @{
|
|
||||||
Face = $fNode.GetAttribute("faceName")
|
|
||||||
Size = [int]$fNode.GetAttribute("height")
|
|
||||||
Bold = $fNode.GetAttribute("bold") -eq "true"
|
|
||||||
Italic = $fNode.GetAttribute("italic") -eq "true"
|
|
||||||
Underline = $fNode.GetAttribute("underline") -eq "true"
|
|
||||||
Strikeout = $fNode.GetAttribute("strikeout") -eq "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Extract line palette ---
|
|
||||||
|
|
||||||
$rawLines = @()
|
|
||||||
foreach ($lNode in $root.SelectNodes("d:line", $ns)) {
|
|
||||||
$rawLines += @{ Width = [int]$lNode.GetAttribute("width") }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Extract format palette ---
|
|
||||||
|
|
||||||
$rawFormats = @()
|
|
||||||
foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) {
|
|
||||||
$fmt = @{
|
|
||||||
FontIdx = -1
|
|
||||||
LB = -1; TB = -1; RB = -1; BB = -1
|
|
||||||
Width = 0; Height = 0
|
|
||||||
HA = ""; VA = ""
|
|
||||||
Wrap = $false; FillType = ""; DataFormat = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:font", $ns)
|
|
||||||
if ($n) { $fmt.FontIdx = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:leftBorder", $ns)
|
|
||||||
if ($n) { $fmt.LB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:topBorder", $ns)
|
|
||||||
if ($n) { $fmt.TB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:rightBorder", $ns)
|
|
||||||
if ($n) { $fmt.RB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns)
|
|
||||||
if ($n) { $fmt.BB = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:width", $ns)
|
|
||||||
if ($n) { $fmt.Width = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:height", $ns)
|
|
||||||
if ($n) { $fmt.Height = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns)
|
|
||||||
if ($n) { $fmt.HA = $n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns)
|
|
||||||
if ($n) { $fmt.VA = $n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:textPlacement", $ns)
|
|
||||||
if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:fillType", $ns)
|
|
||||||
if ($n) { $fmt.FillType = $n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns)
|
|
||||||
if ($n) { $fmt.DataFormat = $n.InnerText }
|
|
||||||
|
|
||||||
$rawFormats += $fmt
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Format {
|
|
||||||
param([int]$idx)
|
|
||||||
if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null }
|
|
||||||
return $rawFormats[$idx - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Extract columns and default width ---
|
|
||||||
|
|
||||||
$colNode = $root.SelectSingleNode("d:columns", $ns)
|
|
||||||
$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText
|
|
||||||
|
|
||||||
$colFormatIndices = @{}
|
|
||||||
foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) {
|
|
||||||
$colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText
|
|
||||||
$fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText
|
|
||||||
$colFormatIndices[$colIdx] = $fmtIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultFmtIdx = 0
|
|
||||||
$n = $root.SelectSingleNode("d:defaultFormatIndex", $ns)
|
|
||||||
if ($n) { $defaultFmtIdx = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$defaultWidth = 10
|
|
||||||
if ($defaultFmtIdx -gt 0) {
|
|
||||||
$defFmt = Get-Format $defaultFmtIdx
|
|
||||||
if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build column width map (1-based col → width), only non-default
|
|
||||||
$colWidthMap = [ordered]@{}
|
|
||||||
foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) {
|
|
||||||
$fmt = Get-Format $colFormatIndices[$col0]
|
|
||||||
if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) {
|
|
||||||
$col1 = [string]($col0 + 1)
|
|
||||||
$colWidthMap.Add($col1, $fmt.Width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Extract merges ---
|
|
||||||
|
|
||||||
$mergeMap = @{}
|
|
||||||
foreach ($mNode in $root.SelectNodes("d:merge", $ns)) {
|
|
||||||
$r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText
|
|
||||||
$c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText
|
|
||||||
$w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText
|
|
||||||
$hNode = $mNode.SelectSingleNode("d:h", $ns)
|
|
||||||
$h = if ($hNode) { [int]$hNode.InnerText } else { 0 }
|
|
||||||
$mergeMap["$r,$c"] = @{ W = $w; H = $h }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Extract named items ---
|
|
||||||
|
|
||||||
$namedAreas = @()
|
|
||||||
foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) {
|
|
||||||
$xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
|
||||||
if ($xsiType -ne "NamedItemCells") { continue }
|
|
||||||
|
|
||||||
$areaNode = $niNode.SelectSingleNode("d:area", $ns)
|
|
||||||
$areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText
|
|
||||||
if ($areaType -ne "Rows") { continue }
|
|
||||||
|
|
||||||
$namedAreas += @{
|
|
||||||
Name = $niNode.SelectSingleNode("d:name", $ns).InnerText
|
|
||||||
BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText
|
|
||||||
EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 8. Extract rows ---
|
|
||||||
|
|
||||||
$rowData = @{}
|
|
||||||
foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) {
|
|
||||||
$rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText
|
|
||||||
$rowNode = $riNode.SelectSingleNode("d:row", $ns)
|
|
||||||
|
|
||||||
$indexTo = $rowIdx
|
|
||||||
$itNode = $riNode.SelectSingleNode("d:indexTo", $ns)
|
|
||||||
if ($itNode) { $indexTo = [int]$itNode.InnerText }
|
|
||||||
|
|
||||||
$rowFmtIdx = 0
|
|
||||||
$fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns)
|
|
||||||
if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText }
|
|
||||||
|
|
||||||
$isEmpty = $false
|
|
||||||
$emptyNode = $rowNode.SelectSingleNode("d:empty", $ns)
|
|
||||||
if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true }
|
|
||||||
|
|
||||||
$cells = @()
|
|
||||||
if (-not $isEmpty) {
|
|
||||||
$col = -1
|
|
||||||
foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) {
|
|
||||||
$iNode = $cGroup.SelectSingleNode("d:i", $ns)
|
|
||||||
if ($iNode) { $col = [int]$iNode.InnerText }
|
|
||||||
else { $col++ }
|
|
||||||
|
|
||||||
$cContent = $cGroup.SelectSingleNode("d:c", $ns)
|
|
||||||
if (-not $cContent) { continue }
|
|
||||||
|
|
||||||
$cellFmtIdx = 0
|
|
||||||
$fNode = $cContent.SelectSingleNode("d:f", $ns)
|
|
||||||
if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText }
|
|
||||||
|
|
||||||
$param = $null
|
|
||||||
$pNode = $cContent.SelectSingleNode("d:parameter", $ns)
|
|
||||||
if ($pNode) { $param = $pNode.InnerText }
|
|
||||||
|
|
||||||
$detail = $null
|
|
||||||
$dNode = $cContent.SelectSingleNode("d:detailParameter", $ns)
|
|
||||||
if ($dNode) { $detail = $dNode.InnerText }
|
|
||||||
|
|
||||||
$text = $null
|
|
||||||
$tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns)
|
|
||||||
if ($tNode) { $text = $tNode.InnerText }
|
|
||||||
|
|
||||||
$cells += @{
|
|
||||||
Col = $col
|
|
||||||
FormatIdx = $cellFmtIdx
|
|
||||||
Param = $param
|
|
||||||
Detail = $detail
|
|
||||||
Text = $text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($r = $rowIdx; $r -le $indexTo; $r++) {
|
|
||||||
$rowData[$r] = @{
|
|
||||||
FormatIdx = $rowFmtIdx
|
|
||||||
Cells = $cells
|
|
||||||
Empty = $isEmpty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 9. Build style key (ignoring fillType) ---
|
|
||||||
|
|
||||||
function Get-BorderDesc {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return @{ Border = "none"; Thick = $false } }
|
|
||||||
|
|
||||||
$lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0
|
|
||||||
$rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0
|
|
||||||
|
|
||||||
if (-not $lb -and -not $tb -and -not $rb -and -not $bb) {
|
|
||||||
return @{ Border = "none"; Thick = $false }
|
|
||||||
}
|
|
||||||
|
|
||||||
$thick = $false
|
|
||||||
foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) {
|
|
||||||
if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) {
|
|
||||||
$thick = $true; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lb -and $tb -and $rb -and $bb) {
|
|
||||||
return @{ Border = "all"; Thick = $thick }
|
|
||||||
}
|
|
||||||
|
|
||||||
$sides = @()
|
|
||||||
if ($tb) { $sides += "top" }
|
|
||||||
if ($bb) { $sides += "bottom" }
|
|
||||||
if ($lb) { $sides += "left" }
|
|
||||||
if ($rb) { $sides += "right" }
|
|
||||||
|
|
||||||
return @{ Border = ($sides -join ","); Thick = $thick }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-StyleKey {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return "empty" }
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 10. Name fonts ---
|
|
||||||
|
|
||||||
$fontNames = @{}
|
|
||||||
$fontDefs = [ordered]@{}
|
|
||||||
|
|
||||||
if ($rawFonts.Count -gt 0) {
|
|
||||||
$fontNames[0] = "default"
|
|
||||||
$fontDefs["default"] = $rawFonts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-FontKey {
|
|
||||||
param($f)
|
|
||||||
return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$fontKeyMap = @{}
|
|
||||||
$fontKeyMap[(Get-FontKey $rawFonts[0])] = "default"
|
|
||||||
|
|
||||||
for ($i = 1; $i -lt $rawFonts.Count; $i++) {
|
|
||||||
$f = $rawFonts[$i]
|
|
||||||
$df = $rawFonts[0]
|
|
||||||
|
|
||||||
# Dedup: if identical font already named, reuse
|
|
||||||
$fKey = Get-FontKey $f
|
|
||||||
if ($fontKeyMap.ContainsKey($fKey)) {
|
|
||||||
$fontNames[$i] = $fontKeyMap[$fKey]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $null
|
|
||||||
|
|
||||||
if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) {
|
|
||||||
if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) {
|
|
||||||
$name = "bold"
|
|
||||||
} elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) {
|
|
||||||
$name = "italic"
|
|
||||||
} elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) {
|
|
||||||
$name = "underline"
|
|
||||||
}
|
|
||||||
} elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) {
|
|
||||||
$name = "header"
|
|
||||||
} elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) {
|
|
||||||
$name = "small"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $name) {
|
|
||||||
$parts = @()
|
|
||||||
if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() }
|
|
||||||
$parts += "$($f.Size)"
|
|
||||||
if ($f.Bold) { $parts += "bold" }
|
|
||||||
if ($f.Italic) { $parts += "italic" }
|
|
||||||
if ($f.Underline) { $parts += "underline" }
|
|
||||||
if ($f.Strikeout) { $parts += "strikeout" }
|
|
||||||
$name = $parts -join "-"
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseName = $name; $suffix = 2
|
|
||||||
while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
|
|
||||||
|
|
||||||
$fontNames[$i] = $name
|
|
||||||
$fontDefs[$name] = $f
|
|
||||||
$fontKeyMap[$fKey] = $name
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 11. Collect and name styles ---
|
|
||||||
|
|
||||||
$styleKeys = [ordered]@{}
|
|
||||||
$formatToStyleKey = @{}
|
|
||||||
|
|
||||||
foreach ($r in $rowData.Values) {
|
|
||||||
foreach ($cell in $r.Cells) {
|
|
||||||
$fmt = Get-Format $cell.FormatIdx
|
|
||||||
if (-not $fmt) { continue }
|
|
||||||
$key = Get-StyleKey $fmt
|
|
||||||
if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt }
|
|
||||||
$formatToStyleKey[$cell.FormatIdx] = $key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Name-Style {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return "default" }
|
|
||||||
$parts = @()
|
|
||||||
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
|
|
||||||
$parts += $fontNames[$fi]
|
|
||||||
}
|
|
||||||
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
if ($bd.Border -ne "none") {
|
|
||||||
if ($bd.Border -eq "all") { $parts += "bordered" }
|
|
||||||
else { $parts += "border-$($bd.Border)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fmt.HA -eq "Center") { $parts += "center" }
|
|
||||||
elseif ($fmt.HA -eq "Right") { $parts += "right" }
|
|
||||||
if ($fmt.VA -eq "Center") { $parts += "vcenter" }
|
|
||||||
elseif ($fmt.VA -eq "Top") { $parts += "vtop" }
|
|
||||||
if ($fmt.Wrap) { $parts += "wrap" }
|
|
||||||
if ($fmt.DataFormat) { $parts += "fmt" }
|
|
||||||
|
|
||||||
if ($parts.Count -eq 0) { return "default" }
|
|
||||||
return ($parts -join "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
$styleNames = [ordered]@{}
|
|
||||||
$styleDefs = [ordered]@{}
|
|
||||||
|
|
||||||
foreach ($key in $styleKeys.Keys) {
|
|
||||||
$fmt = $styleKeys[$key]
|
|
||||||
$name = Name-Style $fmt
|
|
||||||
|
|
||||||
$baseName = $name; $suffix = 2
|
|
||||||
while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
|
|
||||||
|
|
||||||
$styleNames[$key] = $name
|
|
||||||
|
|
||||||
$sDef = [ordered]@{}
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
|
|
||||||
$sDef["font"] = $fontNames[$fi]
|
|
||||||
}
|
|
||||||
if ($fmt.HA) {
|
|
||||||
$a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } }
|
|
||||||
if ($a) { $sDef["align"] = $a }
|
|
||||||
}
|
|
||||||
if ($fmt.VA) {
|
|
||||||
$a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } }
|
|
||||||
if ($a) { $sDef["valign"] = $a }
|
|
||||||
}
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
if ($bd.Border -ne "none") {
|
|
||||||
$sDef["border"] = $bd.Border
|
|
||||||
if ($bd.Thick) { $sDef["borderWidth"] = "thick" }
|
|
||||||
}
|
|
||||||
if ($fmt.Wrap) { $sDef["wrap"] = $true }
|
|
||||||
if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat }
|
|
||||||
|
|
||||||
$styleDefs[$name] = $sDef
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-StyleName {
|
|
||||||
param([int]$fmtIdx)
|
|
||||||
$key = $formatToStyleKey[$fmtIdx]
|
|
||||||
if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] }
|
|
||||||
return "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 12. Build areas ---
|
|
||||||
|
|
||||||
$dslAreas = @()
|
|
||||||
|
|
||||||
foreach ($area in $namedAreas) {
|
|
||||||
$areaRows = @()
|
|
||||||
|
|
||||||
for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) {
|
|
||||||
$rd = $rowData[$globalRow]
|
|
||||||
|
|
||||||
if (-not $rd -or $rd.Empty) {
|
|
||||||
$areaRows += [ordered]@{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslRow = [ordered]@{}
|
|
||||||
|
|
||||||
# Row height
|
|
||||||
if ($rd.FormatIdx -gt 0) {
|
|
||||||
$rowFmt = Get-Format $rd.FormatIdx
|
|
||||||
if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Separate content cells from gap-fill cells
|
|
||||||
$contentCells = @()
|
|
||||||
$gapCells = @()
|
|
||||||
|
|
||||||
foreach ($cell in $rd.Cells) {
|
|
||||||
$hasContent = $cell.Param -or $cell.Text
|
|
||||||
$hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)")
|
|
||||||
|
|
||||||
if ($hasContent -or $hasMerge) {
|
|
||||||
$contentCells += $cell
|
|
||||||
} else {
|
|
||||||
$gapCells += $cell
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect rowStyle
|
|
||||||
$rowStyleName = $null
|
|
||||||
$rowStyleKey = $null
|
|
||||||
|
|
||||||
if ($gapCells.Count -gt 0) {
|
|
||||||
$gapKeys = @{}
|
|
||||||
foreach ($gc in $gapCells) {
|
|
||||||
$fmt = Get-Format $gc.FormatIdx
|
|
||||||
$gapKeys[(Get-StyleKey $fmt)] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gapKeys.Count -eq 1) {
|
|
||||||
$rowStyleKey = @($gapKeys.Keys)[0]
|
|
||||||
if ($styleNames.Contains($rowStyleKey)) {
|
|
||||||
$rowStyleName = $styleNames[$rowStyleKey]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName }
|
|
||||||
|
|
||||||
# Build cell list
|
|
||||||
$dslCells = @()
|
|
||||||
|
|
||||||
foreach ($cell in ($contentCells | Sort-Object { $_.Col })) {
|
|
||||||
$dslCell = [ordered]@{ col = $cell.Col + 1 }
|
|
||||||
|
|
||||||
# Span/rowspan from merge
|
|
||||||
$mk = "$globalRow,$($cell.Col)"
|
|
||||||
if ($mergeMap.ContainsKey($mk)) {
|
|
||||||
$m = $mergeMap[$mk]
|
|
||||||
if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 }
|
|
||||||
if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Style
|
|
||||||
$cellFmt = Get-Format $cell.FormatIdx
|
|
||||||
$cellStyleKey = Get-StyleKey $cellFmt
|
|
||||||
|
|
||||||
if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) {
|
|
||||||
# Inherits rowStyle
|
|
||||||
} else {
|
|
||||||
$sn = Get-StyleName $cell.FormatIdx
|
|
||||||
if ($sn -ne "default" -or -not $rowStyleName) {
|
|
||||||
$dslCell["style"] = $sn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Content
|
|
||||||
$fillType = if ($cellFmt) { $cellFmt.FillType } else { "" }
|
|
||||||
|
|
||||||
if ($cell.Param) {
|
|
||||||
$dslCell["param"] = $cell.Param
|
|
||||||
if ($cell.Detail) { $dslCell["detail"] = $cell.Detail }
|
|
||||||
} elseif ($fillType -eq "Template" -and $cell.Text) {
|
|
||||||
$dslCell["template"] = $cell.Text
|
|
||||||
} elseif ($cell.Text) {
|
|
||||||
$dslCell["text"] = $cell.Text
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslCells += $dslCell
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells }
|
|
||||||
$areaRows += $dslRow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compress consecutive empty rows ({}) into { empty = N }
|
|
||||||
$compressedRows = @()
|
|
||||||
$emptyRun = 0
|
|
||||||
foreach ($r in $areaRows) {
|
|
||||||
if ($r.Count -eq 0) {
|
|
||||||
$emptyRun++
|
|
||||||
} else {
|
|
||||||
if ($emptyRun -gt 0) {
|
|
||||||
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
|
|
||||||
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
|
|
||||||
$emptyRun = 0
|
|
||||||
}
|
|
||||||
$compressedRows += $r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($emptyRun -gt 0) {
|
|
||||||
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
|
|
||||||
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslAreas += [ordered]@{
|
|
||||||
name = $area.Name
|
|
||||||
rows = [array]$compressedRows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 13. Compress columnWidths ---
|
|
||||||
|
|
||||||
$compressedWidths = [ordered]@{}
|
|
||||||
if ($colWidthMap.Count -gt 0) {
|
|
||||||
$grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] }
|
|
||||||
foreach ($g in $grouped) {
|
|
||||||
$width = [int]$g.Name
|
|
||||||
$cols = @($g.Group | Sort-Object { [int]$_ })
|
|
||||||
|
|
||||||
$ranges = @()
|
|
||||||
$rangeStart = $cols[0]; $rangePrev = $cols[0]
|
|
||||||
|
|
||||||
for ($i = 1; $i -lt $cols.Count; $i++) {
|
|
||||||
if ([int]$cols[$i] -eq [int]$rangePrev + 1) {
|
|
||||||
$rangePrev = $cols[$i]
|
|
||||||
} else {
|
|
||||||
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
|
|
||||||
else { $ranges += "$rangeStart-$rangePrev" }
|
|
||||||
$rangeStart = $cols[$i]; $rangePrev = $cols[$i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
|
|
||||||
else { $ranges += "$rangeStart-$rangePrev" }
|
|
||||||
|
|
||||||
foreach ($range in $ranges) { $compressedWidths[$range] = $width }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 14. Build fonts output ---
|
|
||||||
|
|
||||||
$fontsOut = [ordered]@{}
|
|
||||||
foreach ($name in $fontDefs.Keys) {
|
|
||||||
$f = $fontDefs[$name]
|
|
||||||
$fOut = [ordered]@{ face = $f.Face; size = $f.Size }
|
|
||||||
if ($f.Bold) { $fOut["bold"] = $true }
|
|
||||||
if ($f.Italic) { $fOut["italic"] = $true }
|
|
||||||
if ($f.Underline) { $fOut["underline"] = $true }
|
|
||||||
if ($f.Strikeout) { $fOut["strikeout"] = $true }
|
|
||||||
$fontsOut[$name] = $fOut
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 15. Assemble result ---
|
|
||||||
|
|
||||||
$result = [ordered]@{
|
|
||||||
columns = $totalColumns
|
|
||||||
defaultWidth = $defaultWidth
|
|
||||||
}
|
|
||||||
if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths }
|
|
||||||
# Remove empty "default" style
|
|
||||||
if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) {
|
|
||||||
$styleDefs.Remove("default")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove unused styles
|
|
||||||
$usedStyles = @{}
|
|
||||||
foreach ($a in $dslAreas) {
|
|
||||||
foreach ($r in $a.rows) {
|
|
||||||
if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true }
|
|
||||||
if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) })
|
|
||||||
foreach ($s in $toRemove) { $styleDefs.Remove($s)
|
|
||||||
}
|
|
||||||
|
|
||||||
$result["fonts"] = $fontsOut
|
|
||||||
$result["styles"] = $styleDefs
|
|
||||||
$result["areas"] = [array]$dslAreas
|
|
||||||
|
|
||||||
# --- 16. Convert to JSON and fix Unicode ---
|
|
||||||
|
|
||||||
$json = $result | ConvertTo-Json -Depth 10
|
|
||||||
|
|
||||||
# PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8
|
|
||||||
$json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', {
|
|
||||||
param($m)
|
|
||||||
[char][int]("0x" + $m.Groups[1].Value)
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 17. Output ---
|
|
||||||
|
|
||||||
if ($OutputPath) {
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
[System.IO.File]::WriteAllText(
|
|
||||||
(Join-Path (Get-Location) $OutputPath),
|
|
||||||
$json,
|
|
||||||
$enc
|
|
||||||
)
|
|
||||||
Write-Host "[OK] Decompiled: $OutputPath"
|
|
||||||
} else {
|
|
||||||
Write-Output $json
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray
|
|
||||||
Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray
|
|
||||||
@@ -1,705 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import OrderedDict
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
# --- Namespace map ---
|
|
||||||
|
|
||||||
NSMAP = {
|
|
||||||
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
|
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
|
||||||
"v8ui": "http://v8.1c.ru/8.1/data/ui",
|
|
||||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
||||||
}
|
|
||||||
|
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
|
|
||||||
|
|
||||||
def find(node, xpath):
|
|
||||||
return node.find(xpath, NSMAP)
|
|
||||||
|
|
||||||
|
|
||||||
def findall(node, xpath):
|
|
||||||
return node.findall(xpath, NSMAP)
|
|
||||||
|
|
||||||
|
|
||||||
def text_of(node):
|
|
||||||
if node is not None and node.text:
|
|
||||||
return node.text
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def int_of(node, default=0):
|
|
||||||
if node is not None and node.text:
|
|
||||||
return int(node.text)
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
# --- Main ---
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
|
|
||||||
parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
|
|
||||||
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
template_path = args.TemplatePath
|
|
||||||
output_path = args.OutputPath
|
|
||||||
|
|
||||||
# --- 1. Load and parse XML ---
|
|
||||||
|
|
||||||
if not os.path.isfile(template_path):
|
|
||||||
print(f"File not found: {template_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(template_path, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# --- 2. Extract font palette ---
|
|
||||||
|
|
||||||
raw_fonts = []
|
|
||||||
for f_node in findall(root, "d:font"):
|
|
||||||
raw_fonts.append({
|
|
||||||
"Face": f_node.get("faceName", ""),
|
|
||||||
"Size": int(f_node.get("height", "0")),
|
|
||||||
"Bold": f_node.get("bold") == "true",
|
|
||||||
"Italic": f_node.get("italic") == "true",
|
|
||||||
"Underline": f_node.get("underline") == "true",
|
|
||||||
"Strikeout": f_node.get("strikeout") == "true",
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 3. Extract line palette ---
|
|
||||||
|
|
||||||
raw_lines = []
|
|
||||||
for l_node in findall(root, "d:line"):
|
|
||||||
raw_lines.append({"Width": int(l_node.get("width", "0"))})
|
|
||||||
|
|
||||||
# --- 4. Extract format palette ---
|
|
||||||
|
|
||||||
raw_formats = []
|
|
||||||
for fmt_node in findall(root, "d:format"):
|
|
||||||
fmt = {
|
|
||||||
"FontIdx": -1,
|
|
||||||
"LB": -1, "TB": -1, "RB": -1, "BB": -1,
|
|
||||||
"Width": 0, "Height": 0,
|
|
||||||
"HA": "", "VA": "",
|
|
||||||
"Wrap": False, "FillType": "", "DataFormat": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:font")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["FontIdx"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:leftBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["LB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:topBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["TB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:rightBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["RB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:bottomBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["BB"] = int(n.text)
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:width")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["Width"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:height")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["Height"] = int(n.text)
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:horizontalAlignment")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["HA"] = n.text
|
|
||||||
n = find(fmt_node, "d:verticalAlignment")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["VA"] = n.text
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:textPlacement")
|
|
||||||
if n is not None and n.text == "Wrap":
|
|
||||||
fmt["Wrap"] = True
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:fillType")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["FillType"] = n.text
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:format/v8:item/v8:content")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["DataFormat"] = n.text
|
|
||||||
|
|
||||||
raw_formats.append(fmt)
|
|
||||||
|
|
||||||
def get_format(idx):
|
|
||||||
if idx <= 0 or idx > len(raw_formats):
|
|
||||||
return None
|
|
||||||
return raw_formats[idx - 1]
|
|
||||||
|
|
||||||
# --- 5. Extract columns and default width ---
|
|
||||||
|
|
||||||
col_node = find(root, "d:columns")
|
|
||||||
total_columns = int_of(find(col_node, "d:size"))
|
|
||||||
|
|
||||||
col_format_indices = {}
|
|
||||||
for ci in findall(col_node, "d:columnsItem"):
|
|
||||||
col_idx = int_of(find(ci, "d:index"))
|
|
||||||
fmt_idx = int_of(find(ci, "d:column/d:formatIndex"))
|
|
||||||
col_format_indices[col_idx] = fmt_idx
|
|
||||||
|
|
||||||
default_fmt_idx = 0
|
|
||||||
n = find(root, "d:defaultFormatIndex")
|
|
||||||
if n is not None and n.text:
|
|
||||||
default_fmt_idx = int(n.text)
|
|
||||||
|
|
||||||
default_width = 10
|
|
||||||
if default_fmt_idx > 0:
|
|
||||||
def_fmt = get_format(default_fmt_idx)
|
|
||||||
if def_fmt and def_fmt["Width"] > 0:
|
|
||||||
default_width = def_fmt["Width"]
|
|
||||||
|
|
||||||
# Build column width map (1-based col -> width), only non-default
|
|
||||||
col_width_map = OrderedDict()
|
|
||||||
for col0 in sorted(col_format_indices.keys()):
|
|
||||||
fmt = get_format(col_format_indices[col0])
|
|
||||||
if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width:
|
|
||||||
col1 = str(col0 + 1)
|
|
||||||
col_width_map[col1] = fmt["Width"]
|
|
||||||
|
|
||||||
# --- 6. Extract merges ---
|
|
||||||
|
|
||||||
merge_map = {}
|
|
||||||
for m_node in findall(root, "d:merge"):
|
|
||||||
r = int_of(find(m_node, "d:r"))
|
|
||||||
c = int_of(find(m_node, "d:c"))
|
|
||||||
w = int_of(find(m_node, "d:w"))
|
|
||||||
h_node = find(m_node, "d:h")
|
|
||||||
h = int_of(h_node) if h_node is not None else 0
|
|
||||||
merge_map[f"{r},{c}"] = {"W": w, "H": h}
|
|
||||||
|
|
||||||
# --- 7. Extract named items ---
|
|
||||||
|
|
||||||
named_areas = []
|
|
||||||
for ni_node in findall(root, "d:namedItem"):
|
|
||||||
xsi_type = ni_node.get(f"{{{XSI_NS}}}type", "")
|
|
||||||
if xsi_type != "NamedItemCells":
|
|
||||||
continue
|
|
||||||
|
|
||||||
area_node = find(ni_node, "d:area")
|
|
||||||
area_type_node = find(area_node, "d:type")
|
|
||||||
area_type = text_of(area_type_node) or ""
|
|
||||||
if area_type != "Rows":
|
|
||||||
continue
|
|
||||||
|
|
||||||
named_areas.append({
|
|
||||||
"Name": text_of(find(ni_node, "d:name")) or "",
|
|
||||||
"BeginRow": int_of(find(area_node, "d:beginRow")),
|
|
||||||
"EndRow": int_of(find(area_node, "d:endRow")),
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 8. Extract rows ---
|
|
||||||
|
|
||||||
row_data = {}
|
|
||||||
for ri_node in findall(root, "d:rowsItem"):
|
|
||||||
row_idx = int_of(find(ri_node, "d:index"))
|
|
||||||
row_node = find(ri_node, "d:row")
|
|
||||||
|
|
||||||
index_to = row_idx
|
|
||||||
it_node = find(ri_node, "d:indexTo")
|
|
||||||
if it_node is not None and it_node.text:
|
|
||||||
index_to = int(it_node.text)
|
|
||||||
|
|
||||||
row_fmt_idx = 0
|
|
||||||
fmt_node = find(row_node, "d:formatIndex")
|
|
||||||
if fmt_node is not None and fmt_node.text:
|
|
||||||
row_fmt_idx = int(fmt_node.text)
|
|
||||||
|
|
||||||
is_empty = False
|
|
||||||
empty_node = find(row_node, "d:empty")
|
|
||||||
if empty_node is not None and empty_node.text == "true":
|
|
||||||
is_empty = True
|
|
||||||
|
|
||||||
cells = []
|
|
||||||
if not is_empty:
|
|
||||||
col = -1
|
|
||||||
for c_group in findall(row_node, "d:c"):
|
|
||||||
i_node = find(c_group, "d:i")
|
|
||||||
if i_node is not None and i_node.text:
|
|
||||||
col = int(i_node.text)
|
|
||||||
else:
|
|
||||||
col += 1
|
|
||||||
|
|
||||||
c_content = find(c_group, "d:c")
|
|
||||||
if c_content is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
cell_fmt_idx = 0
|
|
||||||
f_node = find(c_content, "d:f")
|
|
||||||
if f_node is not None and f_node.text:
|
|
||||||
cell_fmt_idx = int(f_node.text)
|
|
||||||
|
|
||||||
param = None
|
|
||||||
p_node = find(c_content, "d:parameter")
|
|
||||||
if p_node is not None and p_node.text:
|
|
||||||
param = p_node.text
|
|
||||||
|
|
||||||
detail = None
|
|
||||||
d_node = find(c_content, "d:detailParameter")
|
|
||||||
if d_node is not None and d_node.text:
|
|
||||||
detail = d_node.text
|
|
||||||
|
|
||||||
text = None
|
|
||||||
t_node = find(c_content, "d:tl/v8:item/v8:content")
|
|
||||||
if t_node is not None and t_node.text:
|
|
||||||
text = t_node.text
|
|
||||||
|
|
||||||
cells.append({
|
|
||||||
"Col": col,
|
|
||||||
"FormatIdx": cell_fmt_idx,
|
|
||||||
"Param": param,
|
|
||||||
"Detail": detail,
|
|
||||||
"Text": text,
|
|
||||||
})
|
|
||||||
|
|
||||||
for r in range(row_idx, index_to + 1):
|
|
||||||
row_data[r] = {
|
|
||||||
"FormatIdx": row_fmt_idx,
|
|
||||||
"Cells": cells,
|
|
||||||
"Empty": is_empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 9. Build style key (ignoring fillType) ---
|
|
||||||
|
|
||||||
def get_border_desc(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return {"Border": "none", "Thick": False}
|
|
||||||
|
|
||||||
lb = fmt["LB"] >= 0
|
|
||||||
tb = fmt["TB"] >= 0
|
|
||||||
rb = fmt["RB"] >= 0
|
|
||||||
bb = fmt["BB"] >= 0
|
|
||||||
|
|
||||||
if not lb and not tb and not rb and not bb:
|
|
||||||
return {"Border": "none", "Thick": False}
|
|
||||||
|
|
||||||
thick = False
|
|
||||||
for b_idx in [fmt["LB"], fmt["TB"], fmt["RB"], fmt["BB"]]:
|
|
||||||
if b_idx >= 0 and b_idx < len(raw_lines) and raw_lines[b_idx]["Width"] >= 2:
|
|
||||||
thick = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if lb and tb and rb and bb:
|
|
||||||
return {"Border": "all", "Thick": thick}
|
|
||||||
|
|
||||||
sides = []
|
|
||||||
if tb:
|
|
||||||
sides.append("top")
|
|
||||||
if bb:
|
|
||||||
sides.append("bottom")
|
|
||||||
if lb:
|
|
||||||
sides.append("left")
|
|
||||||
if rb:
|
|
||||||
sides.append("right")
|
|
||||||
|
|
||||||
return {"Border": ",".join(sides), "Thick": thick}
|
|
||||||
|
|
||||||
def get_style_key(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return "empty"
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
return f"f={fi}|b={bd['Border']}|bw={bd['Thick']}|ha={fmt['HA']}|va={fmt['VA']}|wr={fmt['Wrap']}|df={fmt['DataFormat']}"
|
|
||||||
|
|
||||||
# --- 10. Name fonts ---
|
|
||||||
|
|
||||||
font_names = {}
|
|
||||||
font_defs = OrderedDict()
|
|
||||||
|
|
||||||
if len(raw_fonts) > 0:
|
|
||||||
font_names[0] = "default"
|
|
||||||
font_defs["default"] = raw_fonts[0]
|
|
||||||
|
|
||||||
def get_font_key(f):
|
|
||||||
return f"{f['Face']}|{f['Size']}|{f['Bold']}|{f['Italic']}|{f['Underline']}|{f['Strikeout']}"
|
|
||||||
|
|
||||||
font_key_map = {}
|
|
||||||
if len(raw_fonts) > 0:
|
|
||||||
font_key_map[get_font_key(raw_fonts[0])] = "default"
|
|
||||||
|
|
||||||
for i in range(1, len(raw_fonts)):
|
|
||||||
f = raw_fonts[i]
|
|
||||||
df = raw_fonts[0]
|
|
||||||
|
|
||||||
# Dedup: if identical font already named, reuse
|
|
||||||
f_key = get_font_key(f)
|
|
||||||
if f_key in font_key_map:
|
|
||||||
font_names[i] = font_key_map[f_key]
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = None
|
|
||||||
|
|
||||||
if f["Face"] == df["Face"] and f["Size"] == df["Size"]:
|
|
||||||
if f["Bold"] and not df["Bold"] and not f["Italic"] and not f["Underline"] and not f["Strikeout"]:
|
|
||||||
name = "bold"
|
|
||||||
elif f["Italic"] and not df["Italic"] and not f["Bold"]:
|
|
||||||
name = "italic"
|
|
||||||
elif f["Underline"] and not df["Underline"] and not f["Bold"] and not f["Italic"]:
|
|
||||||
name = "underline"
|
|
||||||
elif f["Face"] == df["Face"] and f["Size"] > df["Size"] and f["Bold"]:
|
|
||||||
name = "header"
|
|
||||||
elif f["Face"] == df["Face"] and f["Size"] < df["Size"]:
|
|
||||||
name = "small"
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
parts = []
|
|
||||||
if f["Face"] and f["Face"] != df["Face"]:
|
|
||||||
parts.append(f["Face"].lower())
|
|
||||||
parts.append(str(f["Size"]))
|
|
||||||
if f["Bold"]:
|
|
||||||
parts.append("bold")
|
|
||||||
if f["Italic"]:
|
|
||||||
parts.append("italic")
|
|
||||||
if f["Underline"]:
|
|
||||||
parts.append("underline")
|
|
||||||
if f["Strikeout"]:
|
|
||||||
parts.append("strikeout")
|
|
||||||
name = "-".join(parts)
|
|
||||||
|
|
||||||
base_name = name
|
|
||||||
suffix = 2
|
|
||||||
while name in font_defs:
|
|
||||||
name = f"{base_name}{suffix}"
|
|
||||||
suffix += 1
|
|
||||||
|
|
||||||
font_names[i] = name
|
|
||||||
font_defs[name] = f
|
|
||||||
font_key_map[f_key] = name
|
|
||||||
|
|
||||||
# --- 11. Collect and name styles ---
|
|
||||||
|
|
||||||
style_keys = OrderedDict()
|
|
||||||
format_to_style_key = {}
|
|
||||||
|
|
||||||
for rd in row_data.values():
|
|
||||||
for cell in rd["Cells"]:
|
|
||||||
fmt = get_format(cell["FormatIdx"])
|
|
||||||
if not fmt:
|
|
||||||
continue
|
|
||||||
key = get_style_key(fmt)
|
|
||||||
if key not in style_keys:
|
|
||||||
style_keys[key] = fmt
|
|
||||||
format_to_style_key[cell["FormatIdx"]] = key
|
|
||||||
|
|
||||||
def name_style(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return "default"
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
if fi in font_names and font_names[fi] != "default":
|
|
||||||
parts.append(font_names[fi])
|
|
||||||
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
if bd["Border"] != "none":
|
|
||||||
if bd["Border"] == "all":
|
|
||||||
parts.append("bordered")
|
|
||||||
else:
|
|
||||||
parts.append(f"border-{bd['Border']}")
|
|
||||||
|
|
||||||
if fmt["HA"] == "Center":
|
|
||||||
parts.append("center")
|
|
||||||
elif fmt["HA"] == "Right":
|
|
||||||
parts.append("right")
|
|
||||||
if fmt["VA"] == "Center":
|
|
||||||
parts.append("vcenter")
|
|
||||||
elif fmt["VA"] == "Top":
|
|
||||||
parts.append("vtop")
|
|
||||||
if fmt["Wrap"]:
|
|
||||||
parts.append("wrap")
|
|
||||||
if fmt["DataFormat"]:
|
|
||||||
parts.append("fmt")
|
|
||||||
|
|
||||||
if len(parts) == 0:
|
|
||||||
return "default"
|
|
||||||
return "-".join(parts)
|
|
||||||
|
|
||||||
style_names = OrderedDict()
|
|
||||||
style_defs = OrderedDict()
|
|
||||||
|
|
||||||
for key in style_keys:
|
|
||||||
fmt = style_keys[key]
|
|
||||||
name = name_style(fmt)
|
|
||||||
|
|
||||||
base_name = name
|
|
||||||
suffix = 2
|
|
||||||
while name in style_defs:
|
|
||||||
name = f"{base_name}{suffix}"
|
|
||||||
suffix += 1
|
|
||||||
|
|
||||||
style_names[key] = name
|
|
||||||
|
|
||||||
s_def = OrderedDict()
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
if fi in font_names and font_names[fi] != "default":
|
|
||||||
s_def["font"] = font_names[fi]
|
|
||||||
if fmt["HA"]:
|
|
||||||
a_map = {"Left": "left", "Center": "center", "Right": "right"}
|
|
||||||
a = a_map.get(fmt["HA"])
|
|
||||||
if a:
|
|
||||||
s_def["align"] = a
|
|
||||||
if fmt["VA"]:
|
|
||||||
va_map = {"Top": "top", "Center": "center"}
|
|
||||||
a = va_map.get(fmt["VA"])
|
|
||||||
if a:
|
|
||||||
s_def["valign"] = a
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
if bd["Border"] != "none":
|
|
||||||
s_def["border"] = bd["Border"]
|
|
||||||
if bd["Thick"]:
|
|
||||||
s_def["borderWidth"] = "thick"
|
|
||||||
if fmt["Wrap"]:
|
|
||||||
s_def["wrap"] = True
|
|
||||||
if fmt["DataFormat"]:
|
|
||||||
s_def["format"] = fmt["DataFormat"]
|
|
||||||
|
|
||||||
style_defs[name] = s_def
|
|
||||||
|
|
||||||
def get_style_name(fmt_idx):
|
|
||||||
key = format_to_style_key.get(fmt_idx)
|
|
||||||
if key and key in style_names:
|
|
||||||
return style_names[key]
|
|
||||||
return "default"
|
|
||||||
|
|
||||||
# --- 12. Build areas ---
|
|
||||||
|
|
||||||
dsl_areas = []
|
|
||||||
|
|
||||||
for area in named_areas:
|
|
||||||
area_rows = []
|
|
||||||
|
|
||||||
for global_row in range(area["BeginRow"], area["EndRow"] + 1):
|
|
||||||
rd = row_data.get(global_row)
|
|
||||||
|
|
||||||
if not rd or rd["Empty"]:
|
|
||||||
area_rows.append(OrderedDict())
|
|
||||||
continue
|
|
||||||
|
|
||||||
dsl_row = OrderedDict()
|
|
||||||
|
|
||||||
# Row height
|
|
||||||
if rd["FormatIdx"] > 0:
|
|
||||||
row_fmt = get_format(rd["FormatIdx"])
|
|
||||||
if row_fmt and row_fmt["Height"] > 0:
|
|
||||||
dsl_row["height"] = row_fmt["Height"]
|
|
||||||
|
|
||||||
# Separate content cells from gap-fill cells
|
|
||||||
content_cells = []
|
|
||||||
gap_cells = []
|
|
||||||
|
|
||||||
for cell in rd["Cells"]:
|
|
||||||
has_content = cell["Param"] or cell["Text"]
|
|
||||||
has_merge = f"{global_row},{cell['Col']}" in merge_map
|
|
||||||
|
|
||||||
if has_content or has_merge:
|
|
||||||
content_cells.append(cell)
|
|
||||||
else:
|
|
||||||
gap_cells.append(cell)
|
|
||||||
|
|
||||||
# Detect rowStyle
|
|
||||||
row_style_name = None
|
|
||||||
row_style_key = None
|
|
||||||
|
|
||||||
if len(gap_cells) > 0:
|
|
||||||
gap_keys = {}
|
|
||||||
for gc in gap_cells:
|
|
||||||
fmt = get_format(gc["FormatIdx"])
|
|
||||||
gap_keys[get_style_key(fmt)] = True
|
|
||||||
|
|
||||||
if len(gap_keys) == 1:
|
|
||||||
row_style_key = list(gap_keys.keys())[0]
|
|
||||||
if row_style_key in style_names:
|
|
||||||
row_style_name = style_names[row_style_key]
|
|
||||||
|
|
||||||
if row_style_name and row_style_name != "default":
|
|
||||||
dsl_row["rowStyle"] = row_style_name
|
|
||||||
|
|
||||||
# Build cell list
|
|
||||||
dsl_cells = []
|
|
||||||
|
|
||||||
for cell in sorted(content_cells, key=lambda c: c["Col"]):
|
|
||||||
dsl_cell = OrderedDict()
|
|
||||||
dsl_cell["col"] = cell["Col"] + 1
|
|
||||||
|
|
||||||
# Span/rowspan from merge
|
|
||||||
mk = f"{global_row},{cell['Col']}"
|
|
||||||
if mk in merge_map:
|
|
||||||
m = merge_map[mk]
|
|
||||||
if m["W"] > 0:
|
|
||||||
dsl_cell["span"] = m["W"] + 1
|
|
||||||
if m["H"] > 0:
|
|
||||||
dsl_cell["rowspan"] = m["H"] + 1
|
|
||||||
|
|
||||||
# Style
|
|
||||||
cell_fmt = get_format(cell["FormatIdx"])
|
|
||||||
cell_style_key = get_style_key(cell_fmt)
|
|
||||||
|
|
||||||
if row_style_key and cell_style_key == row_style_key:
|
|
||||||
pass # Inherits rowStyle
|
|
||||||
else:
|
|
||||||
sn = get_style_name(cell["FormatIdx"])
|
|
||||||
if sn != "default" or not row_style_name:
|
|
||||||
dsl_cell["style"] = sn
|
|
||||||
|
|
||||||
# Content
|
|
||||||
fill_type = cell_fmt["FillType"] if cell_fmt else ""
|
|
||||||
|
|
||||||
if cell["Param"]:
|
|
||||||
dsl_cell["param"] = cell["Param"]
|
|
||||||
if cell["Detail"]:
|
|
||||||
dsl_cell["detail"] = cell["Detail"]
|
|
||||||
elif fill_type == "Template" and cell["Text"]:
|
|
||||||
dsl_cell["template"] = cell["Text"]
|
|
||||||
elif cell["Text"]:
|
|
||||||
dsl_cell["text"] = cell["Text"]
|
|
||||||
|
|
||||||
dsl_cells.append(dsl_cell)
|
|
||||||
|
|
||||||
if len(dsl_cells) > 0:
|
|
||||||
dsl_row["cells"] = dsl_cells
|
|
||||||
area_rows.append(dsl_row)
|
|
||||||
|
|
||||||
# Compress consecutive empty rows ({}) into { empty = N }
|
|
||||||
compressed_rows = []
|
|
||||||
empty_run = 0
|
|
||||||
for r in area_rows:
|
|
||||||
if len(r) == 0:
|
|
||||||
empty_run += 1
|
|
||||||
else:
|
|
||||||
if empty_run > 0:
|
|
||||||
if empty_run == 1:
|
|
||||||
compressed_rows.append(OrderedDict())
|
|
||||||
else:
|
|
||||||
compressed_rows.append(OrderedDict([("empty", empty_run)]))
|
|
||||||
empty_run = 0
|
|
||||||
compressed_rows.append(r)
|
|
||||||
if empty_run > 0:
|
|
||||||
if empty_run == 1:
|
|
||||||
compressed_rows.append(OrderedDict())
|
|
||||||
else:
|
|
||||||
compressed_rows.append(OrderedDict([("empty", empty_run)]))
|
|
||||||
|
|
||||||
dsl_areas.append(OrderedDict([
|
|
||||||
("name", area["Name"]),
|
|
||||||
("rows", compressed_rows),
|
|
||||||
]))
|
|
||||||
|
|
||||||
# --- 13. Compress columnWidths ---
|
|
||||||
|
|
||||||
compressed_widths = OrderedDict()
|
|
||||||
if len(col_width_map) > 0:
|
|
||||||
# Group columns by width
|
|
||||||
width_to_cols = {}
|
|
||||||
for col_str, width in col_width_map.items():
|
|
||||||
width_to_cols.setdefault(width, []).append(col_str)
|
|
||||||
|
|
||||||
for width, cols in width_to_cols.items():
|
|
||||||
cols_sorted = sorted(cols, key=lambda x: int(x))
|
|
||||||
|
|
||||||
ranges = []
|
|
||||||
range_start = cols_sorted[0]
|
|
||||||
range_prev = cols_sorted[0]
|
|
||||||
|
|
||||||
for i in range(1, len(cols_sorted)):
|
|
||||||
if int(cols_sorted[i]) == int(range_prev) + 1:
|
|
||||||
range_prev = cols_sorted[i]
|
|
||||||
else:
|
|
||||||
if range_start == range_prev:
|
|
||||||
ranges.append(range_start)
|
|
||||||
else:
|
|
||||||
ranges.append(f"{range_start}-{range_prev}")
|
|
||||||
range_start = cols_sorted[i]
|
|
||||||
range_prev = cols_sorted[i]
|
|
||||||
|
|
||||||
if range_start == range_prev:
|
|
||||||
ranges.append(range_start)
|
|
||||||
else:
|
|
||||||
ranges.append(f"{range_start}-{range_prev}")
|
|
||||||
|
|
||||||
for rng in ranges:
|
|
||||||
compressed_widths[rng] = width
|
|
||||||
|
|
||||||
# --- 14. Build fonts output ---
|
|
||||||
|
|
||||||
fonts_out = OrderedDict()
|
|
||||||
for name, f in font_defs.items():
|
|
||||||
f_out = OrderedDict()
|
|
||||||
f_out["face"] = f["Face"]
|
|
||||||
f_out["size"] = f["Size"]
|
|
||||||
if f["Bold"]:
|
|
||||||
f_out["bold"] = True
|
|
||||||
if f["Italic"]:
|
|
||||||
f_out["italic"] = True
|
|
||||||
if f["Underline"]:
|
|
||||||
f_out["underline"] = True
|
|
||||||
if f["Strikeout"]:
|
|
||||||
f_out["strikeout"] = True
|
|
||||||
fonts_out[name] = f_out
|
|
||||||
|
|
||||||
# --- 15. Assemble result ---
|
|
||||||
|
|
||||||
result = OrderedDict()
|
|
||||||
result["columns"] = total_columns
|
|
||||||
result["defaultWidth"] = default_width
|
|
||||||
if len(compressed_widths) > 0:
|
|
||||||
result["columnWidths"] = compressed_widths
|
|
||||||
|
|
||||||
# Remove empty "default" style
|
|
||||||
if "default" in style_defs and len(style_defs["default"]) == 0:
|
|
||||||
del style_defs["default"]
|
|
||||||
|
|
||||||
# Remove unused styles
|
|
||||||
used_styles = set()
|
|
||||||
for a in dsl_areas:
|
|
||||||
for r in a["rows"]:
|
|
||||||
if "rowStyle" in r:
|
|
||||||
used_styles.add(r["rowStyle"])
|
|
||||||
if "cells" in r:
|
|
||||||
for c in r["cells"]:
|
|
||||||
if "style" in c:
|
|
||||||
used_styles.add(c["style"])
|
|
||||||
to_remove = [s for s in style_defs if s not in used_styles]
|
|
||||||
for s in to_remove:
|
|
||||||
del style_defs[s]
|
|
||||||
|
|
||||||
result["fonts"] = fonts_out
|
|
||||||
result["styles"] = style_defs
|
|
||||||
result["areas"] = dsl_areas
|
|
||||||
|
|
||||||
# --- 16. Convert to JSON ---
|
|
||||||
|
|
||||||
json_str = json.dumps(result, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
# --- 17. Output ---
|
|
||||||
|
|
||||||
if output_path:
|
|
||||||
abs_path = os.path.join(os.getcwd(), output_path) if not os.path.isabs(output_path) else output_path
|
|
||||||
with open(abs_path, "w", encoding="utf-8") as fh:
|
|
||||||
fh.write(json_str)
|
|
||||||
print(f"[OK] Decompiled: {output_path}")
|
|
||||||
else:
|
|
||||||
print(json_str)
|
|
||||||
|
|
||||||
print(f" Areas: {len(named_areas)}, Rows: {len(row_data)}, Columns: {total_columns}", file=sys.stderr)
|
|
||||||
print(f" Fonts: {len(font_defs)}, Styles: {len(style_defs)}, Merges: {len(merge_map)}", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
# Role DSL — полная справка
|
|
||||||
|
|
||||||
Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md).
|
|
||||||
|
|
||||||
## Структура верхнего уровня
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ИмяРоли",
|
|
||||||
"synonym": "Отображаемое имя роли",
|
|
||||||
"comment": "",
|
|
||||||
"setForNewObjects": false,
|
|
||||||
"setForAttributesByDefault": true,
|
|
||||||
"independentRightsOfChildObjects": false,
|
|
||||||
"objects": [ ... ],
|
|
||||||
"templates": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `name` — программное имя роли (обязательно)
|
|
||||||
- `synonym` — отображаемое имя (по умолчанию = name)
|
|
||||||
- `comment` — комментарий (по умолчанию пусто)
|
|
||||||
- Глобальные флаги — по умолчанию `false`, `true`, `false`
|
|
||||||
|
|
||||||
## Объекты: два формата
|
|
||||||
|
|
||||||
Массив `objects` принимает строки (shorthand) и объекты (полная форма).
|
|
||||||
|
|
||||||
### Строковый shorthand
|
|
||||||
|
|
||||||
```
|
|
||||||
"ОбъектМетаданных: @пресет"
|
|
||||||
"ОбъектМетаданных: Право1, Право2"
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
```json
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: @view",
|
|
||||||
"Document.Реализация: @edit",
|
|
||||||
"InformationRegister.Цены: Read, Update",
|
|
||||||
"DataProcessor.Загрузка: @view"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Объектная форма (для RLS и переопределений)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Document.Реализация",
|
|
||||||
"preset": "view",
|
|
||||||
"rights": { "Delete": false },
|
|
||||||
"rls": { "Read": "#ДляОбъекта(\"\")" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `preset` — базовый набор прав (`"view"`, `"edit"`)
|
|
||||||
- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]`
|
|
||||||
- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}`
|
|
||||||
|
|
||||||
## Пресеты — подробные таблицы
|
|
||||||
|
|
||||||
Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`.
|
|
||||||
|
|
||||||
### `@view` — просмотр
|
|
||||||
|
|
||||||
| Тип объекта | Права |
|
|
||||||
|-------------|-------|
|
|
||||||
| Catalog, ExchangePlan, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task | Read, View, InputByString |
|
|
||||||
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, Constant, DocumentJournal | Read, View |
|
|
||||||
| Sequence | Read |
|
|
||||||
| CommonForm, CommonCommand, Subsystem, FilterCriterion, CommonAttribute | View |
|
|
||||||
| DataProcessor, Report | Use, View |
|
|
||||||
| SessionParameter | Get |
|
|
||||||
| Configuration | ThinClient, WebClient, Output, SaveUserData, MainWindowModeNormal |
|
|
||||||
|
|
||||||
### `@edit` — полное редактирование
|
|
||||||
|
|
||||||
| Тип объекта | Права |
|
|
||||||
|-------------|-------|
|
|
||||||
| Catalog, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark |
|
|
||||||
| Document | Read, Insert, Update, Delete, View, Edit, InputByString, Posting, UndoPosting, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted |
|
|
||||||
| BusinessProcess | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveStart |
|
|
||||||
| Task | Read, Insert, Update, Delete, View, Edit, InputByString, Execute, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveExecute |
|
|
||||||
| InformationRegister, AccumulationRegister, AccountingRegister, Constant | Read, Update, View, Edit |
|
|
||||||
| DocumentJournal | Read, View |
|
|
||||||
| Sequence | Read, Update |
|
|
||||||
| SessionParameter | Get, Set |
|
|
||||||
| CommonAttribute | View, Edit |
|
|
||||||
|
|
||||||
Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`.
|
|
||||||
|
|
||||||
Если пресет не определён для типа объекта — предупреждение с подсказкой доступных.
|
|
||||||
|
|
||||||
## Русские синонимы
|
|
||||||
|
|
||||||
Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает.
|
|
||||||
|
|
||||||
### Типы объектов
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Справочник` | Catalog |
|
|
||||||
| `Документ` | Document |
|
|
||||||
| `РегистрСведений` | InformationRegister |
|
|
||||||
| `РегистрНакопления` | AccumulationRegister |
|
|
||||||
| `РегистрБухгалтерии` | AccountingRegister |
|
|
||||||
| `РегистрРасчета` | CalculationRegister |
|
|
||||||
| `Константа` | Constant |
|
|
||||||
| `ПланСчетов` | ChartOfAccounts |
|
|
||||||
| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes |
|
|
||||||
| `ПланВидовРасчета` | ChartOfCalculationTypes |
|
|
||||||
| `ПланОбмена` | ExchangePlan |
|
|
||||||
| `БизнесПроцесс` | BusinessProcess |
|
|
||||||
| `Задача` | Task |
|
|
||||||
| `Обработка` | DataProcessor |
|
|
||||||
| `Отчет` | Report |
|
|
||||||
| `ОбщаяФорма` | CommonForm |
|
|
||||||
| `ОбщаяКоманда` | CommonCommand |
|
|
||||||
| `Подсистема` | Subsystem |
|
|
||||||
| `КритерийОтбора` | FilterCriterion |
|
|
||||||
| `ЖурналДокументов` | DocumentJournal |
|
|
||||||
| `Последовательность` | Sequence |
|
|
||||||
| `ВебСервис` | WebService |
|
|
||||||
| `HTTPСервис` | HTTPService |
|
|
||||||
| `СервисИнтеграции` | IntegrationService |
|
|
||||||
| `ПараметрСеанса` | SessionParameter |
|
|
||||||
| `ОбщийРеквизит` | CommonAttribute |
|
|
||||||
| `Конфигурация` | Configuration |
|
|
||||||
| `Перечисление` | Enum |
|
|
||||||
|
|
||||||
### Вложенные типы
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Реквизит` | Attribute |
|
|
||||||
| `СтандартныйРеквизит` | StandardAttribute |
|
|
||||||
| `ТабличнаяЧасть` | TabularSection |
|
|
||||||
| `Измерение` | Dimension |
|
|
||||||
| `Ресурс` | Resource |
|
|
||||||
| `Команда` | Command |
|
|
||||||
| `РеквизитАдресации` | AddressingAttribute |
|
|
||||||
|
|
||||||
### Права (основные)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Чтение` | Read |
|
|
||||||
| `Добавление` | Insert |
|
|
||||||
| `Изменение` | Update |
|
|
||||||
| `Удаление` | Delete |
|
|
||||||
| `Просмотр` | View |
|
|
||||||
| `Редактирование` | Edit |
|
|
||||||
| `ВводПоСтроке` | InputByString |
|
|
||||||
| `Проведение` | Posting |
|
|
||||||
| `ОтменаПроведения` | UndoPosting |
|
|
||||||
| `Использование` | Use |
|
|
||||||
| `Получение` | Get |
|
|
||||||
| `Установка` | Set |
|
|
||||||
| `Старт` | Start |
|
|
||||||
| `Выполнение` | Execute |
|
|
||||||
| `УправлениеИтогами` | TotalsControl |
|
|
||||||
|
|
||||||
### Права (интерактивные)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `ИнтерактивноеДобавление` | InteractiveInsert |
|
|
||||||
| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark |
|
|
||||||
| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark |
|
|
||||||
| `ИнтерактивноеУдаление` | InteractiveDelete |
|
|
||||||
| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked |
|
|
||||||
| `ИнтерактивноеПроведение` | InteractivePosting |
|
|
||||||
| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular |
|
|
||||||
| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting |
|
|
||||||
| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted |
|
|
||||||
| `ИнтерактивныйСтарт` | InteractiveStart |
|
|
||||||
| `ИнтерактивнаяАктивация` | InteractiveActivate |
|
|
||||||
| `ИнтерактивноеВыполнение` | InteractiveExecute |
|
|
||||||
|
|
||||||
### Права (конфигурация)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Администрирование` | Administration |
|
|
||||||
| `АдминистрированиеДанных` | DataAdministration |
|
|
||||||
| `ТонкийКлиент` | ThinClient |
|
|
||||||
| `ТолстыйКлиент` | ThickClient |
|
|
||||||
| `ВебКлиент` | WebClient |
|
|
||||||
| `МобильныйКлиент` | MobileClient |
|
|
||||||
| `ВнешнееСоединение` | ExternalConnection |
|
|
||||||
| `Вывод` | Output |
|
|
||||||
| `СохранениеДанныхПользователя` | SaveUserData |
|
|
||||||
|
|
||||||
## Типы объектов без прав в ролях
|
|
||||||
|
|
||||||
Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`):
|
|
||||||
|
|
||||||
| Тип | Причина |
|
|
||||||
|-----|---------|
|
|
||||||
| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно |
|
|
||||||
| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли |
|
|
||||||
| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав |
|
|
||||||
| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав |
|
|
||||||
| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав |
|
|
||||||
| Language (Язык) | Конфигурационный элемент |
|
|
||||||
| FunctionalOption (ФункциональнаяОпция) | Не объект прав |
|
|
||||||
| FunctionalOptionsParameter | Не объект прав |
|
|
||||||
| EventSubscription (ПодпискаНаСобытие) | Не объект прав |
|
|
||||||
| ScheduledJob (РегламентноеЗадание) | Не объект прав |
|
|
||||||
| StyleItem (ЭлементСтиля) | Ресурс оформления |
|
|
||||||
|
|
||||||
## Шаблоны ограничений (RLS templates)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "ДляОбъекта(Модификатор)",
|
|
||||||
"condition": "// текст шаблона\nГДЕ 1=1\n&Модификатор"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `&` в условии автоматически экранируется в `&` в XML
|
|
||||||
- Ссылка на шаблон в `rls`: `"#ИмяШаблона(\"параметры\")"` — начинается с `#`
|
|
||||||
- Параметры шаблона можно передавать пустыми: `#ДляОбъекта("")`
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
### 1. Простая роль (только пресеты)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ЧтениеНоменклатуры",
|
|
||||||
"synonym": "Чтение номенклатуры",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: @view",
|
|
||||||
"Catalog.Контрагенты: @view",
|
|
||||||
"DataProcessor.Загрузка: @view"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Роль для регламентного задания
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ОбновлениеЦен",
|
|
||||||
"synonym": "Обновление цен номенклатуры",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: Read",
|
|
||||||
"Catalog.Валюты: Read",
|
|
||||||
"InformationRegister.ЦеныНоменклатуры: Read, Update",
|
|
||||||
"Constant.ОсновнаяВалюта: Read"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Роль с RLS
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ЧтениеДокументовПоОрганизации",
|
|
||||||
"synonym": "Чтение документов (ограничение по организации)",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Организации: @view",
|
|
||||||
{
|
|
||||||
"name": "Document.РеализацияТоваровУслуг",
|
|
||||||
"preset": "view",
|
|
||||||
"rls": {
|
|
||||||
"Read": "#ДляОбъекта(\"\")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "ДляОбъекта(Модификатор)",
|
|
||||||
"condition": "ГДЕ Организация = &ТекущаяОрганизация"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Роль с русскими синонимами
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ПросмотрДанных",
|
|
||||||
"synonym": "Просмотр данных",
|
|
||||||
"objects": [
|
|
||||||
"Справочник.Контрагенты: @view",
|
|
||||||
"Документ.Реализация: Чтение, Просмотр",
|
|
||||||
"РегистрСведений.Цены: @edit",
|
|
||||||
"Обработка.ЗагрузкаДанных: @view"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Роль с переопределением прав из пресета
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ОграниченноеРедактирование",
|
|
||||||
"synonym": "Редактирование без удаления",
|
|
||||||
"objects": [
|
|
||||||
{
|
|
||||||
"name": "Catalog.Контрагенты",
|
|
||||||
"preset": "edit",
|
|
||||||
"rights": { "Delete": false }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,748 +0,0 @@
|
|||||||
# role-compile v1.5 — Compile 1C role from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$JsonPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$OutputDir
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $JsonPath)) {
|
|
||||||
Write-Error "File not found: $JsonPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
|
||||||
$def = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $def.name) {
|
|
||||||
Write-Error "JSON must have 'name' field (role programmatic name)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$roleName = "$($def.name)"
|
|
||||||
$synonym = if ($def.synonym) { "$($def.synonym)" } else { $roleName }
|
|
||||||
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
|
|
||||||
|
|
||||||
# --- 2. XML helpers ---
|
|
||||||
|
|
||||||
$script:xmlBuf = $null
|
|
||||||
|
|
||||||
function X {
|
|
||||||
param([string]$text)
|
|
||||||
$script:xmlBuf.AppendLine($text) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Esc-Xml {
|
|
||||||
param([string]$s)
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Russian synonyms → canonical English names ---
|
|
||||||
|
|
||||||
$script:typeAliases = @{
|
|
||||||
"Справочник" = "Catalog"
|
|
||||||
"Документ" = "Document"
|
|
||||||
"РегистрСведений" = "InformationRegister"
|
|
||||||
"РегистрНакопления" = "AccumulationRegister"
|
|
||||||
"РегистрБухгалтерии" = "AccountingRegister"
|
|
||||||
"РегистрРасчета" = "CalculationRegister"
|
|
||||||
"Константа" = "Constant"
|
|
||||||
"ПланСчетов" = "ChartOfAccounts"
|
|
||||||
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
|
|
||||||
"ПланВидовРасчета" = "ChartOfCalculationTypes"
|
|
||||||
"ПланОбмена" = "ExchangePlan"
|
|
||||||
"БизнесПроцесс" = "BusinessProcess"
|
|
||||||
"Задача" = "Task"
|
|
||||||
"Обработка" = "DataProcessor"
|
|
||||||
"Отчет" = "Report"
|
|
||||||
"ОбщаяФорма" = "CommonForm"
|
|
||||||
"ОбщаяКоманда" = "CommonCommand"
|
|
||||||
"Подсистема" = "Subsystem"
|
|
||||||
"КритерийОтбора" = "FilterCriterion"
|
|
||||||
"ЖурналДокументов" = "DocumentJournal"
|
|
||||||
"Последовательность" = "Sequence"
|
|
||||||
"ВебСервис" = "WebService"
|
|
||||||
"HTTPСервис" = "HTTPService"
|
|
||||||
"СервисИнтеграции" = "IntegrationService"
|
|
||||||
"ПараметрСеанса" = "SessionParameter"
|
|
||||||
"ОбщийРеквизит" = "CommonAttribute"
|
|
||||||
"Конфигурация" = "Configuration"
|
|
||||||
"Перечисление" = "Enum"
|
|
||||||
# Nested
|
|
||||||
"Реквизит" = "Attribute"
|
|
||||||
"СтандартныйРеквизит" = "StandardAttribute"
|
|
||||||
"ТабличнаяЧасть" = "TabularSection"
|
|
||||||
"Измерение" = "Dimension"
|
|
||||||
"Ресурс" = "Resource"
|
|
||||||
"Команда" = "Command"
|
|
||||||
"РеквизитАдресации" = "AddressingAttribute"
|
|
||||||
}
|
|
||||||
|
|
||||||
$script:rightAliases = @{
|
|
||||||
"Чтение" = "Read"
|
|
||||||
"Добавление" = "Insert"
|
|
||||||
"Изменение" = "Update"
|
|
||||||
"Удаление" = "Delete"
|
|
||||||
"Просмотр" = "View"
|
|
||||||
"Редактирование" = "Edit"
|
|
||||||
"ВводПоСтроке" = "InputByString"
|
|
||||||
"Проведение" = "Posting"
|
|
||||||
"ОтменаПроведения" = "UndoPosting"
|
|
||||||
"ИнтерактивноеДобавление" = "InteractiveInsert"
|
|
||||||
"ИнтерактивнаяПометкаУдаления" = "InteractiveSetDeletionMark"
|
|
||||||
"ИнтерактивноеСнятиеПометкиУдаления" = "InteractiveClearDeletionMark"
|
|
||||||
"ИнтерактивноеУдаление" = "InteractiveDelete"
|
|
||||||
"ИнтерактивноеУдалениеПомеченных" = "InteractiveDeleteMarked"
|
|
||||||
"ИнтерактивноеПроведение" = "InteractivePosting"
|
|
||||||
"ИнтерактивноеПроведениеНеоперативное" = "InteractivePostingRegular"
|
|
||||||
"ИнтерактивнаяОтменаПроведения" = "InteractiveUndoPosting"
|
|
||||||
"ИнтерактивноеИзменениеПроведенных" = "InteractiveChangeOfPosted"
|
|
||||||
"Использование" = "Use"
|
|
||||||
"Получение" = "Get"
|
|
||||||
"Установка" = "Set"
|
|
||||||
"Старт" = "Start"
|
|
||||||
"ИнтерактивныйСтарт" = "InteractiveStart"
|
|
||||||
"ИнтерактивнаяАктивация" = "InteractiveActivate"
|
|
||||||
"Выполнение" = "Execute"
|
|
||||||
"ИнтерактивноеВыполнение" = "InteractiveExecute"
|
|
||||||
"УправлениеИтогами" = "TotalsControl"
|
|
||||||
"Администрирование" = "Administration"
|
|
||||||
"АдминистрированиеДанных" = "DataAdministration"
|
|
||||||
"ТонкийКлиент" = "ThinClient"
|
|
||||||
"ВебКлиент" = "WebClient"
|
|
||||||
"ТолстыйКлиент" = "ThickClient"
|
|
||||||
"ВнешнееСоединение" = "ExternalConnection"
|
|
||||||
"Вывод" = "Output"
|
|
||||||
"СохранениеДанныхПользователя" = "SaveUserData"
|
|
||||||
"МобильныйКлиент" = "MobileClient"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Translate Russian object name to English (e.g. "Справочник.Контрагенты" → "Catalog.Контрагенты")
|
|
||||||
function Translate-ObjectName {
|
|
||||||
param([string]$name)
|
|
||||||
$parts = $name.Split(".")
|
|
||||||
$result = @()
|
|
||||||
foreach ($p in $parts) {
|
|
||||||
if ($script:typeAliases.ContainsKey($p)) {
|
|
||||||
$result += $script:typeAliases[$p]
|
|
||||||
} else {
|
|
||||||
$result += $p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $result -join "."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Translate Russian right name to English (e.g. "Чтение" → "Read")
|
|
||||||
function Translate-RightName {
|
|
||||||
param([string]$name)
|
|
||||||
if ($script:rightAliases.ContainsKey($name)) {
|
|
||||||
return $script:rightAliases[$name]
|
|
||||||
}
|
|
||||||
return $name
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Known rights per object type (source: docs/1c-role-spec.md) ---
|
|
||||||
|
|
||||||
$script:knownRights = @{
|
|
||||||
"Configuration" = @(
|
|
||||||
"Administration","DataAdministration","UpdateDataBaseConfiguration",
|
|
||||||
"ConfigurationExtensionsAdministration","ActiveUsers","EventLog","ExclusiveMode",
|
|
||||||
"ThinClient","ThickClient","WebClient","MobileClient","ExternalConnection",
|
|
||||||
"Automation","Output","SaveUserData","TechnicalSpecialistMode",
|
|
||||||
"InteractiveOpenExtDataProcessors","InteractiveOpenExtReports",
|
|
||||||
"AnalyticsSystemClient","CollaborationSystemInfoBaseRegistration",
|
|
||||||
"MainWindowModeNormal","MainWindowModeWorkplace",
|
|
||||||
"MainWindowModeEmbeddedWorkplace","MainWindowModeFullscreenWorkplace","MainWindowModeKiosk"
|
|
||||||
)
|
|
||||||
"Catalog" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"Document" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Posting","UndoPosting",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting",
|
|
||||||
"InteractiveChangeOfPosted",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"InformationRegister" = @(
|
|
||||||
"Read","Update","View","Edit","TotalsControl",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"AccumulationRegister" = @("Read","Update","View","Edit","TotalsControl")
|
|
||||||
"AccountingRegister" = @("Read","Update","View","Edit","TotalsControl")
|
|
||||||
"CalculationRegister" = @("Read","View")
|
|
||||||
"Constant" = @(
|
|
||||||
"Read","Update","View","Edit",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"ChartOfAccounts" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistory","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment"
|
|
||||||
)
|
|
||||||
"ChartOfCharacteristicTypes" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"ChartOfCalculationTypes" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData"
|
|
||||||
)
|
|
||||||
"ExchangePlan" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"BusinessProcess" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveActivate","InteractiveStart"
|
|
||||||
)
|
|
||||||
"Task" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveActivate","InteractiveExecute"
|
|
||||||
)
|
|
||||||
"DataProcessor" = @("Use","View")
|
|
||||||
"Report" = @("Use","View")
|
|
||||||
"CommonForm" = @("View")
|
|
||||||
"CommonCommand" = @("View")
|
|
||||||
"Subsystem" = @("View")
|
|
||||||
"FilterCriterion" = @("View")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read","Update")
|
|
||||||
"WebService" = @("Use")
|
|
||||||
"HTTPService" = @("Use")
|
|
||||||
"IntegrationService" = @("Use")
|
|
||||||
"SessionParameter" = @("Get","Set")
|
|
||||||
"CommonAttribute" = @("View","Edit")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Nested objects: Attribute, StandardAttribute, TabularSection, Dimension, Resource, AddressingAttribute
|
|
||||||
$script:nestedRights = @("View","Edit")
|
|
||||||
$script:commandRights = @("View")
|
|
||||||
|
|
||||||
# --- 4. Presets (@view, @edit) ---
|
|
||||||
|
|
||||||
$script:presets = @{
|
|
||||||
"view" = @{
|
|
||||||
"Catalog" = @("Read","View","InputByString")
|
|
||||||
"ExchangePlan" = @("Read","View","InputByString")
|
|
||||||
"Document" = @("Read","View","InputByString")
|
|
||||||
"ChartOfAccounts" = @("Read","View","InputByString")
|
|
||||||
"ChartOfCharacteristicTypes" = @("Read","View","InputByString")
|
|
||||||
"ChartOfCalculationTypes" = @("Read","View","InputByString")
|
|
||||||
"BusinessProcess" = @("Read","View","InputByString")
|
|
||||||
"Task" = @("Read","View","InputByString")
|
|
||||||
"InformationRegister" = @("Read","View")
|
|
||||||
"AccumulationRegister" = @("Read","View")
|
|
||||||
"AccountingRegister" = @("Read","View")
|
|
||||||
"CalculationRegister" = @("Read","View")
|
|
||||||
"Constant" = @("Read","View")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read")
|
|
||||||
"CommonForm" = @("View")
|
|
||||||
"CommonCommand" = @("View")
|
|
||||||
"Subsystem" = @("View")
|
|
||||||
"FilterCriterion" = @("View")
|
|
||||||
"SessionParameter" = @("Get")
|
|
||||||
"CommonAttribute" = @("View")
|
|
||||||
"DataProcessor" = @("Use","View")
|
|
||||||
"Report" = @("Use","View")
|
|
||||||
"Configuration" = @("ThinClient","WebClient","Output","SaveUserData","MainWindowModeNormal")
|
|
||||||
}
|
|
||||||
"edit" = @{
|
|
||||||
"Catalog" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ExchangePlan" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"Document" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Posting","UndoPosting","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting","InteractiveChangeOfPosted")
|
|
||||||
"ChartOfAccounts" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ChartOfCharacteristicTypes" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ChartOfCalculationTypes" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"BusinessProcess" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractiveActivate","InteractiveStart")
|
|
||||||
"Task" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractiveActivate","InteractiveExecute")
|
|
||||||
"InformationRegister" = @("Read","Update","View","Edit")
|
|
||||||
"AccumulationRegister" = @("Read","Update","View","Edit")
|
|
||||||
"AccountingRegister" = @("Read","Update","View","Edit")
|
|
||||||
"Constant" = @("Read","Update","View","Edit")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read","Update")
|
|
||||||
"SessionParameter" = @("Get","Set")
|
|
||||||
"CommonAttribute" = @("View","Edit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Helpers ---
|
|
||||||
|
|
||||||
function Get-ObjectType {
|
|
||||||
param([string]$objectName)
|
|
||||||
$dotIdx = $objectName.IndexOf(".")
|
|
||||||
if ($dotIdx -lt 0) { return $objectName }
|
|
||||||
return $objectName.Substring(0, $dotIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Is-NestedObject {
|
|
||||||
param([string]$objectName)
|
|
||||||
return ($objectName.Split(".").Count -ge 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Resolve-Preset {
|
|
||||||
param([string]$objectType, [string]$presetName)
|
|
||||||
|
|
||||||
$preset = $presetName.TrimStart('@')
|
|
||||||
|
|
||||||
if (-not $script:presets.ContainsKey($preset)) {
|
|
||||||
Write-Warning "Unknown preset '@$preset'. Known: @view, @edit"
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
$typeMap = $script:presets[$preset]
|
|
||||||
if (-not $typeMap.ContainsKey($objectType)) {
|
|
||||||
$available = @()
|
|
||||||
foreach ($k in $script:presets.Keys) {
|
|
||||||
if ($script:presets[$k].ContainsKey($objectType)) {
|
|
||||||
$available += "@$k"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$availStr = if ($available.Count -gt 0) { $available -join ", " } else { "none" }
|
|
||||||
Write-Warning "Preset '@$preset' not defined for type '$objectType'. Available: $availStr"
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
return @($typeMap[$objectType])
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate-RightName {
|
|
||||||
param([string]$objectName, [string]$rightName)
|
|
||||||
|
|
||||||
$objectType = Get-ObjectType $objectName
|
|
||||||
|
|
||||||
if (Is-NestedObject $objectName) {
|
|
||||||
if ($objectName -match '\.Command\.') {
|
|
||||||
if ($rightName -notin $script:commandRights) {
|
|
||||||
Write-Warning "${objectName}: '$rightName' not valid for commands (only: View)"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($rightName -notin $script:nestedRights) {
|
|
||||||
Write-Warning "${objectName}: '$rightName' not valid for nested objects (only: View, Edit)"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $script:knownRights.ContainsKey($objectType)) {
|
|
||||||
Write-Warning "${objectName}: unknown object type '$objectType'"
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
$validRights = $script:knownRights[$objectType]
|
|
||||||
if ($rightName -notin $validRights) {
|
|
||||||
$suggestions = @($validRights | Where-Object {
|
|
||||||
$_ -like "*$rightName*" -or $rightName -like "*$_*"
|
|
||||||
})
|
|
||||||
$sugStr = if ($suggestions.Count -gt 0) { " Did you mean: $($suggestions -join ', ')?" } else { "" }
|
|
||||||
Write-Warning "${objectName}: unknown right '$rightName'.$sugStr"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Parse object entries ---
|
|
||||||
|
|
||||||
function Parse-ObjectEntry {
|
|
||||||
param($entry)
|
|
||||||
|
|
||||||
# --- String shorthand ---
|
|
||||||
if ($entry -is [string]) {
|
|
||||||
$colonIdx = $entry.IndexOf(':')
|
|
||||||
if ($colonIdx -lt 0) {
|
|
||||||
Write-Warning "Invalid string '$entry' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'"
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$objName = Translate-ObjectName ($entry.Substring(0, $colonIdx).Trim())
|
|
||||||
$rightsStr = $entry.Substring($colonIdx + 1).Trim()
|
|
||||||
$objectType = Get-ObjectType $objName
|
|
||||||
|
|
||||||
if ($rightsStr.StartsWith('@')) {
|
|
||||||
$rightNames = @(Resolve-Preset -objectType $objectType -presetName $rightsStr)
|
|
||||||
} else {
|
|
||||||
$rightNames = @($rightsStr -split ',\s*' | ForEach-Object { Translate-RightName $_.Trim() } | Where-Object { $_ })
|
|
||||||
foreach ($r in $rightNames) {
|
|
||||||
Validate-RightName -objectName $objName -rightName $r | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rights = @()
|
|
||||||
foreach ($r in $rightNames) {
|
|
||||||
$rights += ,@{Name=$r; Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
return @{ Name = $objName; Rights = $rights }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Object form ---
|
|
||||||
$objName = Translate-ObjectName "$($entry.name)"
|
|
||||||
if (-not $objName) {
|
|
||||||
Write-Warning "Object entry missing 'name' field"
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectType = Get-ObjectType $objName
|
|
||||||
$rightsMap = [ordered]@{}
|
|
||||||
|
|
||||||
# 1) Start with preset
|
|
||||||
if ($entry.preset) {
|
|
||||||
$presetRights = @(Resolve-Preset -objectType $objectType -presetName "$($entry.preset)")
|
|
||||||
foreach ($r in $presetRights) {
|
|
||||||
$rightsMap[$r] = @{Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2) Apply explicit rights
|
|
||||||
if ($entry.rights) {
|
|
||||||
if ($entry.rights -is [array]) {
|
|
||||||
foreach ($r in $entry.rights) {
|
|
||||||
$rName = Translate-RightName "$r"
|
|
||||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
|
||||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foreach ($p in $entry.rights.PSObject.Properties) {
|
|
||||||
$rName = Translate-RightName $p.Name
|
|
||||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
|
||||||
$boolVal = $p.Value
|
|
||||||
if ($boolVal -eq $true -or "$boolVal" -eq "True") {
|
|
||||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
|
||||||
} else {
|
|
||||||
$rightsMap[$rName] = @{Value="false"; Condition=$null}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3) Apply RLS conditions
|
|
||||||
if ($entry.rls) {
|
|
||||||
foreach ($p in $entry.rls.PSObject.Properties) {
|
|
||||||
$rlsRight = Translate-RightName $p.Name
|
|
||||||
if ($rightsMap.Contains($rlsRight)) {
|
|
||||||
$rightsMap[$rlsRight].Condition = "$($p.Value)"
|
|
||||||
} else {
|
|
||||||
Write-Warning "${objName}: RLS for '$rlsRight' but this right is not in the rights list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert to array
|
|
||||||
$rights = @()
|
|
||||||
foreach ($k in $rightsMap.Keys) {
|
|
||||||
$rights += ,@{
|
|
||||||
Name = $k
|
|
||||||
Value = $rightsMap[$k].Value
|
|
||||||
Condition = $rightsMap[$k].Condition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @{ Name = $objName; Rights = $rights }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Parse all object entries ---
|
|
||||||
|
|
||||||
# Synonym: accept "rights" as alias for "objects"
|
|
||||||
if (-not $def.objects -and $def.rights) { $def | Add-Member -NotePropertyName objects -NotePropertyValue $def.rights }
|
|
||||||
|
|
||||||
$parsedObjects = @()
|
|
||||||
if ($def.objects) {
|
|
||||||
foreach ($entry in $def.objects) {
|
|
||||||
$parsed = Parse-ObjectEntry -entry $entry
|
|
||||||
if ($parsed) {
|
|
||||||
$parsedObjects += ,$parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
|
|
||||||
$formatVersion = Detect-FormatVersion $resolvedOutputDir
|
|
||||||
|
|
||||||
# --- 8. Generate UUID ---
|
|
||||||
|
|
||||||
$uuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# --- 9. Emit metadata XML (Roles/Name.xml) ---
|
|
||||||
|
|
||||||
$script:xmlBuf = New-Object System.Text.StringBuilder 4096
|
|
||||||
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
|
||||||
X ' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
X ' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
X ' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
X ' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
X ' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
X ' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
X ' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
X ' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
X ' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
X ' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
X ' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
X ' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
X ' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
X ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
X " version=`"$formatVersion`">"
|
|
||||||
X " <Role uuid=`"$uuid`">"
|
|
||||||
X ' <Properties>'
|
|
||||||
X " <Name>$roleName</Name>"
|
|
||||||
X ' <Synonym>'
|
|
||||||
X ' <v8:item>'
|
|
||||||
X ' <v8:lang>ru</v8:lang>'
|
|
||||||
X " <v8:content>$(Esc-Xml $synonym)</v8:content>"
|
|
||||||
X ' </v8:item>'
|
|
||||||
X ' </Synonym>'
|
|
||||||
if ($comment) {
|
|
||||||
X " <Comment>$(Esc-Xml $comment)</Comment>"
|
|
||||||
} else {
|
|
||||||
X ' <Comment/>'
|
|
||||||
}
|
|
||||||
X ' </Properties>'
|
|
||||||
X ' </Role>'
|
|
||||||
X '</MetaDataObject>'
|
|
||||||
|
|
||||||
$metadataXml = $script:xmlBuf.ToString()
|
|
||||||
|
|
||||||
# --- 10. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
|
||||||
|
|
||||||
$script:xmlBuf = New-Object System.Text.StringBuilder 8192
|
|
||||||
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<Rights xmlns="http://v8.1c.ru/8.2/roles"'
|
|
||||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
X " xsi:type=`"Rights`" version=`"$formatVersion`">"
|
|
||||||
|
|
||||||
# Global flags (defaults match typical 1C roles)
|
|
||||||
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
|
|
||||||
$sfab = if ($null -ne $def.setForAttributesByDefault) { "$($def.setForAttributesByDefault)".ToLower() } else { "true" }
|
|
||||||
$irco = if ($null -ne $def.independentRightsOfChildObjects) { "$($def.independentRightsOfChildObjects)".ToLower() } else { "false" }
|
|
||||||
|
|
||||||
X " <setForNewObjects>$sfno</setForNewObjects>"
|
|
||||||
X " <setForAttributesByDefault>$sfab</setForAttributesByDefault>"
|
|
||||||
X " <independentRightsOfChildObjects>$irco</independentRightsOfChildObjects>"
|
|
||||||
|
|
||||||
# Object blocks
|
|
||||||
$totalRights = 0
|
|
||||||
foreach ($obj in $parsedObjects) {
|
|
||||||
X ' <object>'
|
|
||||||
X " <name>$($obj.Name)</name>"
|
|
||||||
foreach ($right in $obj.Rights) {
|
|
||||||
X ' <right>'
|
|
||||||
X " <name>$($right.Name)</name>"
|
|
||||||
X " <value>$($right.Value)</value>"
|
|
||||||
if ($right.Condition) {
|
|
||||||
X ' <restrictionByCondition>'
|
|
||||||
X " <condition>$(Esc-Xml $right.Condition)</condition>"
|
|
||||||
X ' </restrictionByCondition>'
|
|
||||||
}
|
|
||||||
X ' </right>'
|
|
||||||
$totalRights++
|
|
||||||
}
|
|
||||||
X ' </object>'
|
|
||||||
}
|
|
||||||
|
|
||||||
# RLS restriction templates
|
|
||||||
$templateCount = 0
|
|
||||||
if ($def.templates) {
|
|
||||||
foreach ($tpl in $def.templates) {
|
|
||||||
X ' <restrictionTemplate>'
|
|
||||||
X " <name>$(Esc-Xml "$($tpl.name)")</name>"
|
|
||||||
X " <condition>$(Esc-Xml "$($tpl.condition)")</condition>"
|
|
||||||
X ' </restrictionTemplate>'
|
|
||||||
$templateCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
X '</Rights>'
|
|
||||||
|
|
||||||
$rightsXml = $script:xmlBuf.ToString()
|
|
||||||
|
|
||||||
# --- 11. Write output files ---
|
|
||||||
|
|
||||||
$outDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) {
|
|
||||||
$OutputDir
|
|
||||||
} else {
|
|
||||||
Join-Path (Get-Location) $OutputDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine Roles dir and config root
|
|
||||||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
|
||||||
$leaf = Split-Path $outDir -Leaf
|
|
||||||
if ($leaf -eq "Roles") {
|
|
||||||
$rolesDir = $outDir
|
|
||||||
$configDir = Split-Path $outDir -Parent
|
|
||||||
} else {
|
|
||||||
$rolesDir = Join-Path $outDir "Roles"
|
|
||||||
$configDir = $outDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Metadata: Roles/RoleName.xml
|
|
||||||
$metadataPath = Join-Path $rolesDir "$roleName.xml"
|
|
||||||
if (-not (Test-Path $rolesDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $rolesDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rights: Roles/RoleName/Ext/Rights.xml
|
|
||||||
$roleSubDir = Join-Path $rolesDir $roleName
|
|
||||||
$extDir = Join-Path $roleSubDir "Ext"
|
|
||||||
$rightsPath = Join-Path $extDir "Rights.xml"
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($metadataPath, $metadataXml, $enc)
|
|
||||||
[System.IO.File]::WriteAllText($rightsPath, $rightsXml, $enc)
|
|
||||||
|
|
||||||
# --- 12. Register in Configuration.xml ---
|
|
||||||
|
|
||||||
$configXmlPath = Join-Path $configDir "Configuration.xml"
|
|
||||||
$regResult = $null
|
|
||||||
|
|
||||||
if (Test-Path $configXmlPath) {
|
|
||||||
$configDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$configDoc.PreserveWhitespace = $true
|
|
||||||
$configDoc.Load($configXmlPath)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
|
|
||||||
if ($childObjects) {
|
|
||||||
$existing = $childObjects.SelectNodes("md:Role", $nsMgr)
|
|
||||||
$alreadyExists = $false
|
|
||||||
foreach ($r in $existing) {
|
|
||||||
if ($r.InnerText -eq $roleName) {
|
|
||||||
$alreadyExists = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($alreadyExists) {
|
|
||||||
$regResult = "already"
|
|
||||||
} else {
|
|
||||||
$roleElem = $configDoc.CreateElement("Role", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$roleElem.InnerText = $roleName
|
|
||||||
|
|
||||||
if ($existing.Count -gt 0) {
|
|
||||||
# Insert after last existing <Role>
|
|
||||||
$lastRole = $existing[$existing.Count - 1]
|
|
||||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertAfter($newWs, $lastRole) | Out-Null
|
|
||||||
$childObjects.InsertAfter($roleElem, $newWs) | Out-Null
|
|
||||||
} else {
|
|
||||||
# No existing roles — insert before closing whitespace
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($roleElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($roleElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save
|
|
||||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$cfgSettings.Indent = $false
|
|
||||||
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings)
|
|
||||||
$configDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
$regResult = "added"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$regResult = "no-childobj"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$regResult = "no-config"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 13. Summary ---
|
|
||||||
|
|
||||||
Write-Host "[OK] Role '$roleName' compiled"
|
|
||||||
Write-Host " UUID: $uuid"
|
|
||||||
Write-Host " Metadata: $metadataPath"
|
|
||||||
Write-Host " Rights: $rightsPath"
|
|
||||||
Write-Host " Objects: $($parsedObjects.Count), Rights: $totalRights, Templates: $templateCount"
|
|
||||||
switch ($regResult) {
|
|
||||||
"added" { Write-Host " Configuration.xml: <Role>$roleName</Role> added to ChildObjects" }
|
|
||||||
"already" { Write-Host " Configuration.xml: <Role>$roleName</Role> already registered" }
|
|
||||||
"no-childobj" { Write-Warning "Configuration.xml found but <ChildObjects> not found" }
|
|
||||||
"no-config" { Write-Warning "Configuration.xml not found at $configXmlPath — register manually" }
|
|
||||||
}
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# role-compile v1.4 — Compile 1C role from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
def detect_format_version(d):
|
|
||||||
while d:
|
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
|
||||||
if os.path.isfile(cfg_path):
|
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
|
||||||
head = f.read(2000)
|
|
||||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return "2.17"
|
|
||||||
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
|
|
||||||
|
|
||||||
def emit_mltext(lines, indent, tag, text):
|
|
||||||
if not text:
|
|
||||||
lines.append(f"{indent}<{tag}/>")
|
|
||||||
return
|
|
||||||
lines.append(f"{indent}<{tag}>")
|
|
||||||
lines.append(f"{indent}\t<v8:item>")
|
|
||||||
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
|
||||||
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
|
||||||
lines.append(f"{indent}\t</v8:item>")
|
|
||||||
lines.append(f"{indent}</{tag}>")
|
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Russian synonyms -> canonical English names ---
|
|
||||||
|
|
||||||
TYPE_ALIASES = {
|
|
||||||
"Справочник": "Catalog",
|
|
||||||
"Документ": "Document",
|
|
||||||
"РегистрСведений": "InformationRegister",
|
|
||||||
"РегистрНакопления": "AccumulationRegister",
|
|
||||||
"РегистрБухгалтерии": "AccountingRegister",
|
|
||||||
"РегистрРасчета": "CalculationRegister",
|
|
||||||
"Константа": "Constant",
|
|
||||||
"ПланСчетов": "ChartOfAccounts",
|
|
||||||
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
|
|
||||||
"ПланВидовРасчета": "ChartOfCalculationTypes",
|
|
||||||
"ПланОбмена": "ExchangePlan",
|
|
||||||
"БизнесПроцесс": "BusinessProcess",
|
|
||||||
"Задача": "Task",
|
|
||||||
"Обработка": "DataProcessor",
|
|
||||||
"Отчет": "Report",
|
|
||||||
"ОбщаяФорма": "CommonForm",
|
|
||||||
"ОбщаяКоманда": "CommonCommand",
|
|
||||||
"Подсистема": "Subsystem",
|
|
||||||
"КритерийОтбора": "FilterCriterion",
|
|
||||||
"ЖурналДокументов": "DocumentJournal",
|
|
||||||
"Последовательность": "Sequence",
|
|
||||||
"ВебСервис": "WebService",
|
|
||||||
"HTTPСервис": "HTTPService",
|
|
||||||
"СервисИнтеграции": "IntegrationService",
|
|
||||||
"ПараметрСеанса": "SessionParameter",
|
|
||||||
"ОбщийРеквизит": "CommonAttribute",
|
|
||||||
"Конфигурация": "Configuration",
|
|
||||||
"Перечисление": "Enum",
|
|
||||||
# Nested
|
|
||||||
"Реквизит": "Attribute",
|
|
||||||
"СтандартныйРеквизит": "StandardAttribute",
|
|
||||||
"ТабличнаяЧасть": "TabularSection",
|
|
||||||
"Измерение": "Dimension",
|
|
||||||
"Ресурс": "Resource",
|
|
||||||
"Команда": "Command",
|
|
||||||
"РеквизитАдресации": "AddressingAttribute",
|
|
||||||
}
|
|
||||||
|
|
||||||
RIGHT_ALIASES = {
|
|
||||||
"Чтение": "Read",
|
|
||||||
"Добавление": "Insert",
|
|
||||||
"Изменение": "Update",
|
|
||||||
"Удаление": "Delete",
|
|
||||||
"Просмотр": "View",
|
|
||||||
"Редактирование": "Edit",
|
|
||||||
"ВводПоСтроке": "InputByString",
|
|
||||||
"Проведение": "Posting",
|
|
||||||
"ОтменаПроведения": "UndoPosting",
|
|
||||||
"ИнтерактивноеДобавление": "InteractiveInsert",
|
|
||||||
"ИнтерактивнаяПометкаУдаления": "InteractiveSetDeletionMark",
|
|
||||||
"ИнтерактивноеСнятиеПометкиУдаления": "InteractiveClearDeletionMark",
|
|
||||||
"ИнтерактивноеУдаление": "InteractiveDelete",
|
|
||||||
"ИнтерактивноеУдалениеПомеченных": "InteractiveDeleteMarked",
|
|
||||||
"ИнтерактивноеПроведение": "InteractivePosting",
|
|
||||||
"ИнтерактивноеПроведениеНеоперативное": "InteractivePostingRegular",
|
|
||||||
"ИнтерактивнаяОтменаПроведения": "InteractiveUndoPosting",
|
|
||||||
"ИнтерактивноеИзменениеПроведенных": "InteractiveChangeOfPosted",
|
|
||||||
"Использование": "Use",
|
|
||||||
"Получение": "Get",
|
|
||||||
"Установка": "Set",
|
|
||||||
"Старт": "Start",
|
|
||||||
"ИнтерактивныйСтарт": "InteractiveStart",
|
|
||||||
"ИнтерактивнаяАктивация": "InteractiveActivate",
|
|
||||||
"Выполнение": "Execute",
|
|
||||||
"ИнтерактивноеВыполнение": "InteractiveExecute",
|
|
||||||
"УправлениеИтогами": "TotalsControl",
|
|
||||||
"Администрирование": "Administration",
|
|
||||||
"АдминистрированиеДанных": "DataAdministration",
|
|
||||||
"ТонкийКлиент": "ThinClient",
|
|
||||||
"ВебКлиент": "WebClient",
|
|
||||||
"ТолстыйКлиент": "ThickClient",
|
|
||||||
"ВнешнееСоединение": "ExternalConnection",
|
|
||||||
"Вывод": "Output",
|
|
||||||
"СохранениеДанныхПользователя": "SaveUserData",
|
|
||||||
"МобильныйКлиент": "MobileClient",
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Known rights per object type ---
|
|
||||||
|
|
||||||
KNOWN_RIGHTS = {
|
|
||||||
"Configuration": [
|
|
||||||
"Administration", "DataAdministration", "UpdateDataBaseConfiguration",
|
|
||||||
"ConfigurationExtensionsAdministration", "ActiveUsers", "EventLog", "ExclusiveMode",
|
|
||||||
"ThinClient", "ThickClient", "WebClient", "MobileClient", "ExternalConnection",
|
|
||||||
"Automation", "Output", "SaveUserData", "TechnicalSpecialistMode",
|
|
||||||
"InteractiveOpenExtDataProcessors", "InteractiveOpenExtReports",
|
|
||||||
"AnalyticsSystemClient", "CollaborationSystemInfoBaseRegistration",
|
|
||||||
"MainWindowModeNormal", "MainWindowModeWorkplace",
|
|
||||||
"MainWindowModeEmbeddedWorkplace", "MainWindowModeFullscreenWorkplace", "MainWindowModeKiosk",
|
|
||||||
],
|
|
||||||
"Catalog": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"Document": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Posting", "UndoPosting",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting",
|
|
||||||
"InteractiveChangeOfPosted",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"InformationRegister": [
|
|
||||||
"Read", "Update", "View", "Edit", "TotalsControl",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"AccumulationRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
|
||||||
"AccountingRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
|
||||||
"CalculationRegister": ["Read", "View"],
|
|
||||||
"Constant": [
|
|
||||||
"Read", "Update", "View", "Edit",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"ChartOfAccounts": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistory", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
],
|
|
||||||
"ChartOfCharacteristicTypes": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"ChartOfCalculationTypes": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
],
|
|
||||||
"ExchangePlan": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"BusinessProcess": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveActivate", "InteractiveStart",
|
|
||||||
],
|
|
||||||
"Task": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveActivate", "InteractiveExecute",
|
|
||||||
],
|
|
||||||
"DataProcessor": ["Use", "View"],
|
|
||||||
"Report": ["Use", "View"],
|
|
||||||
"CommonForm": ["View"],
|
|
||||||
"CommonCommand": ["View"],
|
|
||||||
"Subsystem": ["View"],
|
|
||||||
"FilterCriterion": ["View"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read", "Update"],
|
|
||||||
"WebService": ["Use"],
|
|
||||||
"HTTPService": ["Use"],
|
|
||||||
"IntegrationService": ["Use"],
|
|
||||||
"SessionParameter": ["Get", "Set"],
|
|
||||||
"CommonAttribute": ["View", "Edit"],
|
|
||||||
}
|
|
||||||
|
|
||||||
NESTED_RIGHTS = ["View", "Edit"]
|
|
||||||
COMMAND_RIGHTS = ["View"]
|
|
||||||
|
|
||||||
# --- Presets ---
|
|
||||||
|
|
||||||
PRESETS = {
|
|
||||||
"view": {
|
|
||||||
"Catalog": ["Read", "View", "InputByString"],
|
|
||||||
"ExchangePlan": ["Read", "View", "InputByString"],
|
|
||||||
"Document": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfAccounts": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfCharacteristicTypes": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfCalculationTypes": ["Read", "View", "InputByString"],
|
|
||||||
"BusinessProcess": ["Read", "View", "InputByString"],
|
|
||||||
"Task": ["Read", "View", "InputByString"],
|
|
||||||
"InformationRegister": ["Read", "View"],
|
|
||||||
"AccumulationRegister": ["Read", "View"],
|
|
||||||
"AccountingRegister": ["Read", "View"],
|
|
||||||
"CalculationRegister": ["Read", "View"],
|
|
||||||
"Constant": ["Read", "View"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read"],
|
|
||||||
"CommonForm": ["View"],
|
|
||||||
"CommonCommand": ["View"],
|
|
||||||
"Subsystem": ["View"],
|
|
||||||
"FilterCriterion": ["View"],
|
|
||||||
"SessionParameter": ["Get"],
|
|
||||||
"CommonAttribute": ["View"],
|
|
||||||
"DataProcessor": ["Use", "View"],
|
|
||||||
"Report": ["Use", "View"],
|
|
||||||
"Configuration": ["ThinClient", "WebClient", "Output", "SaveUserData", "MainWindowModeNormal"],
|
|
||||||
},
|
|
||||||
"edit": {
|
|
||||||
"Catalog": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ExchangePlan": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"Document": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Posting", "UndoPosting", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting", "InteractiveChangeOfPosted"],
|
|
||||||
"ChartOfAccounts": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ChartOfCharacteristicTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ChartOfCalculationTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"BusinessProcess": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveStart"],
|
|
||||||
"Task": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveExecute"],
|
|
||||||
"InformationRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"AccumulationRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"AccountingRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"Constant": ["Read", "Update", "View", "Edit"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read", "Update"],
|
|
||||||
"SessionParameter": ["Get", "Set"],
|
|
||||||
"CommonAttribute": ["View", "Edit"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def translate_object_name(name):
|
|
||||||
parts = name.split('.')
|
|
||||||
result = []
|
|
||||||
for p in parts:
|
|
||||||
result.append(TYPE_ALIASES.get(p, p))
|
|
||||||
return '.'.join(result)
|
|
||||||
|
|
||||||
|
|
||||||
def translate_right_name(name):
|
|
||||||
return RIGHT_ALIASES.get(name, name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_type(object_name):
|
|
||||||
dot_idx = object_name.find('.')
|
|
||||||
if dot_idx < 0:
|
|
||||||
return object_name
|
|
||||||
return object_name[:dot_idx]
|
|
||||||
|
|
||||||
|
|
||||||
def is_nested_object(object_name):
|
|
||||||
return len(object_name.split('.')) >= 3
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_preset(object_type, preset_name):
|
|
||||||
preset = preset_name.lstrip('@')
|
|
||||||
if preset not in PRESETS:
|
|
||||||
print(f"WARNING: Unknown preset '@{preset}'. Known: @view, @edit", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
type_map = PRESETS[preset]
|
|
||||||
if object_type not in type_map:
|
|
||||||
available = []
|
|
||||||
for k in PRESETS:
|
|
||||||
if object_type in PRESETS[k]:
|
|
||||||
available.append(f'@{k}')
|
|
||||||
avail_str = ', '.join(available) if available else 'none'
|
|
||||||
print(f"WARNING: Preset '@{preset}' not defined for type '{object_type}'. Available: {avail_str}", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
return list(type_map[object_type])
|
|
||||||
|
|
||||||
|
|
||||||
def validate_right_name(object_name, right_name):
|
|
||||||
object_type = get_object_type(object_name)
|
|
||||||
|
|
||||||
if is_nested_object(object_name):
|
|
||||||
if '.Command.' in object_name:
|
|
||||||
if right_name not in COMMAND_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if right_name not in NESTED_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if object_type not in KNOWN_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr)
|
|
||||||
return True
|
|
||||||
|
|
||||||
valid_rights = KNOWN_RIGHTS[object_type]
|
|
||||||
if right_name not in valid_rights:
|
|
||||||
suggestions = [r for r in valid_rights if right_name in r or r in right_name]
|
|
||||||
sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
|
|
||||||
print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def parse_object_entry(entry):
|
|
||||||
# --- String shorthand ---
|
|
||||||
if isinstance(entry, str):
|
|
||||||
colon_idx = entry.find(':')
|
|
||||||
if colon_idx < 0:
|
|
||||||
print(f"WARNING: Invalid string '{entry}' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
obj_name = translate_object_name(entry[:colon_idx].strip())
|
|
||||||
rights_str = entry[colon_idx + 1:].strip()
|
|
||||||
object_type = get_object_type(obj_name)
|
|
||||||
|
|
||||||
if rights_str.startswith('@'):
|
|
||||||
right_names = resolve_preset(object_type, rights_str)
|
|
||||||
else:
|
|
||||||
right_names = [translate_right_name(r.strip()) for r in rights_str.split(',') if r.strip()]
|
|
||||||
for r in right_names:
|
|
||||||
validate_right_name(obj_name, r)
|
|
||||||
|
|
||||||
rights = []
|
|
||||||
for r in right_names:
|
|
||||||
rights.append({'Name': r, 'Value': 'true', 'Condition': None})
|
|
||||||
return {'Name': obj_name, 'Rights': rights}
|
|
||||||
|
|
||||||
# --- Object form ---
|
|
||||||
obj_name = translate_object_name(str(entry.get('name', '')))
|
|
||||||
if not obj_name:
|
|
||||||
print("WARNING: Object entry missing 'name' field", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
|
|
||||||
object_type = get_object_type(obj_name)
|
|
||||||
# Use a list of tuples to preserve insertion order
|
|
||||||
rights_map = {} # name -> {Value, Condition}
|
|
||||||
rights_order = [] # preserve order
|
|
||||||
|
|
||||||
# 1) Start with preset
|
|
||||||
if entry.get('preset'):
|
|
||||||
preset_rights = resolve_preset(object_type, str(entry['preset']))
|
|
||||||
for r in preset_rights:
|
|
||||||
if r not in rights_map:
|
|
||||||
rights_order.append(r)
|
|
||||||
rights_map[r] = {'Value': 'true', 'Condition': None}
|
|
||||||
|
|
||||||
# 2) Apply explicit rights
|
|
||||||
if entry.get('rights') is not None:
|
|
||||||
if isinstance(entry['rights'], list):
|
|
||||||
for r in entry['rights']:
|
|
||||||
r_name = translate_right_name(str(r))
|
|
||||||
validate_right_name(obj_name, r_name)
|
|
||||||
if r_name not in rights_map:
|
|
||||||
rights_order.append(r_name)
|
|
||||||
rights_map[r_name] = {'Value': 'true', 'Condition': None}
|
|
||||||
elif isinstance(entry['rights'], dict):
|
|
||||||
for p_name, p_value in entry['rights'].items():
|
|
||||||
r_name = translate_right_name(p_name)
|
|
||||||
validate_right_name(obj_name, r_name)
|
|
||||||
bool_val = 'true' if p_value is True or str(p_value) == 'True' else 'false'
|
|
||||||
if r_name not in rights_map:
|
|
||||||
rights_order.append(r_name)
|
|
||||||
rights_map[r_name] = {'Value': bool_val, 'Condition': None}
|
|
||||||
|
|
||||||
# 3) Apply RLS conditions
|
|
||||||
if entry.get('rls'):
|
|
||||||
for p_name, p_value in entry['rls'].items():
|
|
||||||
rls_right = translate_right_name(p_name)
|
|
||||||
if rls_right in rights_map:
|
|
||||||
rights_map[rls_right]['Condition'] = str(p_value)
|
|
||||||
else:
|
|
||||||
print(f"WARNING: {obj_name}: RLS for '{rls_right}' but this right is not in the rights list", file=sys.stderr)
|
|
||||||
|
|
||||||
# Convert to array
|
|
||||||
rights = []
|
|
||||||
for k in rights_order:
|
|
||||||
rights.append({
|
|
||||||
'Name': k,
|
|
||||||
'Value': rights_map[k]['Value'],
|
|
||||||
'Condition': rights_map[k]['Condition'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {'Name': obj_name, 'Rights': rights}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False)
|
|
||||||
parser.add_argument('-JsonPath', type=str, required=True)
|
|
||||||
parser.add_argument('-OutputDir', type=str, required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
json_path = args.JsonPath
|
|
||||||
if not os.path.exists(json_path):
|
|
||||||
print(f"File not found: {json_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
defn = json.load(f)
|
|
||||||
|
|
||||||
if not defn.get('name'):
|
|
||||||
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
role_name = str(defn['name'])
|
|
||||||
synonym = str(defn['synonym']) if defn.get('synonym') else role_name
|
|
||||||
comment = str(defn['comment']) if defn.get('comment') else ''
|
|
||||||
|
|
||||||
# Synonym: accept "rights" as alias for "objects"
|
|
||||||
if not defn.get('objects') and defn.get('rights'):
|
|
||||||
defn['objects'] = defn['rights']
|
|
||||||
|
|
||||||
out_dir_resolved = args.OutputDir if os.path.isabs(args.OutputDir) else os.path.join(os.getcwd(), args.OutputDir)
|
|
||||||
format_version = detect_format_version(out_dir_resolved)
|
|
||||||
|
|
||||||
# --- 2. Parse all object entries ---
|
|
||||||
parsed_objects = []
|
|
||||||
if defn.get('objects'):
|
|
||||||
for entry in defn['objects']:
|
|
||||||
parsed = parse_object_entry(entry)
|
|
||||||
if parsed:
|
|
||||||
parsed_objects.append(parsed)
|
|
||||||
|
|
||||||
# --- 3. Generate UUID ---
|
|
||||||
uid = new_uuid()
|
|
||||||
|
|
||||||
# --- 4. Emit metadata XML (Roles/Name.xml) ---
|
|
||||||
lines = []
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"')
|
|
||||||
lines.append(' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"')
|
|
||||||
lines.append(' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"')
|
|
||||||
lines.append(' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"')
|
|
||||||
lines.append(' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"')
|
|
||||||
lines.append(' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"')
|
|
||||||
lines.append(' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"')
|
|
||||||
lines.append(' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"')
|
|
||||||
lines.append(' xmlns:v8="http://v8.1c.ru/8.1/data/core"')
|
|
||||||
lines.append(' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"')
|
|
||||||
lines.append(' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"')
|
|
||||||
lines.append(' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"')
|
|
||||||
lines.append(' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"')
|
|
||||||
lines.append(' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"')
|
|
||||||
lines.append(' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"')
|
|
||||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
||||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
|
||||||
lines.append(f' version="{format_version}">')
|
|
||||||
lines.append(f' <Role uuid="{uid}">')
|
|
||||||
lines.append(' <Properties>')
|
|
||||||
lines.append(f' <Name>{role_name}</Name>')
|
|
||||||
lines.append(' <Synonym>')
|
|
||||||
lines.append(' <v8:item>')
|
|
||||||
lines.append(' <v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f' <v8:content>{esc_xml(synonym)}</v8:content>')
|
|
||||||
lines.append(' </v8:item>')
|
|
||||||
lines.append(' </Synonym>')
|
|
||||||
if comment:
|
|
||||||
lines.append(f' <Comment>{esc_xml(comment)}</Comment>')
|
|
||||||
else:
|
|
||||||
lines.append(' <Comment/>')
|
|
||||||
lines.append(' </Properties>')
|
|
||||||
lines.append(' </Role>')
|
|
||||||
lines.append('</MetaDataObject>')
|
|
||||||
|
|
||||||
metadata_xml = '\n'.join(lines) + '\n'
|
|
||||||
|
|
||||||
# --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
|
||||||
lines = []
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles"')
|
|
||||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
||||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
|
||||||
lines.append(f' xsi:type="Rights" version="{format_version}">')
|
|
||||||
|
|
||||||
# Global flags
|
|
||||||
sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false'
|
|
||||||
sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true'
|
|
||||||
irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false'
|
|
||||||
|
|
||||||
lines.append(f' <setForNewObjects>{sfno}</setForNewObjects>')
|
|
||||||
lines.append(f' <setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
|
|
||||||
lines.append(f' <independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
|
|
||||||
|
|
||||||
# Object blocks
|
|
||||||
total_rights = 0
|
|
||||||
for obj in parsed_objects:
|
|
||||||
lines.append(' <object>')
|
|
||||||
lines.append(f' <name>{obj["Name"]}</name>')
|
|
||||||
for right in obj['Rights']:
|
|
||||||
lines.append(' <right>')
|
|
||||||
lines.append(f' <name>{right["Name"]}</name>')
|
|
||||||
lines.append(f' <value>{right["Value"]}</value>')
|
|
||||||
if right['Condition']:
|
|
||||||
lines.append(' <restrictionByCondition>')
|
|
||||||
lines.append(f' <condition>{esc_xml(right["Condition"])}</condition>')
|
|
||||||
lines.append(' </restrictionByCondition>')
|
|
||||||
lines.append(' </right>')
|
|
||||||
total_rights += 1
|
|
||||||
lines.append(' </object>')
|
|
||||||
|
|
||||||
# RLS restriction templates
|
|
||||||
template_count = 0
|
|
||||||
if defn.get('templates'):
|
|
||||||
for tpl in defn['templates']:
|
|
||||||
lines.append(' <restrictionTemplate>')
|
|
||||||
lines.append(f' <name>{esc_xml(str(tpl["name"]))}</name>')
|
|
||||||
lines.append(f' <condition>{esc_xml(str(tpl["condition"]))}</condition>')
|
|
||||||
lines.append(' </restrictionTemplate>')
|
|
||||||
template_count += 1
|
|
||||||
|
|
||||||
lines.append('</Rights>')
|
|
||||||
|
|
||||||
rights_xml = '\n'.join(lines) + '\n'
|
|
||||||
|
|
||||||
# --- 6. Write output files ---
|
|
||||||
out_dir = args.OutputDir
|
|
||||||
if not os.path.isabs(out_dir):
|
|
||||||
out_dir = os.path.join(os.getcwd(), out_dir)
|
|
||||||
|
|
||||||
# Determine Roles dir and config root
|
|
||||||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
|
||||||
leaf = os.path.basename(out_dir.rstrip(os.sep).rstrip('/'))
|
|
||||||
if leaf == 'Roles':
|
|
||||||
roles_dir = out_dir
|
|
||||||
config_dir = os.path.dirname(out_dir)
|
|
||||||
else:
|
|
||||||
roles_dir = os.path.join(out_dir, 'Roles')
|
|
||||||
config_dir = out_dir
|
|
||||||
|
|
||||||
# Metadata: Roles/RoleName.xml
|
|
||||||
metadata_path = os.path.join(roles_dir, f'{role_name}.xml')
|
|
||||||
os.makedirs(roles_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Rights: Roles/RoleName/Ext/Rights.xml
|
|
||||||
role_sub_dir = os.path.join(roles_dir, role_name)
|
|
||||||
ext_dir = os.path.join(role_sub_dir, 'Ext')
|
|
||||||
rights_path = os.path.join(ext_dir, 'Rights.xml')
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(metadata_path, metadata_xml)
|
|
||||||
write_utf8_bom(rights_path, rights_xml)
|
|
||||||
|
|
||||||
# --- 7. Register in Configuration.xml ---
|
|
||||||
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
|
|
||||||
reg_result = None
|
|
||||||
|
|
||||||
if os.path.exists(config_xml_path):
|
|
||||||
with open(config_xml_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
raw_text = f.read()
|
|
||||||
|
|
||||||
# Check if already registered
|
|
||||||
if f'<Role>{role_name}</Role>' in raw_text:
|
|
||||||
reg_result = 'already'
|
|
||||||
else:
|
|
||||||
# Find last <Role>...</Role> and insert after it
|
|
||||||
role_pattern = re.compile(r'(<Role>[^<]*</Role>)')
|
|
||||||
matches = list(role_pattern.finditer(raw_text))
|
|
||||||
new_role_tag = f'<Role>{role_name}</Role>'
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
# Insert after last existing <Role>
|
|
||||||
last_match = matches[-1]
|
|
||||||
insert_pos = last_match.end()
|
|
||||||
raw_text = raw_text[:insert_pos] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:]
|
|
||||||
else:
|
|
||||||
# No existing roles — insert before </ChildObjects>
|
|
||||||
raw_text = raw_text.replace('</ChildObjects>', f'\t\t\t{new_role_tag}\n\t\t</ChildObjects>')
|
|
||||||
|
|
||||||
write_utf8_bom(config_xml_path, raw_text)
|
|
||||||
reg_result = 'added'
|
|
||||||
else:
|
|
||||||
reg_result = 'no-config'
|
|
||||||
|
|
||||||
# --- 8. Summary ---
|
|
||||||
print(f"[OK] Role '{role_name}' compiled")
|
|
||||||
print(f" UUID: {uid}")
|
|
||||||
print(f" Metadata: {metadata_path}")
|
|
||||||
print(f" Rights: {rights_path}")
|
|
||||||
print(f" Objects: {len(parsed_objects)}, Rights: {total_rights}, Templates: {template_count}")
|
|
||||||
if reg_result == 'added':
|
|
||||||
print(f" Configuration.xml: <Role>{role_name}</Role> added to ChildObjects")
|
|
||||||
elif reg_result == 'already':
|
|
||||||
print(f" Configuration.xml: <Role>{role_name}</Role> already registered")
|
|
||||||
elif reg_result == 'no-childobj':
|
|
||||||
print(f"WARNING: Configuration.xml found but <ChildObjects> not found", file=sys.stderr)
|
|
||||||
elif reg_result == 'no-config':
|
|
||||||
print(f"WARNING: Configuration.xml not found at {config_xml_path} -- register manually", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
# template-add v1.5 — Add template to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias("ProcessorName")]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$TemplateName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")]
|
|
||||||
[string]$TemplateType,
|
|
||||||
|
|
||||||
[string]$Synonym = $TemplateName,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src",
|
|
||||||
|
|
||||||
[switch]$SetMainSKD
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Маппинг типов ---
|
|
||||||
|
|
||||||
$typeMap = @{
|
|
||||||
"HTML" = @{ TemplateType = "HTMLDocument"; Ext = ".html" }
|
|
||||||
"Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" }
|
|
||||||
"SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" }
|
|
||||||
"BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" }
|
|
||||||
"DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmpl = $typeMap[$TemplateType]
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$objectTypeFolders = @(
|
|
||||||
"Reports", "DataProcessors", "Documents", "Catalogs",
|
|
||||||
"InformationRegisters", "AccumulationRegisters",
|
|
||||||
"ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcesses", "Tasks", "ExchangePlans"
|
|
||||||
)
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
$candidates = @()
|
|
||||||
foreach ($folder in $objectTypeFolders) {
|
|
||||||
$probe = Join-Path (Join-Path $SrcDir $folder) "$ObjectName.xml"
|
|
||||||
if (Test-Path $probe) { $candidates += (Join-Path $SrcDir $folder) }
|
|
||||||
}
|
|
||||||
if ($candidates.Count -eq 1) {
|
|
||||||
$SrcDir = $candidates[0]
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
Write-Host "[INFO] SrcDir расширен до: $SrcDir"
|
|
||||||
} elseif ($candidates.Count -gt 1) {
|
|
||||||
Write-Error "Объект '$ObjectName' найден в нескольких подпапках: $($candidates -join ', ')`nУкажи SrcDir явно"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
Write-Error "Корневой файл объекта не найден: $rootXmlPath`nОжидается: <SrcDir>/<ObjectName>.xml`nПодсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$templatesDir = Join-Path $processorDir "Templates"
|
|
||||||
$templateMetaPath = Join-Path $templatesDir "$TemplateName.xml"
|
|
||||||
|
|
||||||
if (Test-Path $templateMetaPath) {
|
|
||||||
Write-Error "Макет уже существует: $templateMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Создание каталогов ---
|
|
||||||
|
|
||||||
$templateExtDir = Join-Path (Join-Path $templatesDir $TemplateName) "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $templateExtDir -Force | Out-Null
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
|
||||||
|
|
||||||
# --- 1. Метаданные макета (Templates/<TemplateName>.xml) ---
|
|
||||||
|
|
||||||
$templateUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$templateMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version=`"$formatVersion`">
|
|
||||||
<Template uuid="$templateUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$TemplateName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<TemplateType>$($tmpl.TemplateType)</TemplateType>
|
|
||||||
</Properties>
|
|
||||||
</Template>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($templateMetaPath, $templateMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Содержимое макета (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
|
||||||
|
|
||||||
$templateFilePath = Join-Path $templateExtDir "Template$($tmpl.Ext)"
|
|
||||||
|
|
||||||
switch ($TemplateType) {
|
|
||||||
"HTML" {
|
|
||||||
$content = @"
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
"Text" {
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, "", $encBom)
|
|
||||||
}
|
|
||||||
"SpreadsheetDocument" {
|
|
||||||
$content = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document" xmlns:ss="http://v8.1c.ru/spreadsheet/document" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
||||||
</SpreadsheetDocument>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
"BinaryData" {
|
|
||||||
[System.IO.File]::WriteAllBytes($templateFilePath, @())
|
|
||||||
}
|
|
||||||
"DataCompositionSchema" {
|
|
||||||
$content = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
||||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
|
||||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
|
||||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
||||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
|
||||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<dataSource>
|
|
||||||
<name>ИсточникДанных1</name>
|
|
||||||
<dataSourceType>Local</dataSourceType>
|
|
||||||
</dataSource>
|
|
||||||
</DataCompositionSchema>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Template> в конец ChildObjects
|
|
||||||
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$templateElem.InnerText = $TemplateName
|
|
||||||
|
|
||||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($templateElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
} else {
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($templateElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($templateElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
|
|
||||||
|
|
||||||
$mainDCSUpdated = $false
|
|
||||||
if ($TemplateType -eq "DataCompositionSchema") {
|
|
||||||
# Определяем корневой элемент объекта
|
|
||||||
$reportLikeTypes = @("ExternalReport", "Report")
|
|
||||||
$objectTypeNode = $null
|
|
||||||
$objectTypeName = $null
|
|
||||||
foreach ($rt in $reportLikeTypes) {
|
|
||||||
$node = $xmlDoc.SelectSingleNode("//md:$rt", $nsMgr)
|
|
||||||
if ($node) {
|
|
||||||
$objectTypeNode = $node
|
|
||||||
$objectTypeName = $rt
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($objectTypeNode) {
|
|
||||||
$mainDCS = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:MainDataCompositionSchema", $nsMgr)
|
|
||||||
if ($mainDCS) {
|
|
||||||
$isEmpty = [string]::IsNullOrWhiteSpace($mainDCS.InnerText)
|
|
||||||
if ($isEmpty -or $SetMainSKD) {
|
|
||||||
$objName = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:Name", $nsMgr).InnerText
|
|
||||||
$mainDCS.InnerText = "$objectTypeName.$objName.Template.$TemplateName"
|
|
||||||
$mainDCSUpdated = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
|
|
||||||
Write-Host " Метаданные: $templateMetaPath"
|
|
||||||
Write-Host " Содержимое: $templateFilePath"
|
|
||||||
if ($mainDCSUpdated) {
|
|
||||||
Write-Host " MainDataCompositionSchema: $($mainDCS.InnerText)"
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-template v1.5 — Add template to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
TYPE_MAP = {
|
|
||||||
"HTML": {"TemplateType": "HTMLDocument", "Ext": ".html"},
|
|
||||||
"Text": {"TemplateType": "TextDocument", "Ext": ".txt"},
|
|
||||||
"SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"},
|
|
||||||
"BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"},
|
|
||||||
"DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def detect_format_version(d):
|
|
||||||
while d:
|
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
|
||||||
if os.path.isfile(cfg_path):
|
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
|
||||||
head = f.read(2000)
|
|
||||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return "2.17"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-TemplateName", required=True)
|
|
||||||
parser.add_argument("-TemplateType", required=True,
|
|
||||||
choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"])
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
parser.add_argument("-SetMainSKD", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
template_name = args.TemplateName
|
|
||||||
template_type = args.TemplateType
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else template_name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
set_main_skd = args.SetMainSKD
|
|
||||||
|
|
||||||
tmpl = TYPE_MAP[template_type]
|
|
||||||
|
|
||||||
format_version = detect_format_version(os.path.abspath(src_dir))
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
object_type_folders = [
|
|
||||||
"Reports", "DataProcessors", "Documents", "Catalogs",
|
|
||||||
"InformationRegisters", "AccumulationRegisters",
|
|
||||||
"ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcesses", "Tasks", "ExchangePlans",
|
|
||||||
]
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
candidates = []
|
|
||||||
for folder in object_type_folders:
|
|
||||||
probe = os.path.join(src_dir, folder, f"{object_name}.xml")
|
|
||||||
if os.path.exists(probe):
|
|
||||||
candidates.append(os.path.join(src_dir, folder))
|
|
||||||
if len(candidates) == 1:
|
|
||||||
src_dir = candidates[0]
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
print(f"[INFO] SrcDir расширен до: {src_dir}")
|
|
||||||
elif len(candidates) > 1:
|
|
||||||
print(f"Объект '{object_name}' найден в нескольких подпапках: {', '.join(candidates)}", file=sys.stderr)
|
|
||||||
print(f"Укажи SrcDir явно", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print(f"Корневой файл объекта не найден: {root_xml_path}", file=sys.stderr)
|
|
||||||
print(f"Ожидается: <SrcDir>/<ObjectName>.xml", file=sys.stderr)
|
|
||||||
print(f"Подсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, object_name)
|
|
||||||
templates_dir = os.path.join(processor_dir, "Templates")
|
|
||||||
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
|
|
||||||
|
|
||||||
if os.path.exists(template_meta_path):
|
|
||||||
print(f"Макет уже существует: {template_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
|
|
||||||
template_ext_dir = os.path.join(templates_dir, template_name, "Ext")
|
|
||||||
os.makedirs(template_ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
|
|
||||||
|
|
||||||
template_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
template_meta_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
f' version="{format_version}">\n'
|
|
||||||
f'\t<Template uuid="{template_uuid}">\n'
|
|
||||||
'\t\t<Properties>\n'
|
|
||||||
f'\t\t\t<Name>{template_name}</Name>\n'
|
|
||||||
'\t\t\t<Synonym>\n'
|
|
||||||
'\t\t\t\t<v8:item>\n'
|
|
||||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
|
||||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
|
||||||
'\t\t\t\t</v8:item>\n'
|
|
||||||
'\t\t\t</Synonym>\n'
|
|
||||||
'\t\t\t<Comment/>\n'
|
|
||||||
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
|
|
||||||
'\t\t</Properties>\n'
|
|
||||||
'\t</Template>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(template_meta_path, template_meta_xml)
|
|
||||||
|
|
||||||
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
|
||||||
|
|
||||||
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
|
|
||||||
|
|
||||||
if template_type == "HTML":
|
|
||||||
content = (
|
|
||||||
'<!DOCTYPE html>\n'
|
|
||||||
'<html>\n'
|
|
||||||
'<head>\n'
|
|
||||||
'\t<meta charset="UTF-8">\n'
|
|
||||||
'\t<title></title>\n'
|
|
||||||
'</head>\n'
|
|
||||||
'<body>\n'
|
|
||||||
'</body>\n'
|
|
||||||
'</html>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
elif template_type == "Text":
|
|
||||||
write_text_with_bom(template_file_path, "")
|
|
||||||
|
|
||||||
elif template_type == "SpreadsheetDocument":
|
|
||||||
content = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
|
|
||||||
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema">\n'
|
|
||||||
'</SpreadsheetDocument>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
elif template_type == "BinaryData":
|
|
||||||
with open(template_file_path, "wb") as f:
|
|
||||||
pass # empty file
|
|
||||||
|
|
||||||
elif template_type == "DataCompositionSchema":
|
|
||||||
content = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
|
|
||||||
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
|
|
||||||
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
|
|
||||||
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
|
|
||||||
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
|
|
||||||
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
|
|
||||||
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
|
|
||||||
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
|
|
||||||
'\t<dataSource>\n'
|
|
||||||
'\t\t<name>ИсточникДанных1</name>\n'
|
|
||||||
'\t\t<dataSourceType>Local</dataSourceType>\n'
|
|
||||||
'\t</dataSource>\n'
|
|
||||||
'</DataCompositionSchema>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
# --- 3. Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Template> to end of ChildObjects
|
|
||||||
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
|
|
||||||
template_elem.text = template_name
|
|
||||||
# Remove auto-appended element to reinsert with proper whitespace
|
|
||||||
child_objects.remove(template_elem)
|
|
||||||
|
|
||||||
children = list(child_objects)
|
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
|
||||||
# Empty ChildObjects (self-closing)
|
|
||||||
child_objects.text = "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = "\n\t\t"
|
|
||||||
else:
|
|
||||||
if len(children) > 0:
|
|
||||||
last_child = children[-1]
|
|
||||||
# last_child.tail is the trailing whitespace before </ChildObjects>
|
|
||||||
old_tail = last_child.tail
|
|
||||||
last_child.tail = "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
# Has text content but no element children
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = "\n\t\t"
|
|
||||||
|
|
||||||
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
|
|
||||||
|
|
||||||
main_dcs_updated = False
|
|
||||||
if template_type == "DataCompositionSchema":
|
|
||||||
report_like_types = ["ExternalReport", "Report"]
|
|
||||||
object_type_node = None
|
|
||||||
object_type_name = None
|
|
||||||
for rt in report_like_types:
|
|
||||||
node = root.find(f".//md:{rt}", NSMAP)
|
|
||||||
if node is not None:
|
|
||||||
object_type_node = node
|
|
||||||
object_type_name = rt
|
|
||||||
break
|
|
||||||
|
|
||||||
if object_type_node is not None:
|
|
||||||
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
|
|
||||||
if main_dcs is not None:
|
|
||||||
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
|
|
||||||
if is_empty or set_main_skd:
|
|
||||||
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
|
|
||||||
obj_name = obj_name_node.text if obj_name_node is not None else ""
|
|
||||||
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
|
|
||||||
main_dcs_updated = True
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Создан макет: {template_name} ({template_type})")
|
|
||||||
print(f" Метаданные: {template_meta_path}")
|
|
||||||
print(f" Содержимое: {template_file_path}")
|
|
||||||
if main_dcs_updated:
|
|
||||||
print(f" MainDataCompositionSchema: {main_dcs.text}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# remove-template v1.1 — Remove template from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Remove template from 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-TemplateName", required=True)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
template_name = args.TemplateName
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, object_name)
|
|
||||||
templates_dir = os.path.join(processor_dir, "Templates")
|
|
||||||
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
|
|
||||||
template_dir = os.path.join(templates_dir, template_name)
|
|
||||||
|
|
||||||
if not os.path.exists(template_meta_path):
|
|
||||||
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Delete files ---
|
|
||||||
|
|
||||||
if os.path.isdir(template_dir):
|
|
||||||
shutil.rmtree(template_dir)
|
|
||||||
print(f"[OK] Удалён каталог: {template_dir}")
|
|
||||||
|
|
||||||
os.remove(template_meta_path)
|
|
||||||
print(f"[OK] Удалён файл: {template_meta_path}")
|
|
||||||
|
|
||||||
# --- Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Remove <Template>TemplateName</Template> from ChildObjects
|
|
||||||
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
|
|
||||||
if node.text and node.text.strip() == template_name:
|
|
||||||
parent = node.getparent()
|
|
||||||
prev = node.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Whitespace is in prev.tail
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = ""
|
|
||||||
else:
|
|
||||||
# First child — whitespace is in parent.text
|
|
||||||
if parent.text and parent.text.strip() == "":
|
|
||||||
parent.text = ""
|
|
||||||
parent.remove(node)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Clear MainDataCompositionSchema if it pointed to this template
|
|
||||||
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
|
|
||||||
if main_dcs is not None and main_dcs.text:
|
|
||||||
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
|
|
||||||
main_dcs.text = ""
|
|
||||||
print("[OK] Очищён MainDataCompositionSchema")
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
name: web-info
|
|
||||||
description: Статус Apache и веб-публикаций 1С — запущен ли сервер, какие базы опубликованы, ошибки. Используй когда пользователь спрашивает про статус веб-сервера, опубликованные базы, работает ли Apache
|
|
||||||
argument-hint: ""
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /web-info — Статус Apache и публикаций 1С
|
|
||||||
|
|
||||||
Показывает состояние Apache HTTP Server, список опубликованных баз и последние ошибки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/web-info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры подключения
|
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта. Если задан `webPath` — используй как `-ApachePath`.
|
|
||||||
По умолчанию `tools/apache24` от корня проекта.
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1" <параметры>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры скрипта
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|----------|:------------:|----------|
|
|
||||||
| `-ApachePath <путь>` | нет | Корень Apache (по умолчанию `tools/apache24`) |
|
|
||||||
|
|
||||||
## Формат вывода
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Apache Web Server ===
|
|
||||||
Status: Запущен (PID: 12345)
|
|
||||||
Path: C:\...\tools\apache24
|
|
||||||
Port: 8081
|
|
||||||
Module: C:/Program Files/1cv8/8.3.24.1691/bin/wsap24.dll
|
|
||||||
|
|
||||||
=== Опубликованные базы ===
|
|
||||||
mydb http://localhost:8081/mydb File="C:\Bases\MyDB";
|
|
||||||
|
|
||||||
=== Последние ошибки ===
|
|
||||||
(пусто)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Статус по умолчанию
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1"
|
|
||||||
|
|
||||||
# Указать путь к Apache
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1" -ApachePath "C:\tools\apache24"
|
|
||||||
```
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// web-test cli/commands/status v1.0 — check session
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
|
||||||
import { out } from '../util.mjs';
|
|
||||||
import { SESSION_FILE } from '../session.mjs';
|
|
||||||
|
|
||||||
export function cmdStatus() {
|
|
||||||
if (!existsSync(SESSION_FILE)) {
|
|
||||||
out({ ok: false, message: 'No active session' });
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const sess = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
|
||||||
out({ ok: true, ...sess });
|
|
||||||
}
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
// web-test cli/commands/test v1.3 — regression test runner
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
||||||
import { resolve, dirname, basename, relative } from 'path';
|
|
||||||
import * as browser from '../../browser.mjs';
|
|
||||||
import { out, die, elapsed, slugify, formatDuration, interpolate, printSteps } from '../util.mjs';
|
|
||||||
import { buildContext, buildScopedContext } from '../exec-context.mjs';
|
|
||||||
import { createAssertions } from '../test-runner/assertions.mjs';
|
|
||||||
import { buildSeverityIndex } from '../test-runner/severity.mjs';
|
|
||||||
import { writeAllure, buildJUnit, syncAllureExtras } from '../test-runner/reporters.mjs';
|
|
||||||
import { discoverTests, resetState } from '../test-runner/discover.mjs';
|
|
||||||
|
|
||||||
export async function cmdTest(rawArgs) {
|
|
||||||
// Split off everything after `--` — those args belong to user-defined hooks
|
|
||||||
// (see spec §6: "all arguments after `--` are forwarded verbatim to _hooks.mjs
|
|
||||||
// via the hookArgs field; the runner does not interpret them").
|
|
||||||
const sepIdx = rawArgs.indexOf('--');
|
|
||||||
const ownArgs = sepIdx >= 0 ? rawArgs.slice(0, sepIdx) : rawArgs;
|
|
||||||
const hookArgs = sepIdx >= 0 ? rawArgs.slice(sepIdx + 1) : [];
|
|
||||||
|
|
||||||
// Parse flags
|
|
||||||
const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false };
|
|
||||||
let tags = null, grep = null, urlFlag = null;
|
|
||||||
const positional = [];
|
|
||||||
for (const a of ownArgs) {
|
|
||||||
if (a.startsWith('--tags=')) tags = a.slice(7).split(',');
|
|
||||||
else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i');
|
|
||||||
else if (a.startsWith('--url=')) urlFlag = a.slice(6);
|
|
||||||
else if (a === '--bail') opts.bail = true;
|
|
||||||
else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0;
|
|
||||||
else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000;
|
|
||||||
else if (a.startsWith('--report=')) opts.report = a.slice(9);
|
|
||||||
else if (a.startsWith('--format=')) opts.format = a.slice(9);
|
|
||||||
else if (a.startsWith('--screenshot=')) opts.screenshot = a.slice(13);
|
|
||||||
else if (a.startsWith('--report-dir=')) opts.reportDir = a.slice(13);
|
|
||||||
else if (a === '--record') opts.record = true;
|
|
||||||
else if (!a.startsWith('--')) positional.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positional args are ALWAYS test paths (one or many). URL comes from --url= or config
|
|
||||||
// (see webtest.config.mjs). This matches pytest/jest/playwright; a positional that looks
|
|
||||||
// like a URL is a mistake → fail fast with a hint instead of feeding it to page.goto().
|
|
||||||
const isUrl = (s) => /^https?:\/\//i.test(s);
|
|
||||||
let url = urlFlag || null;
|
|
||||||
const testPaths = [...positional];
|
|
||||||
if (testPaths.length === 0) {
|
|
||||||
die('Usage: node run.mjs test <dir|file>... [--url=URL] [--tags=...] [--grep=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
|
|
||||||
}
|
|
||||||
for (const p of testPaths) {
|
|
||||||
if (existsSync(resolve(p))) continue;
|
|
||||||
if (isUrl(p)) {
|
|
||||||
die(`"${p}" looks like a URL — use --url=<url>; positional args are test paths.`);
|
|
||||||
}
|
|
||||||
die(`Test path not found: "${p}". To run a subset use --grep= / --tags=, or pass an existing dir/file.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config if exists. config (webtest.config.mjs) and hooks (_hooks.mjs) resolve from
|
|
||||||
// the FIRST path's directory — list paths from the same suite folder.
|
|
||||||
const firstPath = resolve(testPaths[0]);
|
|
||||||
const isFile = firstPath.endsWith('.test.mjs');
|
|
||||||
const testDir = isFile ? dirname(firstPath) : firstPath;
|
|
||||||
const configPath = resolve(testDir, 'webtest.config.mjs');
|
|
||||||
let config = {};
|
|
||||||
if (existsSync(configPath)) {
|
|
||||||
const mod = await import('file:///' + configPath.replace(/\\/g, '/'));
|
|
||||||
config = mod.default || {};
|
|
||||||
}
|
|
||||||
const severityIndex = buildSeverityIndex(config);
|
|
||||||
|
|
||||||
// Build context registry: name → url. Supports config.contexts or single config.url / CLI url.
|
|
||||||
const contextSpecs = {};
|
|
||||||
let defaultContextName = 'default';
|
|
||||||
const defaultIsolation = config.isolation || 'tab';
|
|
||||||
if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) {
|
|
||||||
for (const [n, spec] of Object.entries(config.contexts)) {
|
|
||||||
contextSpecs[n] = { ...spec };
|
|
||||||
}
|
|
||||||
defaultContextName = config.defaultContext || Object.keys(config.contexts)[0];
|
|
||||||
if (url) contextSpecs[defaultContextName] = { ...contextSpecs[defaultContextName], url };
|
|
||||||
} else {
|
|
||||||
const fallbackUrl = url || config.url;
|
|
||||||
if (!fallbackUrl) die('No URL provided and no webtest.config.mjs found');
|
|
||||||
contextSpecs.default = { url: fallbackUrl };
|
|
||||||
}
|
|
||||||
if (!contextSpecs[defaultContextName]) {
|
|
||||||
die(`defaultContext "${defaultContextName}" not found in contexts: [${Object.keys(contextSpecs).join(', ')}]`);
|
|
||||||
}
|
|
||||||
if (!url) url = contextSpecs[defaultContextName].url;
|
|
||||||
|
|
||||||
// Apply config defaults (CLI flags override)
|
|
||||||
if (!tags && config.tags) tags = config.tags;
|
|
||||||
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
|
|
||||||
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
|
|
||||||
if (config.preserveClipboard === false && !ownArgs.includes('--no-preserve-clipboard')) {
|
|
||||||
browser.setPreserveClipboard(false);
|
|
||||||
}
|
|
||||||
opts.record = opts.record || !!config.record;
|
|
||||||
opts.screenshot = opts.screenshot || config.screenshot || 'on-failure';
|
|
||||||
if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) {
|
|
||||||
die(`Invalid --screenshot=${opts.screenshot} (expected on-failure|every-step|off)`);
|
|
||||||
}
|
|
||||||
if (!['json', 'allure', 'junit'].includes(opts.format)) {
|
|
||||||
die(`Invalid --format=${opts.format} (expected json|allure|junit)`);
|
|
||||||
}
|
|
||||||
if (opts.format === 'junit' && !opts.report) {
|
|
||||||
die('--format=junit requires --report=path.xml');
|
|
||||||
}
|
|
||||||
// `--report=-` means "machine report to stdout" (Unix `-` convention).
|
|
||||||
// Only meaningful for streamable formats (json/junit); allure is a directory.
|
|
||||||
const reportToStdout = opts.report === '-';
|
|
||||||
if (reportToStdout && opts.format === 'allure') {
|
|
||||||
die('--report=- (stdout) is not valid with --format=allure: allure emits a directory of files, not a single stream. Use --report-dir=<dir> instead.');
|
|
||||||
}
|
|
||||||
const reportDir = opts.reportDir
|
|
||||||
? resolve(opts.reportDir)
|
|
||||||
: (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir);
|
|
||||||
if (opts.screenshot !== 'off') {
|
|
||||||
try { mkdirSync(reportDir, { recursive: true }); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover test files
|
|
||||||
const testFiles = discoverTests(testPaths);
|
|
||||||
if (!testFiles.length) die(`No *.test.mjs files found in ${testPaths.join(', ')}`);
|
|
||||||
|
|
||||||
// Import and filter tests
|
|
||||||
const tests = [];
|
|
||||||
let hasOnly = false;
|
|
||||||
for (const file of testFiles) {
|
|
||||||
const mod = await import('file:///' + file.replace(/\\/g, '/'));
|
|
||||||
const base = {
|
|
||||||
file: relative(testDir, file).replace(/\\/g, '/'),
|
|
||||||
name: mod.name || basename(file, '.test.mjs'),
|
|
||||||
tags: mod.tags || [],
|
|
||||||
timeout: mod.timeout || opts.timeout,
|
|
||||||
skip: mod.skip || false,
|
|
||||||
only: mod.only || false,
|
|
||||||
setup: mod.setup,
|
|
||||||
teardown: mod.teardown,
|
|
||||||
fn: mod.default,
|
|
||||||
param: undefined,
|
|
||||||
context: mod.context || null,
|
|
||||||
contexts: Array.isArray(mod.contexts) ? mod.contexts : null,
|
|
||||||
severity: typeof mod.severity === 'string' ? mod.severity : null,
|
|
||||||
};
|
|
||||||
if (base.only) hasOnly = true;
|
|
||||||
if (Array.isArray(mod.params) && mod.params.length) {
|
|
||||||
for (let i = 0; i < mod.params.length; i++) {
|
|
||||||
const p = mod.params[i];
|
|
||||||
const name = base.name.includes('{') ? interpolate(base.name, p) : `${base.name}[${i}]`;
|
|
||||||
tests.push({ ...base, name, param: p });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tests.push(base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
const filtered = tests.filter(t => {
|
|
||||||
if (hasOnly && !t.only) return false;
|
|
||||||
if (tags && !tags.some(tag => t.tags.includes(tag))) return false;
|
|
||||||
if (grep && !grep.test(t.name)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load hooks
|
|
||||||
const hooksPath = resolve(testDir, '_hooks.mjs');
|
|
||||||
let hooks = {};
|
|
||||||
if (existsSync(hooksPath)) {
|
|
||||||
hooks = await import('file:///' + hooksPath.replace(/\\/g, '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human-readable report goes to stdout (test-runner convention: jest/pytest/playwright).
|
|
||||||
// In `--report -` mode the machine JSON/XML takes over stdout, so progress moves to stderr.
|
|
||||||
const W = reportToStdout ? process.stderr : process.stdout;
|
|
||||||
W.write(`\nweb-test -- ${url}\n`);
|
|
||||||
W.write(`Running ${filtered.length} tests from ${relative(process.cwd(), testDir).replace(/\\/g, '/') || '.'}/\n\n`);
|
|
||||||
|
|
||||||
const startedAt = new Date().toISOString();
|
|
||||||
const results = [];
|
|
||||||
let passCount = 0, failCount = 0, skipCount = 0;
|
|
||||||
|
|
||||||
const hookLog = (...a) => W.write(`[hooks] ${a.map(String).join(' ')}\n`);
|
|
||||||
const hookEnv = { hookArgs, log: hookLog, config };
|
|
||||||
if (hooks.prepare) await hooks.prepare(hookEnv);
|
|
||||||
|
|
||||||
// Lazy context creation
|
|
||||||
async function ensureContext(name) {
|
|
||||||
if (browser.hasContext(name)) return;
|
|
||||||
const spec = contextSpecs[name];
|
|
||||||
if (!spec) throw new Error(`Unknown context "${name}". Defined: [${Object.keys(contextSpecs).join(', ')}]`);
|
|
||||||
await browser.createContext(name, spec.url, { isolation: spec.isolation || defaultIsolation });
|
|
||||||
if (hooks.afterOpenContext && hookCtx) {
|
|
||||||
try { await hooks.afterOpenContext(hookCtx, name, spec); }
|
|
||||||
catch (e) { hookLog(`afterOpenContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hookCtx = null;
|
|
||||||
|
|
||||||
function wrapCloseContextHook(target) {
|
|
||||||
const orig = target.closeContext;
|
|
||||||
if (typeof orig !== 'function') return;
|
|
||||||
target.closeContext = async (name) => {
|
|
||||||
if (hooks.beforeCloseContext) {
|
|
||||||
try { await hooks.beforeCloseContext(target, name, contextSpecs[name]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
return await orig(name);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Connect: create default context up front
|
|
||||||
await ensureContext(defaultContextName);
|
|
||||||
|
|
||||||
const ctx = buildContext({ noRecord: false });
|
|
||||||
ctx.assert = createAssertions();
|
|
||||||
ctx.log = (...a) => { /* per-test, overridden below */ };
|
|
||||||
wrapCloseContextHook(ctx);
|
|
||||||
hookCtx = ctx;
|
|
||||||
|
|
||||||
// Default context was created BEFORE hookCtx existed → fire afterOpenContext now.
|
|
||||||
if (hooks.afterOpenContext) {
|
|
||||||
try { await hooks.afterOpenContext(ctx, defaultContextName, contextSpecs[defaultContextName]); }
|
|
||||||
catch (e) { hookLog(`afterOpenContext("${defaultContextName}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hooks.beforeAll) await hooks.beforeAll(ctx);
|
|
||||||
|
|
||||||
let testIdx = 0;
|
|
||||||
for (const t of filtered) {
|
|
||||||
testIdx++;
|
|
||||||
const declaredContexts = t.contexts && t.contexts.length
|
|
||||||
? t.contexts
|
|
||||||
: [t.context || defaultContextName];
|
|
||||||
|
|
||||||
if (t.skip) {
|
|
||||||
const reason = typeof t.skip === 'string' ? t.skip : '';
|
|
||||||
W.write(` ○ ${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`);
|
|
||||||
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null });
|
|
||||||
skipCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testContextNames = declaredContexts;
|
|
||||||
try {
|
|
||||||
for (const cn of testContextNames) await ensureContext(cn);
|
|
||||||
await browser.setActiveContext(testContextNames[0]);
|
|
||||||
} catch (e) {
|
|
||||||
W.write(` ✗ ${t.name} (context setup failed: ${e.message})\n`);
|
|
||||||
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null });
|
|
||||||
failCount++;
|
|
||||||
if (opts.bail) break;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastError = null;
|
|
||||||
let testResult = null;
|
|
||||||
const maxAttempts = 1 + opts.retry;
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
const output = [];
|
|
||||||
let steps = [];
|
|
||||||
let currentSteps = steps;
|
|
||||||
let stepIdx = 0;
|
|
||||||
const t0 = Date.now();
|
|
||||||
|
|
||||||
ctx.testInfo = {
|
|
||||||
name: t.name,
|
|
||||||
file: basename(t.file),
|
|
||||||
filePath: t.file,
|
|
||||||
tags: t.tags,
|
|
||||||
timeout: t.timeout,
|
|
||||||
attempt,
|
|
||||||
maxAttempts,
|
|
||||||
param: t.param,
|
|
||||||
contexts: Object.fromEntries(testContextNames.map(n => [n, contextSpecs[n]])),
|
|
||||||
primaryContext: testContextNames[0],
|
|
||||||
};
|
|
||||||
ctx.testResult = null;
|
|
||||||
|
|
||||||
let videoFile = null;
|
|
||||||
if (opts.record) {
|
|
||||||
videoFile = resolve(reportDir, `${testIdx}-${slugify(t.name)}.mp4`);
|
|
||||||
try { await browser.startRecording(videoFile, { force: true }); } catch { videoFile = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.log = (...a) => output.push(a.map(String).join(' '));
|
|
||||||
ctx.step = async (name, fn) => {
|
|
||||||
const s = { name, start: Date.now(), status: 'passed', steps: [] };
|
|
||||||
currentSteps.push(s);
|
|
||||||
const prev = currentSteps;
|
|
||||||
currentSteps = s.steps;
|
|
||||||
stepIdx++;
|
|
||||||
const myIdx = stepIdx;
|
|
||||||
try {
|
|
||||||
await fn();
|
|
||||||
} catch (e) {
|
|
||||||
s.status = 'failed';
|
|
||||||
s.error = e.message;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
s.stop = Date.now();
|
|
||||||
currentSteps = prev;
|
|
||||||
if (opts.screenshot === 'every-step' && s.status === 'passed') {
|
|
||||||
try {
|
|
||||||
const slug = slugify(name);
|
|
||||||
const file = resolve(reportDir, `${testIdx}-${myIdx}-${slug}.png`);
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
writeFileSync(file, png);
|
|
||||||
s.screenshot = file;
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scopedKeys = [];
|
|
||||||
if (t.contexts && t.contexts.length) {
|
|
||||||
for (const cn of t.contexts) {
|
|
||||||
ctx[cn] = buildScopedContext(cn);
|
|
||||||
wrapCloseContextHook(ctx[cn]);
|
|
||||||
scopedKeys.push(cn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hooks.beforeEach) await hooks.beforeEach(ctx);
|
|
||||||
if (t.setup) await t.setup(ctx);
|
|
||||||
|
|
||||||
let timeoutTimer;
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
t.fn(ctx, t.param),
|
|
||||||
new Promise((_, reject) => { timeoutTimer = setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout); }),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
// Clear the guard timer — otherwise it stays armed in the event loop and,
|
|
||||||
// since the success path never calls process.exit(), node can't exit until
|
|
||||||
// it fires (up to `timeout` ms after the last test finished).
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.teardown) try { await t.teardown(ctx); } catch {}
|
|
||||||
ctx.testResult = { status: 'passed', duration: elapsed(t0), attempts: attempt, error: null, steps };
|
|
||||||
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
|
|
||||||
for (const cn of testContextNames) {
|
|
||||||
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
|
|
||||||
}
|
|
||||||
for (const k of scopedKeys) delete ctx[k];
|
|
||||||
|
|
||||||
if (videoFile) {
|
|
||||||
try { await browser.stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
const dur = elapsed(t0);
|
|
||||||
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'passed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: null, screenshot: null, video: videoFile };
|
|
||||||
lastError = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
// Screenshot on failure FIRST — before teardown/afterEach/resetState reset the UI.
|
|
||||||
let shotFile = e.onecError?.screenshot;
|
|
||||||
if (!shotFile && opts.screenshot !== 'off') {
|
|
||||||
try {
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`);
|
|
||||||
writeFileSync(shotFile, png);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.teardown) try { await t.teardown(ctx); } catch {}
|
|
||||||
const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile, onecError: e.onecError };
|
|
||||||
ctx.testResult = { status: 'failed', duration: elapsed(t0), attempts: attempt, error: errInfo, steps };
|
|
||||||
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
|
|
||||||
for (const cn of testContextNames) {
|
|
||||||
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
|
|
||||||
}
|
|
||||||
for (const k of scopedKeys) delete ctx[k];
|
|
||||||
|
|
||||||
if (videoFile) {
|
|
||||||
try { await browser.stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
lastError = errInfo;
|
|
||||||
const dur = elapsed(t0);
|
|
||||||
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: errInfo, screenshot: shotFile, video: videoFile };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(testResult);
|
|
||||||
|
|
||||||
if (testResult.status === 'passed') {
|
|
||||||
passCount++;
|
|
||||||
W.write(` ✓ ${t.name} (${testResult.duration}s)\n`);
|
|
||||||
} else {
|
|
||||||
failCount++;
|
|
||||||
W.write(` ✗ ${t.name} (${testResult.duration}s)\n`);
|
|
||||||
printSteps(W, testResult.steps, ' ');
|
|
||||||
if (lastError?.message) W.write(` ${lastError.message}\n`);
|
|
||||||
if (lastError?.screenshot) W.write(` screenshot: ${lastError.screenshot}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.bail && testResult.status === 'failed') break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hooks.afterAll) try { await hooks.afterAll(ctx); } catch {}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Per-context teardown
|
|
||||||
try {
|
|
||||||
const remaining = browser.listContexts();
|
|
||||||
if (remaining.length > 0) {
|
|
||||||
const survivor = remaining[0];
|
|
||||||
try { await browser.setActiveContext(survivor); } catch {}
|
|
||||||
for (let i = remaining.length - 1; i >= 1; i--) {
|
|
||||||
const name = remaining[i];
|
|
||||||
if (hooks.beforeCloseContext && hookCtx) {
|
|
||||||
try { await hooks.beforeCloseContext(hookCtx, name, contextSpecs[name]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
try { await browser.closeContext(name); }
|
|
||||||
catch (e) { hookLog(`closeContext("${name}") failed: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
if (hooks.beforeCloseContext && hookCtx) {
|
|
||||||
try { await hooks.beforeCloseContext(hookCtx, survivor, contextSpecs[survivor]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${survivor}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
hookLog(`final teardown loop failed: ${e.message.split('\n')[0]}`);
|
|
||||||
}
|
|
||||||
try { await browser.disconnect(); } catch {}
|
|
||||||
if (hooks.cleanup) try { await hooks.cleanup(hookEnv); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishedAt = new Date().toISOString();
|
|
||||||
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
||||||
|
|
||||||
W.write(`\n${passCount} passed, ${failCount} failed, ${skipCount} skipped (${formatDuration(totalDuration)})\n\n`);
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
runner: 'web-test', url, startedAt, finishedAt,
|
|
||||||
duration: totalDuration,
|
|
||||||
summary: { total: results.length, passed: passCount, failed: failCount, skipped: skipCount },
|
|
||||||
tests: results,
|
|
||||||
};
|
|
||||||
if (opts.format === 'allure') {
|
|
||||||
writeAllure(results, reportDir, severityIndex);
|
|
||||||
syncAllureExtras(testDir, reportDir);
|
|
||||||
} else if (opts.format === 'junit') {
|
|
||||||
if (reportToStdout) process.stdout.write(buildJUnit(report, testDir) + '\n');
|
|
||||||
else writeFileSync(resolve(opts.report), buildJUnit(report, testDir));
|
|
||||||
} else if (reportToStdout) {
|
|
||||||
out(report);
|
|
||||||
} else if (opts.report) {
|
|
||||||
writeFileSync(resolve(opts.report), JSON.stringify(report, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failCount > 0) process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, readdirSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
|
|
||||||
// taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
|
|
||||||
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
|
|
||||||
// (00-, 01-, …) even when paths are listed out of order.
|
|
||||||
export function discoverTests(testPaths) {
|
|
||||||
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
|
|
||||||
const files = [];
|
|
||||||
function walk(dir) {
|
|
||||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
||||||
const full = resolve(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(full);
|
|
||||||
else if (entry.name.endsWith('.test.mjs')) files.push(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const p of paths) {
|
|
||||||
const full = resolve(p);
|
|
||||||
if (full.endsWith('.test.mjs')) {
|
|
||||||
if (existsSync(full)) files.push(full);
|
|
||||||
} else if (existsSync(full)) {
|
|
||||||
walk(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...new Set(files)].sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resetState(ctx) {
|
|
||||||
try { if (typeof ctx.dismissPendingErrors === 'function') await ctx.dismissPendingErrors(); } catch {}
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
try {
|
|
||||||
const state = await ctx.getFormState();
|
|
||||||
// form === null means no form open (desktop). form === 0 is a real background form
|
|
||||||
// 1C exposes in some states — must still close it to fully reset.
|
|
||||||
if (state.form == null) break;
|
|
||||||
await ctx.closeForm({ save: false });
|
|
||||||
} catch { break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
// web-test dom shared v1.0 — embedded JS function constants
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
/**
|
|
||||||
* Shared function strings embedded into page.evaluate() generators.
|
|
||||||
* Не экспортируются наружу через dom.mjs facade — внутренняя кухня.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Find visible #modalSurface. 1C may leave multiple #modalSurface in DOM (duplicate id),
|
|
||||||
* e.g. when a second form (drill-down) creates its own alongside a stale one from the first
|
|
||||||
* form. getElementById returns the FIRST in document order, which may be hidden. Scan all. */
|
|
||||||
export const HAS_VISIBLE_MODAL_FN = `function hasVisibleModal() {
|
|
||||||
const all = document.querySelectorAll('#modalSurface');
|
|
||||||
for (const el of all) { if (el.offsetWidth > 0) return true; }
|
|
||||||
return false;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
|
||||||
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
|
||||||
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
|
||||||
function detectForm() {
|
|
||||||
const counts = {};
|
|
||||||
document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const m = el.id.match(/^form(\\d+)_/);
|
|
||||||
if (m) counts[m[1]] = (counts[m[1]] || 0) + 1;
|
|
||||||
});
|
|
||||||
const nums = Object.keys(counts).map(Number);
|
|
||||||
if (!nums.length) return null;
|
|
||||||
const candidates = nums.filter(n => n > 0);
|
|
||||||
if (!candidates.length) return nums[0];
|
|
||||||
// When modal surface is visible, prefer the highest-numbered form (modal dialog)
|
|
||||||
if (hasVisibleModal()) {
|
|
||||||
const maxForm = Math.max(...candidates);
|
|
||||||
if (counts[maxForm] >= 1) return maxForm;
|
|
||||||
}
|
|
||||||
return candidates.reduce((best, n) => counts[n] > counts[best] ? n : best);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Detect all open forms + modal state. Returns { activeForm, allForms, formCount, modal }.
|
|
||||||
* Works even when the open-windows tab bar is hidden. */
|
|
||||||
export const DETECT_FORMS_FN = HAS_VISIBLE_MODAL_FN + `
|
|
||||||
function detectForms() {
|
|
||||||
const counts = {};
|
|
||||||
document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const m = el.id.match(/^form(\\d+)_/);
|
|
||||||
if (m) counts[m[1]] = (counts[m[1]] || 0) + 1;
|
|
||||||
});
|
|
||||||
const nums = Object.keys(counts).map(Number);
|
|
||||||
return { allForms: nums.sort((a, b) => a - b), formCount: nums.length, modal: hasVisibleModal() };
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
|
||||||
export const READ_FORM_FN = `function readForm(p) {
|
|
||||||
const result = {};
|
|
||||||
const fields = [];
|
|
||||||
const buttons = [];
|
|
||||||
const formTabs = [];
|
|
||||||
const texts = [];
|
|
||||||
const hyperlinks = [];
|
|
||||||
// Normalize non-breaking spaces to regular spaces
|
|
||||||
const nbsp = s => (s || '').replace(/\\u00a0/g, ' ');
|
|
||||||
|
|
||||||
// Fields (inputs)
|
|
||||||
document.querySelectorAll('input.editInput[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '').replace(/_i\\d+$/, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text')
|
|
||||||
|| document.getElementById(p + name + '#title_div');
|
|
||||||
const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' '));
|
|
||||||
const actions = [];
|
|
||||||
if (document.getElementById(p + name + '_DLB')?.offsetWidth > 0) actions.push('select');
|
|
||||||
if (document.getElementById(p + name + '_OB')?.offsetWidth > 0) actions.push('open');
|
|
||||||
if (document.getElementById(p + name + '_CLR')?.offsetWidth > 0) actions.push('clear');
|
|
||||||
if (document.getElementById(p + name + '_CB')?.offsetWidth > 0) actions.push('pick');
|
|
||||||
const field = { name, value: el.value || '' };
|
|
||||||
// Multi-value reference fields keep their value in .chipsItem chips, not in input.value
|
|
||||||
if (!field.value) {
|
|
||||||
const labelEl = document.getElementById(p + name);
|
|
||||||
if (labelEl) {
|
|
||||||
const chipTexts = [...labelEl.querySelectorAll('.chipsItem .chipsTitle')]
|
|
||||||
.map(c => nbsp(c.innerText?.trim() || ''))
|
|
||||||
.filter(Boolean);
|
|
||||||
if (chipTexts.length) field.value = chipTexts.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
if (el.readOnly) field.readonly = true;
|
|
||||||
if (el.disabled) field.disabled = true;
|
|
||||||
if (el.type && el.type !== 'text') field.type = el.type;
|
|
||||||
if (document.activeElement === el) field.focused = true;
|
|
||||||
if (actions.length) field.actions = actions;
|
|
||||||
if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Textareas
|
|
||||||
document.querySelectorAll('textarea[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '').replace(/_i\\d+$/, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text')
|
|
||||||
|| document.getElementById(p + name + '#title_div');
|
|
||||||
const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' '));
|
|
||||||
const field = { name, value: el.value || '', type: 'textarea' };
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
if (el.readOnly) field.readonly = true;
|
|
||||||
if (el.disabled) field.disabled = true;
|
|
||||||
if (document.activeElement === el) field.focused = true;
|
|
||||||
if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Checkboxes
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].checkbox').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = nbsp(titleEl?.innerText?.trim() || '');
|
|
||||||
const field = {
|
|
||||||
name,
|
|
||||||
value: el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select'),
|
|
||||||
type: 'checkbox'
|
|
||||||
};
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Radio buttons — base element is option 0, others are #N#radio (N >= 1)
|
|
||||||
const radioGroups = {};
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].radio').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const id = el.id.replace(p, '');
|
|
||||||
const m = id.match(/^(.+?)#(\\d+)#radio$/);
|
|
||||||
if (m) {
|
|
||||||
// Options 1, 2, ... have explicit #N#radio suffix
|
|
||||||
const [, groupName, idx] = m;
|
|
||||||
if (!radioGroups[groupName]) radioGroups[groupName] = [];
|
|
||||||
const labelEl = document.getElementById(p + groupName + '#' + idx + '#radio_text');
|
|
||||||
const label = nbsp(labelEl?.innerText?.trim() || 'option' + idx);
|
|
||||||
radioGroups[groupName].push({ index: parseInt(idx), label, selected: el.classList.contains('select') });
|
|
||||||
} else if (!id.includes('#')) {
|
|
||||||
// Base element = option 0 (no #0#radio suffix)
|
|
||||||
if (!radioGroups[id]) radioGroups[id] = [];
|
|
||||||
const labelEl = document.getElementById(p + id + '#0#radio_text');
|
|
||||||
const label = nbsp(labelEl?.innerText?.trim() || 'option0');
|
|
||||||
radioGroups[id].unshift({ index: 0, label, selected: el.classList.contains('select') });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (const [name, options] of Object.entries(radioGroups)) {
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = titleEl?.innerText?.trim() || '';
|
|
||||||
const selected = options.find(o => o.selected);
|
|
||||||
const field = {
|
|
||||||
name,
|
|
||||||
value: selected?.label || '',
|
|
||||||
type: 'radio',
|
|
||||||
options: options.map(o => o.label)
|
|
||||||
};
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
fields.push(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons (a.press)
|
|
||||||
document.querySelectorAll('a.press[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const idName = el.id.replace(p, '');
|
|
||||||
if (/_(?:DLB|CLR|OB|CB)$/.test(idName)) return;
|
|
||||||
const span = el.querySelector('.submenuText') || el.querySelector('span');
|
|
||||||
const text = nbsp(span?.textContent?.trim() || el.innerText?.trim() || '');
|
|
||||||
if (!text && !el.classList.contains('pressCommand')) return;
|
|
||||||
const btn = { name: text || idName };
|
|
||||||
if (el.classList.contains('pressDefault')) btn.default = true;
|
|
||||||
if (el.classList.contains('pressDisabled')) btn.disabled = true;
|
|
||||||
// Icon-only buttons: expose tooltip from DOM title attribute (1C puts title on parent .framePress)
|
|
||||||
if (!text) {
|
|
||||||
const tip = nbsp(el.title || el.parentElement?.title || '');
|
|
||||||
if (tip) btn.tooltip = tip;
|
|
||||||
}
|
|
||||||
buttons.push(btn);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Frame buttons
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].frameButton, [id^="' + p + '"] .frameButton').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const text = nbsp(el.innerText?.trim() || '');
|
|
||||||
const idName = el.id?.replace(p, '') || '';
|
|
||||||
if (!text && !idName) return;
|
|
||||||
buttons.push({ name: text || idName, frame: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tumbler items
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].tumblerItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const text = el.innerText?.trim();
|
|
||||||
const idName = el.id?.replace(p, '') || '';
|
|
||||||
buttons.push({ name: text || idName, tumbler: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tabs — scoped to form by checking ancestor IDs
|
|
||||||
document.querySelectorAll('[data-content]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
let node = el.parentElement;
|
|
||||||
let inForm = false;
|
|
||||||
while (node) {
|
|
||||||
if (node.id && node.id.startsWith(p)) { inForm = true; break; }
|
|
||||||
node = node.parentElement;
|
|
||||||
}
|
|
||||||
if (!inForm) return;
|
|
||||||
const tab = { name: el.dataset.content };
|
|
||||||
if (el.classList.contains('select')) tab.active = true;
|
|
||||||
formTabs.push(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Static texts and hyperlinks
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].staticText').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '');
|
|
||||||
if (name.endsWith('_div') || name.includes('#title')) return;
|
|
||||||
const text = el.innerText?.trim();
|
|
||||||
if (!text) return;
|
|
||||||
if (el.classList.contains('staticTextHyper')) {
|
|
||||||
hyperlinks.push({ name: text });
|
|
||||||
} else {
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = titleEl?.innerText?.trim() || '';
|
|
||||||
const entry = { name, value: text };
|
|
||||||
if (label) entry.label = label;
|
|
||||||
texts.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tables/grids — collect ALL visible grids
|
|
||||||
const allGrids = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
|
||||||
.filter(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
|
||||||
if (allGrids.length > 0) {
|
|
||||||
const tables = allGrids.map(grid => {
|
|
||||||
const name = grid.id ? grid.id.replace(p, '') : '';
|
|
||||||
const head = grid.querySelector('.gridHead');
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
const columns = [];
|
|
||||||
if (head) {
|
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
|
||||||
[...headLine.children].forEach(box => {
|
|
||||||
if (box.offsetWidth === 0) return;
|
|
||||||
const textEl = box.querySelector('.gridBoxText');
|
|
||||||
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
|
||||||
if (text) {
|
|
||||||
const r = box.getBoundingClientRect();
|
|
||||||
columns.push({ text, x: r.x, right: r.x + r.width, y: r.y, h: r.height });
|
|
||||||
} else {
|
|
||||||
// Unnamed column — check if data cells contain checkboxes
|
|
||||||
const firstLine = body?.querySelector('.gridLine');
|
|
||||||
if (firstLine) {
|
|
||||||
const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0);
|
|
||||||
const idx = visibleHeaders.indexOf(box);
|
|
||||||
const cells = [...firstLine.children].filter(c => c.offsetWidth > 0);
|
|
||||||
if (cells[idx]?.querySelector('.checkbox')) {
|
|
||||||
columns.push({ text: '(checkbox)', x: 0, right: 0, y: 0, h: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Expand single merged headers with multiple data sub-rows (e.g. "Субконто Дт" → 1/2/3)
|
|
||||||
const firstLine = body?.querySelector('.gridLine');
|
|
||||||
if (firstLine && columns.length > 0) {
|
|
||||||
const xGrp = new Map();
|
|
||||||
columns.forEach(c => {
|
|
||||||
const k = Math.round(c.x) + ':' + Math.round(c.right);
|
|
||||||
if (!xGrp.has(k)) xGrp.set(k, []);
|
|
||||||
xGrp.get(k).push(c);
|
|
||||||
});
|
|
||||||
for (const [k, hdrs] of xGrp) {
|
|
||||||
if (hdrs.length !== 1) continue;
|
|
||||||
let cnt = 0;
|
|
||||||
[...firstLine.children].forEach(box => {
|
|
||||||
if (box.offsetWidth === 0) return;
|
|
||||||
const r = box.getBoundingClientRect();
|
|
||||||
const cx = r.x + r.width / 2;
|
|
||||||
if (cx >= hdrs[0].x && cx < hdrs[0].right) cnt++;
|
|
||||||
});
|
|
||||||
if (cnt > 1) {
|
|
||||||
const base = hdrs[0];
|
|
||||||
const baseIdx = columns.indexOf(base);
|
|
||||||
columns.splice(baseIdx, 1);
|
|
||||||
for (let si = 0; si < cnt; si++) {
|
|
||||||
columns.splice(baseIdx + si, 0, { text: base.text + ' ' + (si + 1), x: base.x, right: base.right, y: 0, h: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const colNames = columns.map(c => c.text);
|
|
||||||
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
|
||||||
// Visual label from group title (e.g. "Входящие:" for grid "Входящие")
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_div')
|
|
||||||
|| document.getElementById(p + 'Группа' + name + '#title_div');
|
|
||||||
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null;
|
|
||||||
return { name, columns: colNames, rowCount, ...(label ? { label } : {}) };
|
|
||||||
});
|
|
||||||
result.tables = tables;
|
|
||||||
// Backward compat: table = first grid summary
|
|
||||||
const first = tables[0];
|
|
||||||
result.table = { present: true, columns: first.columns, rowCount: first.rowCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active filters (train badges above grid: *СостояниеПросмотра)
|
|
||||||
const filters = [];
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].trainItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const titleEl = el.querySelector('.trainName');
|
|
||||||
const valueEl = el.querySelector('.trainTitle');
|
|
||||||
if (!titleEl && !valueEl) return;
|
|
||||||
const field = (titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ').replace(/\\s*:$/, '').trim();
|
|
||||||
const value = valueEl?.innerText?.trim()?.replace(/\\n/g, ' ') || '';
|
|
||||||
if (field || value) filters.push({ field, value });
|
|
||||||
});
|
|
||||||
// Also check search field value
|
|
||||||
const searchInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
|
||||||
if (searchInput?.value) {
|
|
||||||
filters.push({ type: 'search', value: searchInput.value });
|
|
||||||
}
|
|
||||||
if (filters.length) result.filters = filters;
|
|
||||||
|
|
||||||
// Navigation panel (FormNavigationPanel) — lives in parent page{N} container
|
|
||||||
const navigation = [];
|
|
||||||
const formEl = document.querySelector('[id^="' + p + '"]');
|
|
||||||
if (formEl) {
|
|
||||||
let pageEl = formEl.parentElement;
|
|
||||||
while (pageEl && !(pageEl.id && /^page\\d+$/.test(pageEl.id))) pageEl = pageEl.parentElement;
|
|
||||||
if (pageEl) {
|
|
||||||
pageEl.querySelectorAll('.navigationItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const nameEl = el.querySelector('.navigationItemName');
|
|
||||||
const text = (nameEl?.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
|
||||||
if (!text) return;
|
|
||||||
const nav = { name: text };
|
|
||||||
if (el.classList.contains('select')) nav.active = true;
|
|
||||||
navigation.push(nav);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iframes
|
|
||||||
let iframeCount = 0;
|
|
||||||
document.querySelectorAll('[id^="' + p + '"] iframe, iframe[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth > 0 && el.offsetHeight > 0) iframeCount++;
|
|
||||||
});
|
|
||||||
if (iframeCount) result.iframes = iframeCount;
|
|
||||||
|
|
||||||
if (fields.length) result.fields = fields;
|
|
||||||
if (buttons.length) result.buttons = buttons;
|
|
||||||
if (formTabs.length) result.tabs = formTabs;
|
|
||||||
if (navigation.length) result.navigation = navigation;
|
|
||||||
if (texts.length) result.texts = texts;
|
|
||||||
if (hyperlinks.length) result.hyperlinks = hyperlinks;
|
|
||||||
|
|
||||||
// Group DCS report settings into readable format
|
|
||||||
if (result.fields) {
|
|
||||||
const dcsRe = /^(.+Элемент(\\d+))(Использование|Значение|ВидСравнения)$/;
|
|
||||||
const dcsGroups = {};
|
|
||||||
const dcsNames = new Set();
|
|
||||||
for (const f of result.fields) {
|
|
||||||
const m = f.name.match(dcsRe);
|
|
||||||
if (!m) continue;
|
|
||||||
if (!dcsGroups[m[1]]) dcsGroups[m[1]] = { _n: parseInt(m[2]) };
|
|
||||||
dcsGroups[m[1]][m[3]] = f;
|
|
||||||
dcsNames.add(f.name);
|
|
||||||
}
|
|
||||||
const dcsEntries = Object.entries(dcsGroups).sort((a, b) => a[1]._n - b[1]._n);
|
|
||||||
if (dcsEntries.length) {
|
|
||||||
result.reportSettings = dcsEntries.map(([, g]) => {
|
|
||||||
const cb = g['Использование'];
|
|
||||||
const val = g['Значение'];
|
|
||||||
if (!cb && !val) return null;
|
|
||||||
// No checkbox present (class="staticText" instead of .checkbox) — setting is always enabled
|
|
||||||
const label = (val?.label || cb?.label || val?.name || cb?.name || '').replace(/:$/, '').trim();
|
|
||||||
const s = { name: label, enabled: cb ? !!cb.value : true };
|
|
||||||
if (val) {
|
|
||||||
s.value = val.value || '';
|
|
||||||
if (val.actions && val.actions.length) s.actions = val.actions;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}).filter(Boolean);
|
|
||||||
result.fields = result.fields.filter(f => !dcsNames.has(f.name));
|
|
||||||
if (!result.fields.length) delete result.fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}`;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// web-test dom/form-state v1.0 — combined detectForm + readForm + open tabs
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { DETECT_FORM_FN, DETECT_FORMS_FN, READ_FORM_FN } from './_shared.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combined: detect form + read form + read open tabs.
|
|
||||||
* Single evaluate call instead of 3. Used by browser.getFormState().
|
|
||||||
*/
|
|
||||||
export function getFormStateScript() {
|
|
||||||
return `(() => {
|
|
||||||
${DETECT_FORM_FN}
|
|
||||||
${DETECT_FORMS_FN}
|
|
||||||
${READ_FORM_FN}
|
|
||||||
const formNum = detectForm();
|
|
||||||
const meta = detectForms();
|
|
||||||
if (formNum === null) return { form: null, formCount: 0, message: 'No form detected' };
|
|
||||||
const p = 'form' + formNum + '_';
|
|
||||||
const formData = readForm(p);
|
|
||||||
// Open tabs bar (present only when tab panel is enabled in 1C settings)
|
|
||||||
const openTabs = [];
|
|
||||||
document.querySelectorAll('[id^="openedCell_cmd_"]').forEach(el => {
|
|
||||||
const text = el.innerText?.trim();
|
|
||||||
if (!text) return;
|
|
||||||
const entry = { name: text };
|
|
||||||
if (el.classList.contains('select')) entry.active = true;
|
|
||||||
openTabs.push(entry);
|
|
||||||
});
|
|
||||||
const activeTab = openTabs.find(t => t.active)?.name || null;
|
|
||||||
const result = { form: formNum, activeTab, openForms: meta.allForms, formCount: meta.formCount, ...formData };
|
|
||||||
if (meta.modal) result.modal = true;
|
|
||||||
if (openTabs.length) result.openTabs = openTabs;
|
|
||||||
return result;
|
|
||||||
})()`;
|
|
||||||
}
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
// web-test core/session v1.17 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import { chromium } from 'playwright';
|
|
||||||
import { statSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
||||||
import { join as pathJoin } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import {
|
|
||||||
browser, page, sessionPrefix, seanceId, recorder, highlightMode,
|
|
||||||
contexts, activeContextName, activeMode, persistentUserDataDir,
|
|
||||||
setBrowser, setPage, setSessionPrefix, setSeanceId, setHighlightMode,
|
|
||||||
setActiveContextName, setActiveMode, setPersistentUserDataDir,
|
|
||||||
isConnected, LOAD_TIMEOUT, INIT_TIMEOUT, EXT_ID,
|
|
||||||
} from './state.mjs';
|
|
||||||
import { closeModals } from './errors.mjs';
|
|
||||||
import { stopRecording } from '../recording/capture.mjs';
|
|
||||||
import { getPageState } from '../nav/navigation.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the 1C browser extension in Chrome/Edge user profiles.
|
|
||||||
* Returns the path to the latest version, or null if not found.
|
|
||||||
* Can be overridden via extensionPath in .v8-project.json.
|
|
||||||
*/
|
|
||||||
function findExtension(overridePath) {
|
|
||||||
if (overridePath) {
|
|
||||||
try { if (statSync(overridePath).isDirectory()) return overridePath; } catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const localAppData = process.env.LOCALAPPDATA;
|
|
||||||
if (!localAppData) return null;
|
|
||||||
const browsers = [
|
|
||||||
pathJoin(localAppData, 'Google', 'Chrome', 'User Data'),
|
|
||||||
pathJoin(localAppData, 'Microsoft', 'Edge', 'User Data'),
|
|
||||||
];
|
|
||||||
for (const userData of browsers) {
|
|
||||||
try { if (!statSync(userData).isDirectory()) continue; } catch { continue; }
|
|
||||||
let profiles;
|
|
||||||
try { profiles = readdirSync(userData).filter(d => d === 'Default' || d.startsWith('Profile ')); } catch { continue; }
|
|
||||||
for (const profile of profiles) {
|
|
||||||
const extDir = pathJoin(userData, profile, 'Extensions', EXT_ID);
|
|
||||||
try { if (!statSync(extDir).isDirectory()) continue; } catch { continue; }
|
|
||||||
let versions;
|
|
||||||
try { versions = readdirSync(extDir).filter(d => /^\d/.test(d)).sort(); } catch { continue; }
|
|
||||||
if (versions.length > 0) {
|
|
||||||
const best = pathJoin(extDir, versions[versions.length - 1]);
|
|
||||||
try { if (statSync(pathJoin(best, 'manifest.json')).isFile()) return best; } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* isConnected moved to core/state.mjs */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open browser and navigate to 1C web client URL.
|
|
||||||
* Waits for initialization (themesCell_theme_0 selector) and attempts to close startup modals.
|
|
||||||
*/
|
|
||||||
export async function connect(url, { extensionPath } = {}) {
|
|
||||||
if (isConnected()) {
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
} else {
|
|
||||||
const extPath = findExtension(extensionPath);
|
|
||||||
if (extPath) {
|
|
||||||
// Launch with 1C browser extension via persistent context
|
|
||||||
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now()));
|
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
|
||||||
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
|
|
||||||
headless: false,
|
|
||||||
args: [
|
|
||||||
'--start-maximized',
|
|
||||||
'--disable-extensions-except=' + extPath,
|
|
||||||
'--load-extension=' + extPath,
|
|
||||||
],
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
setBrowser(context); // persistent context IS the browser
|
|
||||||
setPage(context.pages()[0] || await context.newPage());
|
|
||||||
} else {
|
|
||||||
// Fallback: launch without extension
|
|
||||||
setBrowser(await chromium.launch({ headless: false, args: ['--start-maximized'] }));
|
|
||||||
const context = await browser.newContext({
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
setPage(await context.newPage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-accept native browser dialogs (confirm/alert from 1C scripts like vis.js)
|
|
||||||
page.on('dialog', dialog => dialog.accept().catch(() => {}));
|
|
||||||
|
|
||||||
// Capture seanceId from network requests for graceful logout
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
page.on('request', req => {
|
|
||||||
if (seanceId) return;
|
|
||||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
|
||||||
if (m) { setSessionPrefix(m[1]); setSeanceId(m[2]); }
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for 1C to initialize — detect by section panel appearance
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT });
|
|
||||||
} catch {
|
|
||||||
// Fallback: wait fixed time if selector doesn't appear (e.g. login page)
|
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to close startup modals (Путеводитель etc.)
|
|
||||||
await closeModals();
|
|
||||||
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Best-effort POST /e1cib/logout on a slot to release the 1C session license.
|
|
||||||
* Silent — if page is closed or session info missing, just returns.
|
|
||||||
* @param {object} slot { page, sessionPrefix, seanceId } from contexts Map
|
|
||||||
* @param {number} [waitMs=500] pause after logout fetch (gives 1C time to process)
|
|
||||||
*/
|
|
||||||
async function logoutSlot(slot, waitMs = 500) {
|
|
||||||
if (!slot?.page || slot.page.isClosed() || !slot.seanceId || !slot.sessionPrefix) return;
|
|
||||||
try {
|
|
||||||
const logoutUrl = `${slot.sessionPrefix}/e1cib/logout?seanceId=${slot.seanceId}`;
|
|
||||||
await slot.page.evaluate(async (url) => {
|
|
||||||
await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"root":{}}' });
|
|
||||||
}, logoutUrl);
|
|
||||||
await slot.page.waitForTimeout(waitMs);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gracefully terminate the 1C session and close the browser.
|
|
||||||
* Sends POST /e1cib/logout to release the license before closing.
|
|
||||||
*/
|
|
||||||
export async function disconnect() {
|
|
||||||
// Multi-context path: stop recording + logout each slot before closing browser
|
|
||||||
if (contexts.size > 0) {
|
|
||||||
saveActiveSlot();
|
|
||||||
// Recorder is global — one stop covers all contexts
|
|
||||||
if (recorder) {
|
|
||||||
try { await stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
for (const [, slot] of contexts.entries()) {
|
|
||||||
await logoutSlot(slot);
|
|
||||||
}
|
|
||||||
contexts.clear();
|
|
||||||
setActiveContextName(null);
|
|
||||||
setActiveMode(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single-session path (connect): auto-stop recording if active
|
|
||||||
if (recorder) {
|
|
||||||
try { await stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
// Graceful logout — release the 1C license (single-session connect path)
|
|
||||||
await logoutSlot({ page, sessionPrefix, seanceId }, 1000);
|
|
||||||
await browser.close().catch(() => {});
|
|
||||||
setBrowser(null);
|
|
||||||
setPage(null);
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
// Clean up persistent user data dir
|
|
||||||
if (persistentUserDataDir) {
|
|
||||||
try { rmSync(persistentUserDataDir, { recursive: true, force: true }); } catch {}
|
|
||||||
setPersistentUserDataDir(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach to a running browser server via CDP WebSocket.
|
|
||||||
* Sets module state so all functions (getFormState, clickElement, etc.) work.
|
|
||||||
*/
|
|
||||||
export async function attach(wsEndpoint, session = {}) {
|
|
||||||
if (isConnected()) return;
|
|
||||||
setBrowser(await chromium.connect(wsEndpoint));
|
|
||||||
const ctx = browser.contexts()[0];
|
|
||||||
setPage(ctx?.pages()[0]);
|
|
||||||
if (!page) throw new Error('No page found in browser');
|
|
||||||
setSessionPrefix(session.sessionPrefix || null);
|
|
||||||
setSeanceId(session.seanceId || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detach from browser without closing it.
|
|
||||||
* Returns session state for persistence.
|
|
||||||
*/
|
|
||||||
export function detach() {
|
|
||||||
const session = { sessionPrefix, seanceId };
|
|
||||||
setBrowser(null);
|
|
||||||
setPage(null);
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get current session state (for saving between reconnections). */
|
|
||||||
export function getSession() {
|
|
||||||
return { sessionPrefix, seanceId };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Multi-context support (used by run.mjs cmdTest only)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save current module-level state into the active slot before switching.
|
|
||||||
* No-op if no active slot.
|
|
||||||
*/
|
|
||||||
function saveActiveSlot() {
|
|
||||||
if (!activeContextName) return;
|
|
||||||
const slot = contexts.get(activeContextName);
|
|
||||||
if (!slot) return;
|
|
||||||
slot.page = page;
|
|
||||||
slot.sessionPrefix = sessionPrefix;
|
|
||||||
slot.seanceId = seanceId;
|
|
||||||
slot.highlightMode = highlightMode;
|
|
||||||
// Note: `recorder`, `lastCaptions`, `lastRecordingDuration` are intentionally NOT
|
|
||||||
// mirrored per-slot. A multi-context recording produces one continuous output file —
|
|
||||||
// the recorder follows the active page via recorder._attachPage(), not per-slot state.
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load a slot's state into module-level vars and mark it active. */
|
|
||||||
function activateSlot(name) {
|
|
||||||
const slot = contexts.get(name);
|
|
||||||
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
|
||||||
setPage(slot.page);
|
|
||||||
setSessionPrefix(slot.sessionPrefix);
|
|
||||||
setSeanceId(slot.seanceId);
|
|
||||||
setHighlightMode(slot.highlightMode || false);
|
|
||||||
setActiveContextName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Attach 1C session listeners to a page, writing into the given slot. */
|
|
||||||
function attachSessionListeners(pg, slot, name) {
|
|
||||||
pg.on('dialog', dialog => dialog.accept().catch(() => {}));
|
|
||||||
pg.on('request', req => {
|
|
||||||
if (slot.seanceId) return;
|
|
||||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
|
||||||
if (m) {
|
|
||||||
slot.sessionPrefix = m[1];
|
|
||||||
slot.seanceId = m[2];
|
|
||||||
if (activeContextName === name) {
|
|
||||||
setSessionPrefix(m[1]);
|
|
||||||
setSeanceId(m[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create (or navigate) a named browser context.
|
|
||||||
* First call launches Chromium via chromium.launch() (NOT launchPersistentContext) so that
|
|
||||||
* subsequent calls can create additional isolated BrowserContexts in the same process.
|
|
||||||
* Trade-off: 1C browser extension is loaded via --load-extension (process-level) rather than
|
|
||||||
* persistent profile.
|
|
||||||
*
|
|
||||||
* Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the
|
|
||||||
* legacy persistent-context path.
|
|
||||||
*/
|
|
||||||
export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) {
|
|
||||||
if (contexts.has(name)) {
|
|
||||||
await setActiveContext(name);
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
|
||||||
catch { await page.waitForTimeout(5000); }
|
|
||||||
await closeModals();
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['tab', 'window'].includes(isolation)) {
|
|
||||||
throw new Error(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`);
|
|
||||||
}
|
|
||||||
if (activeMode && activeMode !== isolation) {
|
|
||||||
throw new Error(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First context: launch browser. Subsequent: reuse existing.
|
|
||||||
let isFirstContext = !browser;
|
|
||||||
if (isFirstContext) {
|
|
||||||
const extPath = findExtension(extensionPath);
|
|
||||||
const launchArgs = ['--start-maximized'];
|
|
||||||
if (extPath) {
|
|
||||||
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
|
|
||||||
}
|
|
||||||
if (isolation === 'tab') {
|
|
||||||
// Persistent context: extension loads reliably, one window with tabs per context
|
|
||||||
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()));
|
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
|
||||||
setBrowser(await chromium.launchPersistentContext(persistentUserDataDir, {
|
|
||||||
headless: false,
|
|
||||||
args: launchArgs,
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// Window mode: separate BrowserContext per slot, full cookie isolation
|
|
||||||
setBrowser(await chromium.launch({ headless: false, args: launchArgs }));
|
|
||||||
}
|
|
||||||
setActiveMode(isolation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current active before switching
|
|
||||||
saveActiveSlot();
|
|
||||||
|
|
||||||
// Create slot — page differs by mode
|
|
||||||
let newCtx, newPage;
|
|
||||||
if (activeMode === 'tab') {
|
|
||||||
// Reuse the persistent context for all slots; each slot gets its own page (tab)
|
|
||||||
newCtx = browser;
|
|
||||||
if (isFirstContext) {
|
|
||||||
newPage = browser.pages()[0] || await browser.newPage();
|
|
||||||
} else {
|
|
||||||
newPage = await browser.newPage();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Window mode: each slot owns its BrowserContext + page
|
|
||||||
newCtx = await browser.newContext({
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
newPage = await newCtx.newPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
const slot = {
|
|
||||||
context: newCtx,
|
|
||||||
page: newPage,
|
|
||||||
sessionPrefix: null,
|
|
||||||
seanceId: null,
|
|
||||||
highlightMode: false,
|
|
||||||
};
|
|
||||||
contexts.set(name, slot);
|
|
||||||
|
|
||||||
attachSessionListeners(newPage, slot, name);
|
|
||||||
activateSlot(name);
|
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
|
||||||
catch { await page.waitForTimeout(5000); }
|
|
||||||
await closeModals();
|
|
||||||
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Switch the active context. Subsequent browser API calls operate on this context's page. */
|
|
||||||
export async function setActiveContext(name) {
|
|
||||||
if (activeContextName === name) return;
|
|
||||||
if (!contexts.has(name)) throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
|
||||||
// If a recording is active, flush the outgoing page's last frame so the gap is filled
|
|
||||||
// up to the moment of the switch (avoids a "jump" in video time).
|
|
||||||
if (recorder && recorder._flushFrames) recorder._flushFrames();
|
|
||||||
saveActiveSlot();
|
|
||||||
activateSlot(name);
|
|
||||||
// If the recording is still alive (it lives across slots — we keep the same ffmpeg/output),
|
|
||||||
// re-attach its screencast to the newly active page.
|
|
||||||
if (recorder && recorder._attachPage) {
|
|
||||||
await recorder._attachPage(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listContexts() {
|
|
||||||
return [...contexts.keys()];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getActiveContext() {
|
|
||||||
return activeContextName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasContext(name) {
|
|
||||||
return contexts.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close a named context: logout, close its page (tab mode) or BrowserContext
|
|
||||||
* (window mode), remove from registry. Cannot close the currently active
|
|
||||||
* context — caller must setActiveContext to another first. This keeps the
|
|
||||||
* recorder/page invariants simple: recorder is always attached to the
|
|
||||||
* active slot, which closeContext never touches.
|
|
||||||
*
|
|
||||||
* @throws if name is not registered or equals the active context.
|
|
||||||
*/
|
|
||||||
export async function closeContext(name) {
|
|
||||||
if (!contexts.has(name)) {
|
|
||||||
throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
|
||||||
}
|
|
||||||
if (name === activeContextName) {
|
|
||||||
throw new Error(`closeContext: cannot close the active context "${name}". setActiveContext to another context first.`);
|
|
||||||
}
|
|
||||||
const slot = contexts.get(name);
|
|
||||||
await logoutSlot(slot);
|
|
||||||
if (activeMode === 'tab') {
|
|
||||||
try { await slot.page.close(); } catch {}
|
|
||||||
} else {
|
|
||||||
try { await slot.context.close(); } catch {}
|
|
||||||
}
|
|
||||||
contexts.delete(name);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// web-test forms/close v1.18 — Close current form via Escape, handle save-changes confirmation.
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import { page, recorder, ensureConnected } from '../core/state.mjs';
|
|
||||||
import { detectFormScript } from '../../dom.mjs';
|
|
||||||
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
|
||||||
import { waitForStable } from '../core/wait.mjs';
|
|
||||||
import { returnFormState } from '../core/helpers.mjs';
|
|
||||||
import { getFormState } from './state.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the current form/dialog via Escape.
|
|
||||||
* @param {Object} [opts]
|
|
||||||
* @param {boolean} [opts.save] - Handle "Save changes?" confirmation automatically:
|
|
||||||
* true → click "Да" (save and close)
|
|
||||||
* false → click "Нет" (discard and close)
|
|
||||||
* undefined → return confirmation as hint for caller to decide
|
|
||||||
*/
|
|
||||||
export async function closeForm({ save } = {}) {
|
|
||||||
ensureConnected();
|
|
||||||
await dismissPendingErrors();
|
|
||||||
// If platform dialogs are open, close them instead of pressing Escape
|
|
||||||
const pd = await detectPlatformDialogs();
|
|
||||||
if (pd.length) {
|
|
||||||
await closePlatformDialogs();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
return returnFormState({ closed: true, closedPlatformDialogs: pd });
|
|
||||||
}
|
|
||||||
const beforeForm = await page.evaluate(detectFormScript());
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable(beforeForm);
|
|
||||||
const state = await getFormState();
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err?.confirmation) {
|
|
||||||
if (save === true || save === false) {
|
|
||||||
const label = save ? 'Да' : 'Нет';
|
|
||||||
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
|
|
||||||
const btns = await page.$$(btnSel);
|
|
||||||
for (const b of btns) {
|
|
||||||
const txt = (await b.textContent()).trim();
|
|
||||||
if (txt === label) {
|
|
||||||
if (recorder) await page.waitForTimeout(500); // show confirmation to viewer during recording
|
|
||||||
await b.click({ force: true });
|
|
||||||
await waitForStable(beforeForm);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const afterForm = await page.evaluate(detectFormScript());
|
|
||||||
return returnFormState({ closed: afterForm !== beforeForm });
|
|
||||||
}
|
|
||||||
state.confirmation = err.confirmation;
|
|
||||||
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return returnFormState({ closed: state.form !== beforeForm });
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
|
|
||||||
|
|
||||||
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
|
|
||||||
|
|
||||||
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
|
|
||||||
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
|
|
||||||
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
|
|
||||||
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- **Windows** с PowerShell 5.1+ (входит в Windows) — для PowerShell-сборки.
|
|
||||||
- **Python 3.10+** — для Python-сборки. Зависимости: `lxml>=4.9.0`, `psutil>=5.9.0` (для DOM- и web-навыков).
|
|
||||||
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
|
|
||||||
- **Node.js 18+** — для `/web-test`.
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Source: {{MAIN_REPO_URL}}
|
|
||||||
Build commit: `{{COMMIT_SHA}}`
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"version": "{{VERSION}}",
|
|
||||||
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.codex/skills/",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
|
|
||||||
"shortDescription": "{{SHORT_DESCRIPTION}}",
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
name: Build port branches
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- '.claude/skills/**'
|
|
||||||
- 'scripts/switch.py'
|
|
||||||
- '.github/templates/README.port.md.tmpl'
|
|
||||||
- '.github/templates/codex-plugin.json.tmpl'
|
|
||||||
- '.github/templates/claude-plugin.json.tmpl'
|
|
||||||
- '.github/workflows/build-ports.yml'
|
|
||||||
- 'LICENSE'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: claude-code
|
|
||||||
runtime: python
|
|
||||||
branch: port-claude-code-py
|
|
||||||
label: Claude Code
|
|
||||||
target_dir: .claude/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cursor
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: python
|
|
||||||
branch: port-cursor-py
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codex
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: python
|
|
||||||
branch: port-codex-py
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-copilot
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: python
|
|
||||||
branch: port-copilot-py
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-augment
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: python
|
|
||||||
branch: port-augment-py
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cline
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: python
|
|
||||||
branch: port-cline-py
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kilo
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: python
|
|
||||||
branch: port-kilo-py
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kiro
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: python
|
|
||||||
branch: port-kiro-py
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-gemini
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: python
|
|
||||||
branch: port-gemini-py
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-opencode
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: python
|
|
||||||
branch: port-opencode-py
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-roo
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: python
|
|
||||||
branch: port-roo-py
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-windsurf
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: python
|
|
||||||
branch: port-windsurf-py
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codeassistant
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: python
|
|
||||||
branch: port-codeassistant-py
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-agents
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: python
|
|
||||||
branch: port-agents-py
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout main
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
|
|
||||||
run: |
|
|
||||||
python scripts/switch.py "${{ matrix.platform }}" \
|
|
||||||
--project-dir build \
|
|
||||||
--runtime "${{ matrix.runtime }}"
|
|
||||||
|
|
||||||
- name: Render port README
|
|
||||||
env:
|
|
||||||
PLATFORM_LABEL: ${{ matrix.label }}
|
|
||||||
PLATFORM_DIR: ${{ matrix.target_dir }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
MAIN_REPO_URL: https://github.com/${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
|
|
||||||
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
|
|
||||||
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
|
|
||||||
.github/templates/README.port.md.tmpl > build/README.md
|
|
||||||
|
|
||||||
- name: Render Codex plugin manifest
|
|
||||||
if: matrix.platform == 'codex'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
|
|
||||||
mkdir -p build/.codex-plugin
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
-e "s|{{VERSION}}|${VERSION}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
|
|
||||||
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Render Claude plugin manifest (Py variant)
|
|
||||||
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: 1c-skills-py
|
|
||||||
run: |
|
|
||||||
mkdir -p build/.claude-plugin
|
|
||||||
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Copy LICENSE
|
|
||||||
run: cp LICENSE build/LICENSE
|
|
||||||
|
|
||||||
- name: Force-push orphan snapshot to ${{ matrix.branch }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cd build
|
|
||||||
git init -q -b master
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add -A
|
|
||||||
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
|
|
||||||
git push --force \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
|
|
||||||
"master:${{ matrix.branch }}"
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
# Реальные выгрузки обработок (примеры, не для версионирования)
|
|
||||||
upload/
|
|
||||||
|
|
||||||
# Результаты сборки
|
|
||||||
build/
|
|
||||||
base/
|
|
||||||
*.epf
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Временные файлы тестов
|
|
||||||
test-tmp/
|
|
||||||
|
|
||||||
# Локальные настройки Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# Инструменты (portable Apache и т.д.)
|
|
||||||
tools/
|
|
||||||
|
|
||||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
|
||||||
debug/
|
|
||||||
|
|
||||||
# Кэш тестов навыков
|
|
||||||
tests/skills/.cache/
|
|
||||||
|
|
||||||
# Python кэш
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Локальный реестр баз данных 1С
|
|
||||||
.v8-project.json
|
|
||||||
|
|
||||||
# web-test: Node.js зависимости и runtime-артефакты
|
|
||||||
.claude/skills/web-test/scripts/node_modules/
|
|
||||||
.claude/skills/web-test/.browser-session.json
|
|
||||||
|
|
||||||
# Скриншоты и видео (артефакты тестирования web-test)
|
|
||||||
*.png
|
|
||||||
*.mp4
|
|
||||||
|
|
||||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
|
||||||
.agents/skills/
|
|
||||||
.augment/
|
|
||||||
.cline/
|
|
||||||
.codex/
|
|
||||||
.cursor/
|
|
||||||
.gemini/
|
|
||||||
.github/skills/
|
|
||||||
.kilocode/
|
|
||||||
.kiro/
|
|
||||||
.opencode/
|
|
||||||
.roo/
|
|
||||||
.windsurf/
|
|
||||||
debug-templates.txt
|
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
python ".opencode/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
+159
-6
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -29,11 +29,151 @@ if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; e
|
|||||||
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
||||||
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||||
|
|
||||||
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
|
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||||
|
# throws — guard errors degrade to allow.
|
||||||
|
function Get-RootUuid([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $null }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Get-EditMode([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||||
|
if (-not $pj) { return 'deny' }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||||
|
return 'deny'
|
||||||
|
} catch { return 'deny' }
|
||||||
|
}
|
||||||
|
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||||
|
try {
|
||||||
|
$rp = $targetPath
|
||||||
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
$cfgDir = $null; $binPath = $null
|
||||||
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
|
if (-not $cfgDir) {
|
||||||
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||||
|
}
|
||||||
|
if ($elemUuid -and $cfgDir) { break }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
# New object (no element file): fall back to config root uuid.
|
||||||
|
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||||
|
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return }
|
||||||
|
$start = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||||
|
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $hm.Success) { return }
|
||||||
|
$G = [int]$hm.Groups[1].Value
|
||||||
|
$K = [int]$hm.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return }
|
||||||
|
$best = $null
|
||||||
|
if ($elemUuid) {
|
||||||
|
$u = [regex]::Escape($elemUuid.ToLower())
|
||||||
|
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||||
|
$f1 = [int]$m.Groups[1].Value
|
||||||
|
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$blocked = $false; $code = ""; $reason = ""
|
||||||
|
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||||
|
elseif ($require -eq 'removed') {
|
||||||
|
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||||
|
}
|
||||||
|
if (-not $blocked) { return }
|
||||||
|
$mode = Get-EditMode $cfgDir
|
||||||
|
if ($mode -eq 'off') { return }
|
||||||
|
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||||
|
# latter throws and would be swallowed by this function's own catch.
|
||||||
|
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||||
|
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if ($code -eq "capability-off") {
|
||||||
|
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||||
|
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
} elseif ($code -eq "not-removed") {
|
||||||
|
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||||
|
} else {
|
||||||
|
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||||
|
}
|
||||||
|
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||||
|
exit 1
|
||||||
|
} catch { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-EditAllowed $resolvedPath 'editable'
|
||||||
|
|
||||||
# --- Load XML with PreserveWhitespace ---
|
# --- Load XML with PreserveWhitespace ---
|
||||||
|
# NB: парсер XML по спецификации схлопывает CRLF в LF, а вставки ниже собираются с
|
||||||
|
# явным CRLF — поэтому EOL приводится к целевому в точке записи (см. финализацию).
|
||||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$script:xmlDoc.PreserveWhitespace = $true
|
$script:xmlDoc.PreserveWhitespace = $true
|
||||||
$script:xmlDoc.Load($resolvedPath)
|
$script:xmlDoc.Load($resolvedPath)
|
||||||
|
|
||||||
|
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||||
|
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||||
|
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
|
||||||
|
|
||||||
$script:addCount = 0
|
$script:addCount = 0
|
||||||
$script:removeCount = 0
|
$script:removeCount = 0
|
||||||
$script:modifyCount = 0
|
$script:modifyCount = 0
|
||||||
@@ -76,7 +216,7 @@ Info "Configuration: $($script:objName)"
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -92,7 +232,7 @@ $script:typeOrder = @(
|
|||||||
$script:typeToDir = @{
|
$script:typeToDir = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||||
@@ -553,7 +693,9 @@ $bodyBlock$declarations
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$caiXml = ($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($caiPath, $caiXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote panel layout: $caiPath"
|
Info "Wrote panel layout: $caiPath"
|
||||||
}
|
}
|
||||||
@@ -580,6 +722,7 @@ $script:ruTypeMap = @{
|
|||||||
"регистррасчёта" = "CalculationRegister"
|
"регистррасчёта" = "CalculationRegister"
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
"задача" = "Task"
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
@@ -730,7 +873,7 @@ function Do-SetHomePage($valArg) {
|
|||||||
|
|
||||||
$hpXml = @"
|
$hpXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
||||||
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
||||||
$leftXml
|
$leftXml
|
||||||
$rightXml
|
$rightXml
|
||||||
@@ -741,7 +884,9 @@ $rightXml
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$hpXml = ($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($hpPath, $hpXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote home page layout: $hpPath"
|
Info "Wrote home page layout: $hpPath"
|
||||||
}
|
}
|
||||||
@@ -843,6 +988,14 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
+304
-21
@@ -1,16 +1,253 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
|
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||||
|
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _sg_root_uuid(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str) and child.get("uuid"):
|
||||||
|
return child.get("uuid")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_get_edit_mode(cfg_dir):
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||||
|
if not pj:
|
||||||
|
return "deny"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src:
|
||||||
|
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
if db.get("editingAllowedCheck"):
|
||||||
|
return db["editingAllowedCheck"]
|
||||||
|
if proj.get("editingAllowedCheck"):
|
||||||
|
return proj["editingAllowedCheck"]
|
||||||
|
return "deny"
|
||||||
|
except Exception:
|
||||||
|
return "deny"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_edit_allowed(target_path, require):
|
||||||
|
try:
|
||||||
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
|
cfg_dir = None
|
||||||
|
bin_path = None
|
||||||
|
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||||
|
for _ in range(12):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
|
if not elem_uuid:
|
||||||
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
|
if not cfg_dir:
|
||||||
|
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||||
|
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||||
|
cfg_dir = d
|
||||||
|
bin_path = cand
|
||||||
|
if elem_uuid and cfg_dir:
|
||||||
|
break
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
if not elem_uuid and cfg_dir:
|
||||||
|
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||||
|
if not bin_path or not os.path.exists(bin_path):
|
||||||
|
return
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return
|
||||||
|
best = None
|
||||||
|
if elem_uuid:
|
||||||
|
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||||
|
f1 = int(m.group(1))
|
||||||
|
if best is None or f1 < best:
|
||||||
|
best = f1
|
||||||
|
blocked = False
|
||||||
|
code = ""
|
||||||
|
reason = ""
|
||||||
|
if g == 1:
|
||||||
|
blocked = True
|
||||||
|
code = "capability-off"
|
||||||
|
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||||
|
elif require == "removed":
|
||||||
|
if best is not None and best != 2:
|
||||||
|
blocked = True
|
||||||
|
code = "not-removed"
|
||||||
|
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||||
|
else:
|
||||||
|
if best is not None and best == 0:
|
||||||
|
blocked = True
|
||||||
|
code = "locked"
|
||||||
|
reason = "объект на замке — редактирование сломает обновления"
|
||||||
|
if not blocked:
|
||||||
|
return
|
||||||
|
mode = _sg_get_edit_mode(cfg_dir)
|
||||||
|
if mode == "off":
|
||||||
|
return
|
||||||
|
if mode == "warn":
|
||||||
|
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||||
|
return
|
||||||
|
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if code == "capability-off":
|
||||||
|
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||||
|
fix = (
|
||||||
|
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||||
|
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||||
|
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||||
|
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
)
|
||||||
|
elif code == "not-removed":
|
||||||
|
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
fix = (
|
||||||
|
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
fix = (
|
||||||
|
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||||
|
)
|
||||||
|
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
@@ -21,7 +258,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -37,7 +274,7 @@ TYPE_ORDER = [
|
|||||||
TYPE_TO_DIR = {
|
TYPE_TO_DIR = {
|
||||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||||
@@ -146,12 +383,49 @@ def parse_batch_value(val):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
if not xml_bytes.endswith(b"\n"):
|
try:
|
||||||
|
raw = open(path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
body = raw[3:] if bom else raw
|
||||||
|
crlf = b"\r\n" in body
|
||||||
|
m = re.search(rb'encoding="([^"]+)"', body[:200])
|
||||||
|
enc = m.group(1).decode("ascii") if m else "utf-8"
|
||||||
|
final_nl = body.endswith(b"\n")
|
||||||
|
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
|
||||||
|
|
||||||
|
|
||||||
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
|
xml_bytes = xml_bytes.replace(
|
||||||
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
|
want_final_nl = style["final_nl"] if style else False
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
|
if (style["crlf"] if style else True):
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_bom(tree, path):
|
||||||
|
style = _detect_xml_style(path)
|
||||||
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
|
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
|
if style is None or style["bom"]:
|
||||||
f.write(b"\xef\xbb\xbf")
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
@@ -165,7 +439,7 @@ def main():
|
|||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||||
@@ -190,10 +464,16 @@ def main():
|
|||||||
resolved_path = os.path.abspath(config_path)
|
resolved_path = os.path.abspath(config_path)
|
||||||
config_dir = os.path.dirname(resolved_path)
|
config_dir = os.path.dirname(resolved_path)
|
||||||
|
|
||||||
|
assert_edit_allowed(resolved_path, "editable")
|
||||||
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
tree = etree.parse(resolved_path, xml_parser)
|
||||||
xml_root = tree.getroot()
|
xml_root = tree.getroot()
|
||||||
|
|
||||||
|
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||||
|
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||||
|
format_version = xml_root.get('version') or '2.17'
|
||||||
|
|
||||||
add_count = 0
|
add_count = 0
|
||||||
remove_count = 0
|
remove_count = 0
|
||||||
modify_count = 0
|
modify_count = 0
|
||||||
@@ -542,7 +822,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -608,6 +888,7 @@ def main():
|
|||||||
"регистррасчета": "CalculationRegister",
|
"регистррасчета": "CalculationRegister",
|
||||||
"регистррасчёта": "CalculationRegister",
|
"регистррасчёта": "CalculationRegister",
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
"хранилищенастроек": "SettingsStorage",
|
"хранилищенастроек": "SettingsStorage",
|
||||||
}
|
}
|
||||||
@@ -696,7 +977,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -742,7 +1023,7 @@ def main():
|
|||||||
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
||||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
|
f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">\r\n'
|
||||||
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
||||||
f'{left_xml}\r\n'
|
f'{left_xml}\r\n'
|
||||||
f'{right_xml}\r\n'
|
f'{right_xml}\r\n'
|
||||||
@@ -764,7 +1045,7 @@ def main():
|
|||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
ops = json.loads(fh.read())
|
ops = ci_json(json.loads(fh.read()))
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -774,23 +1055,25 @@ def main():
|
|||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
op_name = op.get("operation", args.Operation or "")
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
# PS сравнивает имя операции через switch, а он регистронезависим.
|
||||||
|
op_key = str(op_name).lower()
|
||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "modify-property":
|
if op_key == "modify-property":
|
||||||
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-childObject":
|
elif op_key == "add-childobject":
|
||||||
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-childObject":
|
elif op_key == "remove-childobject":
|
||||||
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-defaultRole":
|
elif op_key == "add-defaultrole":
|
||||||
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-defaultRole":
|
elif op_key == "remove-defaultrole":
|
||||||
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-defaultRoles":
|
elif op_key == "set-defaultroles":
|
||||||
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-panels":
|
elif op_key == "set-panels":
|
||||||
do_set_panels(op_value)
|
do_set_panels(op_value)
|
||||||
elif op_name == "set-home-page":
|
elif op_key == "set-home-page":
|
||||||
do_set_home_page(op_value)
|
do_set_home_page(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
@@ -806,7 +1089,7 @@ def main():
|
|||||||
if os.path.isfile(validate_script):
|
if os.path.isfile(validate_script):
|
||||||
print()
|
print()
|
||||||
print("--- Running cf-validate ---")
|
print("--- Running cf-validate ---")
|
||||||
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
|
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
|
||||||
|
|
||||||
# --- Summary ---
|
# --- Summary ---
|
||||||
print()
|
print()
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
python ".opencode/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
+78
-2
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.2 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
|
|||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -105,6 +105,7 @@ $typeRuNames = @{
|
|||||||
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
||||||
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
||||||
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
||||||
|
"Bot"="Боты"
|
||||||
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
||||||
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
||||||
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
||||||
@@ -218,6 +219,78 @@ function Get-HomePageLayout {
|
|||||||
|
|
||||||
$script:homePage = Get-HomePageLayout
|
$script:homePage = Get-HomePageLayout
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
|
||||||
|
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
|
||||||
|
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
|
||||||
|
# vendor block separately); only computed when G=0.
|
||||||
|
function Read-SupportState([string]$binPath) {
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
|
||||||
|
$startIdx = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
|
||||||
|
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $h.Success) { return $null }
|
||||||
|
$G = [int]$h.Groups[1].Value
|
||||||
|
$K = [int]$h.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return @{ State = 'removed' } }
|
||||||
|
# Vendor descriptors: ...,"ver","vendor","name",count,
|
||||||
|
$vendors = @()
|
||||||
|
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
|
||||||
|
foreach ($m in $vRe.Matches($text)) {
|
||||||
|
$vendors += @{
|
||||||
|
Version = ($m.Groups[1].Value -replace '""','"')
|
||||||
|
Vendor = ($m.Groups[2].Value -replace '""','"')
|
||||||
|
Name = ($m.Groups[3].Value -replace '""','"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
|
||||||
|
# whole config is read-only and stored f1 values are the inactive default.
|
||||||
|
$counts = $null
|
||||||
|
if ($G -eq 0) {
|
||||||
|
$counts = @(0, 0, 0)
|
||||||
|
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
|
||||||
|
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||||
|
foreach ($m in $rRe.Matches($text)) {
|
||||||
|
$counts[[int]$m.Groups[1].Value]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
|
||||||
|
} catch { return $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SupportLines {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
|
||||||
|
$st = Read-SupportState $binPath
|
||||||
|
$out = @()
|
||||||
|
if (-not $st -or $st.State -eq 'absent') {
|
||||||
|
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
|
||||||
|
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
if ($st.State -eq 'removed') {
|
||||||
|
$out += "Поддержка: снята с поддержки полностью"
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
$out += "Поддержка: на поддержке"
|
||||||
|
if ($st.G -eq 0) {
|
||||||
|
$out += " Возможность изменения: включена"
|
||||||
|
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
|
||||||
|
} else {
|
||||||
|
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
|
||||||
|
}
|
||||||
|
$out += " Конфигураций поставщика: $($st.K)"
|
||||||
|
if ($st.K -gt 1) {
|
||||||
|
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" }
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
|
||||||
function Format-HomePageItem($it, [bool]$detailed) {
|
function Format-HomePageItem($it, [bool]$detailed) {
|
||||||
$badges = @()
|
$badges = @()
|
||||||
$badges += "h=$($it.height)"
|
$badges += "h=$($it.height)"
|
||||||
@@ -253,6 +326,7 @@ $cfgVersion = Get-PropText "Version"
|
|||||||
$cfgVendor = Get-PropText "Vendor"
|
$cfgVendor = Get-PropText "Vendor"
|
||||||
$cfgCompat = Get-PropText "CompatibilityMode"
|
$cfgCompat = Get-PropText "CompatibilityMode"
|
||||||
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
||||||
|
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
|
||||||
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
||||||
$cfgScript = Get-PropText "ScriptVariant"
|
$cfgScript = Get-PropText "ScriptVariant"
|
||||||
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
||||||
@@ -284,6 +358,7 @@ if ($Mode -eq "overview" -and -not $Section) {
|
|||||||
Out "Формат: $version"
|
Out "Формат: $version"
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
Out "Совместимость: $cfgCompat"
|
Out "Совместимость: $cfgCompat"
|
||||||
Out "Режим запуска: $cfgDefaultRun"
|
Out "Режим запуска: $cfgDefaultRun"
|
||||||
Out "Язык скриптов: $cfgScript"
|
Out "Язык скриптов: $cfgScript"
|
||||||
@@ -386,6 +461,7 @@ if ($Mode -eq "full" -and -not $Section) {
|
|||||||
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
||||||
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
||||||
Out ""
|
Out ""
|
||||||
+97
-3
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.2 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -11,6 +12,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||||
@@ -19,7 +42,7 @@ parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, he
|
|||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Output helper (collect all, paginate at the end) ---
|
# --- Output helper (collect all, paginate at the end) ---
|
||||||
lines_buf = []
|
lines_buf = []
|
||||||
@@ -94,7 +117,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -110,6 +133,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
@@ -219,6 +243,71 @@ def get_home_page_layout():
|
|||||||
|
|
||||||
home_page = get_home_page_layout()
|
home_page = get_home_page_layout()
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
||||||
|
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
||||||
|
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
||||||
|
# vendor block separately); only computed when g==0.
|
||||||
|
def read_support_state(bin_path):
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(bin_path):
|
||||||
|
return {"state": "absent"}
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return {"state": "removed"}
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return None
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return {"state": "removed"}
|
||||||
|
vendors = []
|
||||||
|
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
||||||
|
vendors.append({
|
||||||
|
"version": m.group(1).replace('""', '"'),
|
||||||
|
"vendor": m.group(2).replace('""', '"'),
|
||||||
|
"name": m.group(3).replace('""', '"'),
|
||||||
|
})
|
||||||
|
counts = None
|
||||||
|
if g == 0:
|
||||||
|
counts = [0, 0, 0]
|
||||||
|
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
||||||
|
counts[int(m.group(1))] += 1
|
||||||
|
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_support_lines():
|
||||||
|
config_dir = os.path.dirname(config_path)
|
||||||
|
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
||||||
|
st = read_support_state(bin_path)
|
||||||
|
res = []
|
||||||
|
if not st or st["state"] == "absent":
|
||||||
|
if cfg_ext_purpose:
|
||||||
|
res.append("Поддержка: расширение (CFE), правки свободны")
|
||||||
|
else:
|
||||||
|
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
||||||
|
return res
|
||||||
|
if st["state"] == "removed":
|
||||||
|
res.append("Поддержка: снята с поддержки полностью")
|
||||||
|
return res
|
||||||
|
res.append("Поддержка: на поддержке")
|
||||||
|
if st["g"] == 0:
|
||||||
|
res.append(" Возможность изменения: включена")
|
||||||
|
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
||||||
|
else:
|
||||||
|
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
||||||
|
res.append(f" Конфигураций поставщика: {st['k']}")
|
||||||
|
if st["k"] > 1:
|
||||||
|
for v in st["vendors"]:
|
||||||
|
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
||||||
|
return res
|
||||||
|
|
||||||
def format_home_page_item(it, detailed):
|
def format_home_page_item(it, detailed):
|
||||||
badges = [f"h={it['height']}"]
|
badges = [f"h={it['height']}"]
|
||||||
if not it["common"]:
|
if not it["common"]:
|
||||||
@@ -249,6 +338,7 @@ cfg_version = get_prop_text("Version")
|
|||||||
cfg_vendor = get_prop_text("Vendor")
|
cfg_vendor = get_prop_text("Vendor")
|
||||||
cfg_compat = get_prop_text("CompatibilityMode")
|
cfg_compat = get_prop_text("CompatibilityMode")
|
||||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||||
|
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||||
cfg_script = get_prop_text("ScriptVariant")
|
cfg_script = get_prop_text("ScriptVariant")
|
||||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||||
@@ -281,6 +371,8 @@ if args.Mode == "overview" and not args.Section:
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
out(f"Совместимость: {cfg_compat}")
|
out(f"Совместимость: {cfg_compat}")
|
||||||
out(f"Режим запуска: {cfg_default_run}")
|
out(f"Режим запуска: {cfg_default_run}")
|
||||||
out(f"Язык скриптов: {cfg_script}")
|
out(f"Язык скриптов: {cfg_script}")
|
||||||
@@ -369,6 +461,8 @@ if args.Mode == "full" and not args.Section:
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||||
if cfg_update_addr:
|
if cfg_update_addr:
|
||||||
out(f"Каталог обн.: {cfg_update_addr}")
|
out(f"Каталог обн.: {cfg_update_addr}")
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: cf-init
|
||||||
|
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
||||||
|
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /cf-init — Создание пустой конфигурации 1С
|
||||||
|
|
||||||
|
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
||||||
|
|
||||||
|
## Параметры и команда
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `Name` | Имя конфигурации (обязат.) |
|
||||||
|
| `Synonym` | Синоним (= Name если не указан) |
|
||||||
|
| `OutputDir` | Каталог для создания (default: `src`) |
|
||||||
|
| `Version` | Версия конфигурации |
|
||||||
|
| `Vendor` | Поставщик |
|
||||||
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
| `FormatVersion` | Версия формата выгрузки (default: `2.17`) |
|
||||||
|
|
||||||
|
Оба параметра задаются от **целевой платформы** — той, на которой конфигурация будет работать, — но по
|
||||||
|
разным правилам.
|
||||||
|
|
||||||
|
`FormatVersion` — **не выше** версии формата целевой платформы: 8.3.24 — `2.17`, 8.3.25 — `2.18`,
|
||||||
|
8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно: платформа читает свой формат и любой
|
||||||
|
более старый, поэтому дефолт `2.17` подходит для всей линейки 8.3.24 и выше.
|
||||||
|
|
||||||
|
`CompatibilityMode` влияет на доступную функциональность. Если в задаче не оговорено иное — **равен
|
||||||
|
целевой платформе** (`Version8_3_27` для 8.3.27); это то же самое, что «Не использовать» в
|
||||||
|
Конфигураторе, и именно такое значение платформа проставляет новой базе. Более низкий режим берут
|
||||||
|
осознанно — когда конфигурация должна работать и на старых платформах; он отключает возможности,
|
||||||
|
появившиеся позже. Выше целевой платформы — нельзя: такая конфигурация загрузится, но работать на ней
|
||||||
|
не будет.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Базовая конфигурация
|
||||||
|
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||||
|
|
||||||
|
# С версией и поставщиком
|
||||||
|
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||||
|
|
||||||
|
# Под платформу 8.3.27 — версия формата и режим совместимости вместе
|
||||||
|
... -Name TestCfg -FormatVersion 2.20 -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
```
|
||||||
|
/cf-init TestConfig -OutputDir test-tmp/cf
|
||||||
|
/cf-info test-tmp/cf — проверить созданное
|
||||||
|
/cf-validate test-tmp/cf — валидировать
|
||||||
|
```
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Synonym = $Name,
|
||||||
|
[string]$OutputDir = "src",
|
||||||
|
[string]$Version,
|
||||||
|
[string]$Vendor,
|
||||||
|
[string]$CompatibilityMode = "Version8_3_24",
|
||||||
|
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||||
|
# совместимости она не зависит. Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
|
||||||
|
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
|
||||||
|
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
|
||||||
|
# на нечисловое значение: это опечатка, а не версия.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$formatRank = Get-FormatRank $FormatVersion
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
|
||||||
|
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
|
||||||
|
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
|
||||||
|
if ($formatRank -eq 0) {
|
||||||
|
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if ($CompatibilityMode -and $CompatibilityMode.ToLowerInvariant() -eq 'dontuse') {
|
||||||
|
[Console]::Error.WriteLine("WARNING: CompatibilityMode 'DontUse' is not `"no restrictions`" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).")
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Resolve output dir ---
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||||
|
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check existing ---
|
||||||
|
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||||
|
if (Test-Path $cfgFile) {
|
||||||
|
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Generate UUIDs ---
|
||||||
|
$uuidCfg = [guid]::NewGuid().ToString()
|
||||||
|
$uuidLang = [guid]::NewGuid().ToString()
|
||||||
|
# 7 ContainedObject ObjectIds
|
||||||
|
$co1 = [guid]::NewGuid().ToString()
|
||||||
|
$co2 = [guid]::NewGuid().ToString()
|
||||||
|
$co3 = [guid]::NewGuid().ToString()
|
||||||
|
$co4 = [guid]::NewGuid().ToString()
|
||||||
|
$co5 = [guid]::NewGuid().ToString()
|
||||||
|
$co6 = [guid]::NewGuid().ToString()
|
||||||
|
$co7 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
$is221 = ($formatRank -ge 221)
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
$is218 = ($formatRank -ge 218)
|
||||||
|
|
||||||
|
$mobileFuncs = @(
|
||||||
|
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||||
|
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||||
|
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
||||||
|
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
||||||
|
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
||||||
|
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
||||||
|
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
||||||
|
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
||||||
|
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
||||||
|
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
||||||
|
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
||||||
|
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
||||||
|
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||||
|
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||||
|
)
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if ($is218) { $mobileFuncs += ,@("TextToSpeech","false") }
|
||||||
|
|
||||||
|
$mobileXml = ""
|
||||||
|
foreach ($mf in $mobileFuncs) {
|
||||||
|
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Synonym XML ---
|
||||||
|
$synonymXml = ""
|
||||||
|
if ($Synonym) {
|
||||||
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Optional properties ---
|
||||||
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
|
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
|
||||||
|
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
|
||||||
|
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
|
||||||
|
# на своё место, а не в конец.
|
||||||
|
$nl = "`r`n"
|
||||||
|
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
|
||||||
|
$palNs = ""
|
||||||
|
if ($is221) {
|
||||||
|
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
|
||||||
|
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
|
||||||
|
$f221AuxForms = $nl + ((@(
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
|
||||||
|
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
|
||||||
|
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
|
||||||
|
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
|
||||||
|
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
|
||||||
|
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
|
||||||
|
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Configuration.xml ---
|
||||||
|
$cfgXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
|
<Configuration uuid="$uuidCfg">
|
||||||
|
<InternalInfo>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co1</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co2</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co3</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co4</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co5</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co6</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
<xr:ContainedObject>
|
||||||
|
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||||
|
<xr:ObjectId>$co7</xr:ObjectId>
|
||||||
|
</xr:ContainedObject>
|
||||||
|
</InternalInfo>
|
||||||
|
<Properties>
|
||||||
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
|
<Synonym>$synonymXml</Synonym>
|
||||||
|
<Comment/>
|
||||||
|
<NamePrefix/>
|
||||||
|
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||||
|
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
|
<UsePurposes>
|
||||||
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
|
</UsePurposes>
|
||||||
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
|
<DefaultRoles/>
|
||||||
|
$vendorEl
|
||||||
|
$versionEl
|
||||||
|
<UpdateCatalogAddress/>
|
||||||
|
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
|
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
|
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||||
|
<AdditionalFullTextSearchDictionaries/>
|
||||||
|
<CommonSettingsStorage/>
|
||||||
|
<ReportsUserSettingsStorage/>
|
||||||
|
<ReportsVariantsStorage/>
|
||||||
|
<FormDataSettingsStorage/>
|
||||||
|
<DynamicListsUserSettingsStorage/>
|
||||||
|
<URLExternalDataStorage/>
|
||||||
|
<Content/>
|
||||||
|
<DefaultReportForm/>
|
||||||
|
<DefaultReportVariantForm/>
|
||||||
|
<DefaultReportSettingsForm/>
|
||||||
|
<DefaultReportAppearanceTemplate/>
|
||||||
|
<DefaultDynamicListSettingsForm/>
|
||||||
|
<DefaultSearchForm/>
|
||||||
|
<DefaultDataHistoryChangeHistoryForm/>
|
||||||
|
<DefaultDataHistoryVersionDataForm/>
|
||||||
|
<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
|
<DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
|
||||||
|
<RequiredMobileApplicationPermissions/>
|
||||||
|
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||||
|
</UsedMobileApplicationFunctionalities>
|
||||||
|
<StandaloneConfigurationRestrictionRoles/>
|
||||||
|
<MobileApplicationURLs/>
|
||||||
|
<AllowedIncomingShareRequestTypes/>$f221WindowVariant
|
||||||
|
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
|
||||||
|
<DefaultInterface/>$f221Captions
|
||||||
|
<DefaultStyle/>
|
||||||
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
|
<BriefInformation/>
|
||||||
|
<DetailedInformation/>
|
||||||
|
<Copyright/>
|
||||||
|
<VendorInformationAddress/>
|
||||||
|
<ConfigurationInformationAddress/>
|
||||||
|
<DataLockControlMode>Managed</DataLockControlMode>
|
||||||
|
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
|
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
|
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
|
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
|
||||||
|
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
|
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||||
|
<DefaultConstantsForm/>
|
||||||
|
</Properties>
|
||||||
|
<ChildObjects>
|
||||||
|
<Language>Русский</Language>
|
||||||
|
</ChildObjects>
|
||||||
|
</Configuration>
|
||||||
|
</MetaDataObject>
|
||||||
|
"@
|
||||||
|
|
||||||
|
# --- Languages/Русский.xml ---
|
||||||
|
$langXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
|
<Language uuid="$uuidLang">
|
||||||
|
<Properties>
|
||||||
|
<Name>Русский</Name>
|
||||||
|
<Synonym>
|
||||||
|
<v8:item>
|
||||||
|
<v8:lang>ru</v8:lang>
|
||||||
|
<v8:content>Русский</v8:content>
|
||||||
|
</v8:item>
|
||||||
|
</Synonym>
|
||||||
|
<Comment/>
|
||||||
|
<LanguageCode>ru</LanguageCode>
|
||||||
|
</Properties>
|
||||||
|
</Language>
|
||||||
|
</MetaDataObject>
|
||||||
|
"@
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
$openPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$caiXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
<top>
|
||||||
|
<panel id="$openPanelInst">
|
||||||
|
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
</panel>
|
||||||
|
</top>
|
||||||
|
<left>
|
||||||
|
<panel id="$sectionsPanelInst">
|
||||||
|
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
</panel>
|
||||||
|
</left>
|
||||||
|
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>
|
||||||
|
"@
|
||||||
|
|
||||||
|
# --- Create directories ---
|
||||||
|
if (-not (Test-Path $OutputDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$langDir = Join-Path $OutputDir "Languages"
|
||||||
|
if (-not (Test-Path $langDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$extDir = Join-Path $OutputDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Write files with UTF-8 BOM ---
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
|
Write-XmlFile $langFile $langXml $enc
|
||||||
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
Write-XmlFile $caiFile $caiXml $enc
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
|
Write-Host " Каталог: $OutputDir"
|
||||||
|
Write-Host " Configuration.xml: $cfgFile"
|
||||||
|
Write-Host " Languages: $langFile"
|
||||||
|
Write-Host " Ext/CAI: $caiFile"
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
|
import sys, os, argparse, re, uuid
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
def new_uuid():
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
|
||||||
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
|
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
|
||||||
|
parser.add_argument('-Version', dest='Version', default='')
|
||||||
|
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||||
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
|
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости.
|
||||||
|
# Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
|
||||||
|
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
|
||||||
|
format_rank_value = format_rank(args.FormatVersion)
|
||||||
|
if format_rank_value == 0:
|
||||||
|
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
|
||||||
|
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
|
||||||
|
f"but was not verified on that platform", file=sys.stderr)
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if (args.CompatibilityMode or "").lower() == "dontuse":
|
||||||
|
print("WARNING: CompatibilityMode 'DontUse' is not \"no restrictions\" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).", file=sys.stderr)
|
||||||
|
|
||||||
|
name = args.Name
|
||||||
|
synonym = args.Synonym if args.Synonym else name
|
||||||
|
output_dir = args.OutputDir
|
||||||
|
version = args.Version
|
||||||
|
vendor = args.Vendor
|
||||||
|
compat = args.CompatibilityMode
|
||||||
|
|
||||||
|
# --- Resolve output dir ---
|
||||||
|
if not os.path.isabs(output_dir):
|
||||||
|
output_dir = os.path.join(os.getcwd(), output_dir)
|
||||||
|
|
||||||
|
# --- Check existing ---
|
||||||
|
cfg_file = os.path.join(output_dir, "Configuration.xml")
|
||||||
|
if os.path.exists(cfg_file):
|
||||||
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Generate UUIDs ---
|
||||||
|
uuid_cfg = new_uuid()
|
||||||
|
uuid_lang = new_uuid()
|
||||||
|
co = [new_uuid() for _ in range(7)]
|
||||||
|
|
||||||
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
is_221 = format_rank_value >= 221
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
is_218 = format_rank_value >= 218
|
||||||
|
|
||||||
|
mobile_funcs = [
|
||||||
|
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
||||||
|
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
||||||
|
("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"),
|
||||||
|
("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"),
|
||||||
|
("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"),
|
||||||
|
("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"),
|
||||||
|
("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"),
|
||||||
|
("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"),
|
||||||
|
("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"),
|
||||||
|
("OSBackup","true"), ("ApplicationUsageStatistics","false"),
|
||||||
|
("BarcodeScanning","false"), ("BackgroundAudioRecording","false"),
|
||||||
|
("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"),
|
||||||
|
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
||||||
|
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
||||||
|
]
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if is_218:
|
||||||
|
mobile_funcs.append(("TextToSpeech", "false"))
|
||||||
|
|
||||||
|
mobile_xml = ""
|
||||||
|
for func_name, func_use in mobile_funcs:
|
||||||
|
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
|
||||||
|
|
||||||
|
# --- Synonym XML ---
|
||||||
|
synonym_xml = ""
|
||||||
|
if synonym:
|
||||||
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
|
class_ids = [
|
||||||
|
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
||||||
|
"9fcd25a0-4822-11d4-9414-008048da11f9",
|
||||||
|
"e3687481-0a87-462c-a166-9f34594f9bba",
|
||||||
|
"9de14907-ec23-4a07-96f0-85521cb6b53b",
|
||||||
|
"51f2d5d8-ea4d-4064-8892-82951750031e",
|
||||||
|
"e68182ea-4237-4383-967f-90c1e3370bc7",
|
||||||
|
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
|
||||||
|
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
|
||||||
|
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
|
||||||
|
pal_ns = ""
|
||||||
|
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
|
||||||
|
if is_221:
|
||||||
|
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
f221_aux_forms = "\r\n" + "\r\n".join(
|
||||||
|
f"\t\t\t{t}" for t in (
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
|
||||||
|
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
|
||||||
|
"</MainClientApplicationWindowInterfaceVariant>"
|
||||||
|
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
|
||||||
|
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
|
||||||
|
"</ClientApplicationWindowsOpenVariant>")
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
|
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
|
||||||
|
"</Version85InterfaceMigrationMode>")
|
||||||
|
|
||||||
|
contained_objects = ""
|
||||||
|
for i in range(7):
|
||||||
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
|
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
||||||
|
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
|
||||||
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
|
\t\t<InternalInfo>
|
||||||
|
{contained_objects}\t\t</InternalInfo>
|
||||||
|
\t\t<Properties>
|
||||||
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
|
\t\t\t<Comment/>
|
||||||
|
\t\t\t<NamePrefix/>
|
||||||
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
|
\t\t\t<UsePurposes>
|
||||||
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
|
\t\t\t</UsePurposes>
|
||||||
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
|
\t\t\t<DefaultRoles/>
|
||||||
|
\t\t\t{vendor_el}
|
||||||
|
\t\t\t{version_el}
|
||||||
|
\t\t\t<UpdateCatalogAddress/>
|
||||||
|
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
|
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
|
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||||
|
\t\t\t<AdditionalFullTextSearchDictionaries/>
|
||||||
|
\t\t\t<CommonSettingsStorage/>
|
||||||
|
\t\t\t<ReportsUserSettingsStorage/>
|
||||||
|
\t\t\t<ReportsVariantsStorage/>
|
||||||
|
\t\t\t<FormDataSettingsStorage/>
|
||||||
|
\t\t\t<DynamicListsUserSettingsStorage/>
|
||||||
|
\t\t\t<URLExternalDataStorage/>
|
||||||
|
\t\t\t<Content/>
|
||||||
|
\t\t\t<DefaultReportForm/>
|
||||||
|
\t\t\t<DefaultReportVariantForm/>
|
||||||
|
\t\t\t<DefaultReportSettingsForm/>
|
||||||
|
\t\t\t<DefaultReportAppearanceTemplate/>
|
||||||
|
\t\t\t<DefaultDynamicListSettingsForm/>
|
||||||
|
\t\t\t<DefaultSearchForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
|
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
|
||||||
|
\t\t\t<RequiredMobileApplicationPermissions/>
|
||||||
|
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
||||||
|
\t\t\t</UsedMobileApplicationFunctionalities>
|
||||||
|
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
||||||
|
\t\t\t<MobileApplicationURLs/>
|
||||||
|
\t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
|
||||||
|
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
|
||||||
|
\t\t\t<DefaultInterface/>{f221_captions}
|
||||||
|
\t\t\t<DefaultStyle/>
|
||||||
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
|
\t\t\t<BriefInformation/>
|
||||||
|
\t\t\t<DetailedInformation/>
|
||||||
|
\t\t\t<Copyright/>
|
||||||
|
\t\t\t<VendorInformationAddress/>
|
||||||
|
\t\t\t<ConfigurationInformationAddress/>
|
||||||
|
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
|
||||||
|
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
|
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
|
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
|
||||||
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
|
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||||
|
\t\t\t<DefaultConstantsForm/>
|
||||||
|
\t\t</Properties>
|
||||||
|
\t\t<ChildObjects>
|
||||||
|
\t\t\t<Language>Русский</Language>
|
||||||
|
\t\t</ChildObjects>
|
||||||
|
\t</Configuration>
|
||||||
|
</MetaDataObject>'''
|
||||||
|
|
||||||
|
# --- Languages/Русский.xml ---
|
||||||
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
|
\t<Language uuid="{uuid_lang}">
|
||||||
|
\t\t<Properties>
|
||||||
|
\t\t\t<Name>Русский</Name>
|
||||||
|
\t\t\t<Synonym>
|
||||||
|
\t\t\t\t<v8:item>
|
||||||
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
|
\t\t\t\t\t<v8:content>Русский</v8:content>
|
||||||
|
\t\t\t\t</v8:item>
|
||||||
|
\t\t\t</Synonym>
|
||||||
|
\t\t\t<Comment/>
|
||||||
|
\t\t\t<LanguageCode>ru</LanguageCode>
|
||||||
|
\t\t</Properties>
|
||||||
|
\t</Language>
|
||||||
|
</MetaDataObject>'''
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
open_panel_inst = new_uuid()
|
||||||
|
sections_panel_inst = new_uuid()
|
||||||
|
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
\t<top>
|
||||||
|
\t\t<panel id="{open_panel_inst}">
|
||||||
|
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</top>
|
||||||
|
\t<left>
|
||||||
|
\t\t<panel id="{sections_panel_inst}">
|
||||||
|
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</left>
|
||||||
|
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>'''
|
||||||
|
|
||||||
|
# --- Create directories ---
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
lang_dir = os.path.join(output_dir, "Languages")
|
||||||
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
ext_dir = os.path.join(output_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- Write files ---
|
||||||
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
|
write_xml_file(lang_file, lang_xml)
|
||||||
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
write_xml_file(cai_file, cai_xml)
|
||||||
|
|
||||||
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
|
print(f" Каталог: {output_dir}")
|
||||||
|
print(f" Configuration.xml: {cfg_file}")
|
||||||
|
print(f" Languages: {lang_file}")
|
||||||
|
print(f" Ext/CAI: {cai_file}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
python ".opencode/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
python ".opencode/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
+24
-5
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.3 — Validate 1C configuration root structure
|
# cf-validate v1.7 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -89,6 +89,19 @@ $finalize = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||||
@@ -104,11 +117,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -125,6 +138,7 @@ $childTypeDirMap = @{
|
|||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
|
"Bot"="Bots"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
@@ -202,10 +216,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
|
$versionRank = Get-FormatRank $version
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($versionRank -eq 0) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
Report-Error "1. Malformed version '$version' (expected N.N)"
|
||||||
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
+50
-6
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.3 — Validate 1C configuration XML structure
|
# cf-validate v1.7 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -33,11 +55,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -109,6 +132,20 @@ VALID_ENUM_VALUES = {
|
|||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
class Reporter:
|
class Reporter:
|
||||||
def __init__(self, max_errors, detailed=False):
|
def __init__(self, max_errors, detailed=False):
|
||||||
@@ -169,7 +206,7 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
@@ -229,10 +266,17 @@ def main():
|
|||||||
check1_ok = False
|
check1_ok = False
|
||||||
|
|
||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version_rank == 0:
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
r.error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
r.warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
r.warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -41,7 +41,6 @@ allowed-tools:
|
|||||||
- `Enum.ВидыОплат` — перечисление
|
- `Enum.ВидыОплат` — перечисление
|
||||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||||
Поддерживаются все 44 типа объектов конфигурации.
|
|
||||||
|
|
||||||
### Заимствование форм
|
### Заимствование форм
|
||||||
|
|
||||||
@@ -71,31 +70,33 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
python ".opencode/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Заимствовать один объект
|
# Заимствовать один объект
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||||
|
|
||||||
# Несколько объектов за раз
|
# Несколько объектов за раз
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||||
|
|
||||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||||
|
|
||||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <ExtensionPath>
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
+692
-146
File diff suppressed because it is too large
Load Diff
+753
-156
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
python ".opencode/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
@@ -50,8 +50,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -Exte
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор — что изменено в расширении
|
# Обзор — что изменено в расширении
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
|
|
||||||
# Проверка переноса — все ли #Вставка перенесены
|
# Проверка переноса — все ли #Вставка перенесены
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
|
||||||
```
|
```
|
||||||
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -49,7 +49,9 @@ $childTypeDirMap = @{
|
|||||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"CommonAttribute"="CommonAttributes"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||||
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
|
"Bot"="Bots"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Parse extension Configuration.xml ---
|
# --- Parse extension Configuration.xml ---
|
||||||
+30
-2
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespace maps ---
|
# --- Namespace maps ---
|
||||||
|
|
||||||
MD_NSMAP = {
|
MD_NSMAP = {
|
||||||
@@ -60,6 +82,12 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"Sequence": "Sequences",
|
"Sequence": "Sequences",
|
||||||
"IntegrationService": "IntegrationServices",
|
"IntegrationService": "IntegrationServices",
|
||||||
"CommonAttribute": "CommonAttributes",
|
"CommonAttribute": "CommonAttributes",
|
||||||
|
"Style": "Styles",
|
||||||
|
"XDTOPackage": "XDTOPackages",
|
||||||
|
"WebService": "WebServices",
|
||||||
|
"HTTPService": "HTTPServices",
|
||||||
|
"WSReference": "WSReferences",
|
||||||
|
"Bot": "Bots",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -468,7 +496,7 @@ def main():
|
|||||||
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
||||||
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
||||||
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
@@ -33,39 +33,39 @@ allowed-tools:
|
|||||||
| `Name` | Имя расширения (обязат.) | — |
|
| `Name` | Имя расширения (обязат.) | — |
|
||||||
| `Synonym` | Синоним | = Name |
|
| `Synonym` | Синоним | = Name |
|
||||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||||
| `OutputDir` | Каталог для создания | `src` |
|
| `OutputDir` | Каталог для создания; клади расширение в свой подкаталог, названный по имени: `src\cfe\<Name>` | `src` |
|
||||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||||
| `Version` | Версия расширения | — |
|
| `Version` | Версия расширения | — |
|
||||||
| `Vendor` | Поставщик | — |
|
| `Vendor` | Поставщик | — |
|
||||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
| `CompatibilityMode` | Режим совместимости; при заданном `ConfigPath` определяется по базовой конфигурации и этот параметр не нужен | `Version8_3_24` |
|
||||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||||
| `NoRole` | Без основной роли | false |
|
| `NoRole` | Без основной роли | false |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
python ".opencode/skills/cfe-init/scripts/cfe-init.py" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
... -Name Расш1 -ConfigPath src\cf -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-исправление с явным режимом совместимости
|
# Расширение-исправление с явным режимом совместимости
|
||||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-доработка с версией
|
# Расширение-доработка с версией
|
||||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src\cfe\МоёРасширение
|
||||||
|
|
||||||
# Без роли, с явным префиксом
|
# Без роли, с явным префиксом
|
||||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src\cfe\ИсправлениеБага
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <OutputDir>
|
/cfe-validate <OutputDir> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
+67
-18
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -16,6 +16,12 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Default NamePrefix ---
|
# --- Default NamePrefix ---
|
||||||
@@ -35,6 +41,10 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
$formatVersion = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -75,6 +85,11 @@ if ($ConfigPath) {
|
|||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if ($fmtVer) {
|
||||||
|
$formatVersion = $fmtVer
|
||||||
|
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||||
|
}
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -112,20 +127,23 @@ $co7 = [guid]::NewGuid().ToString()
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
$synonymXml = ""
|
$synonymXml = ""
|
||||||
if ($Synonym) {
|
if ($Synonym) {
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional properties ---
|
# --- Optional properties ---
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
$defaultRolesXml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
$defaultRolesEl = "<DefaultRoles/>"
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
$defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
@@ -135,10 +153,32 @@ if (-not $NoRole) {
|
|||||||
}
|
}
|
||||||
$childObjectsXml += "`r`n`t`t"
|
$childObjectsXml += "`r`n`t`t"
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
$f221Captions = ""
|
||||||
|
if ((Get-FormatRank $formatVersion) -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
$f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -172,21 +212,21 @@ $cfgXml = @"
|
|||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
<Synonym>$synonymXml</Synonym>
|
<Synonym>$synonymXml</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
<NamePrefix>$(Esc-XmlText ($NamePrefix))</NamePrefix>
|
||||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
</UsePurposes>
|
</UsePurposes>
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
$defaultRolesEl
|
||||||
<Vendor>$vendorXml</Vendor>
|
$vendorEl
|
||||||
<Version>$versionXml</Version>
|
$versionEl$f221Captions
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
<BriefInformation/>
|
<BriefInformation/>
|
||||||
<DetailedInformation/>
|
<DetailedInformation/>
|
||||||
@@ -203,7 +243,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -220,10 +260,10 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$(Esc-XmlText ($roleName))</Name>
|
||||||
<Synonym/>
|
<Synonym/>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
</Properties>
|
</Properties>
|
||||||
@@ -243,9 +283,18 @@ if (-not (Test-Path $langDir)) {
|
|||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
Write-XmlFile $langFile $langXml $enc
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
@@ -254,7 +303,7 @@ if (-not $NoRole) {
|
|||||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
Write-XmlFile $roleFile $roleXml $enc
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
+111
-22
@@ -1,20 +1,62 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -29,7 +71,7 @@ def main():
|
|||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
||||||
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -50,6 +92,10 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
format_version = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -88,6 +134,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
|
fmt_ver = base_cfg_root.get("version")
|
||||||
|
if fmt_ver:
|
||||||
|
format_version = fmt_ver
|
||||||
|
print(f"[INFO] Base config format version: {format_version}")
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -118,18 +168,23 @@ def main():
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
synonym_xml = ""
|
synonym_xml = ""
|
||||||
if synonym:
|
if synonym:
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
version_xml = esc_xml(version) if version else ""
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
role_name = f"{name_prefix}ОсновнаяРоль"
|
role_name = f"{name_prefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
default_roles_xml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
default_roles_el = "<DefaultRoles/>"
|
||||||
if not args.NoRole:
|
if not args.NoRole:
|
||||||
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
|
default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
|
||||||
|
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
|
||||||
|
'\r\n\t\t\t</DefaultRoles>')
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
||||||
@@ -148,6 +203,40 @@ def main():
|
|||||||
]
|
]
|
||||||
|
|
||||||
contained_objects = ""
|
contained_objects = ""
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
f221_captions = ""
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
||||||
@@ -155,27 +244,27 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
||||||
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
|
\t\t\t<NamePrefix>{esc_xml_text(name_prefix)}</NamePrefix>
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
\t\t\t<UsePurposes>
|
\t\t\t<UsePurposes>
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
\t\t\t</UsePurposes>
|
\t\t\t</UsePurposes>
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
|
\t\t\t{default_roles_el}
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
\t\t\t{vendor_el}
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
\t\t\t{version_el}{f221_captions}
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
\t\t\t<BriefInformation/>
|
\t\t\t<BriefInformation/>
|
||||||
\t\t\t<DetailedInformation/>
|
\t\t\t<DetailedInformation/>
|
||||||
@@ -190,7 +279,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -205,10 +294,10 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml_text(role_name)}</Name>
|
||||||
\t\t\t<Synonym/>
|
\t\t\t<Synonym/>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t</Properties>
|
\t\t</Properties>
|
||||||
@@ -221,9 +310,9 @@ def main():
|
|||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_xml_file(lang_file, lang_xml)
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
role_file = None
|
role_file = None
|
||||||
@@ -231,7 +320,7 @@ def main():
|
|||||||
role_dir = os.path.join(output_dir, "Roles")
|
role_dir = os.path.join(output_dir, "Roles")
|
||||||
os.makedirs(role_dir, exist_ok=True)
|
os.makedirs(role_dir, exist_ok=True)
|
||||||
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
||||||
write_utf8_bom(role_file, role_xml)
|
write_xml_file(role_file, role_xml)
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
print(f"[OK] Создано расширение: {name}")
|
print(f"[OK] Создано расширение: {name}")
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
name: cfe-patch-method
|
||||||
|
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
|
||||||
|
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /cfe-patch-method — Генерация перехватчика метода
|
||||||
|
|
||||||
|
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
|
||||||
|
|
||||||
|
## Предусловие
|
||||||
|
|
||||||
|
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
|
||||||
|
|
||||||
|
### Авто-определение ConfigPath
|
||||||
|
|
||||||
|
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||||
|
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||||
|
4. Если `configSrc` нет — спроси у пользователя
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Описание | По умолчанию |
|
||||||
|
|----------|----------|--------------|
|
||||||
|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||||
|
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|
||||||
|
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
|
||||||
|
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
|
||||||
|
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
|
||||||
|
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
|
||||||
|
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
|
||||||
|
|
||||||
|
## Формат ModulePath
|
||||||
|
|
||||||
|
| ModulePath | Файл |
|
||||||
|
|------------|------|
|
||||||
|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
||||||
|
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
||||||
|
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||||
|
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
||||||
|
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
||||||
|
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||||
|
|
||||||
|
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||||
|
|
||||||
|
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
|
||||||
|
|
||||||
|
## Типы перехвата
|
||||||
|
|
||||||
|
| InterceptorType | Декоратор | Назначение | Применим к |
|
||||||
|
|-----------------|-----------|------------|------------|
|
||||||
|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
|
||||||
|
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
|
||||||
|
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
|
||||||
|
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
|
||||||
|
|
||||||
|
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
|
||||||
|
|
||||||
|
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
|
||||||
|
|
||||||
|
- **Добавляешь код** → оберни его `#Вставка` … `#КонецВставки`.
|
||||||
|
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление` … `#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
|
||||||
|
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```bsl
|
||||||
|
&ИзменениеИКонтроль("ПриЗаписи")
|
||||||
|
Процедура Расш_ПриЗаписи(Отказ)
|
||||||
|
СуммаДокумента = РассчитатьСумму();
|
||||||
|
#Вставка
|
||||||
|
// доработка: округляем
|
||||||
|
СуммаДокумента = Окр(СуммаДокумента, 2);
|
||||||
|
#КонецВставки
|
||||||
|
#Удаление
|
||||||
|
Записать();
|
||||||
|
#КонецУдаления
|
||||||
|
#Вставка
|
||||||
|
ЗаписатьСПроверкой(Отказ);
|
||||||
|
#КонецВставки
|
||||||
|
КонецПроцедуры
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||||
|
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||||
|
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||||
|
|
||||||
|
## Актуализация
|
||||||
|
|
||||||
|
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
|
||||||
|
|
||||||
|
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
|
||||||
|
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
|
||||||
|
|
||||||
|
Статусы в выводе:
|
||||||
|
|
||||||
|
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
|
||||||
|
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
|
||||||
|
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
|
||||||
|
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
|
||||||
|
|
||||||
|
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/cfe-patch-method/scripts/cfe-patch-method.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Код перед записью
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
|
||||||
|
# Перехват После на форме
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||||
|
|
||||||
|
# Замена функции (ПродолжитьВызов)
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||||
|
|
||||||
|
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||||
|
... -ExtensionPath src\cfe\extname -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# Проверить все контролируемые методы расширения на дрейф
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Check
|
||||||
|
|
||||||
|
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Actualize
|
||||||
|
```
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
```
|
||||||
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user