mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-13 23:13:22 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c0a1e0fb |
@@ -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,188 +0,0 @@
|
|||||||
# db-create v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,162 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-create v1.1 — Create 1C information base
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,191 +0,0 @@
|
|||||||
# db-dump-cf v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,168 +0,0 @@
|
|||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Выгрузка информационной базы 1С в DT-файл
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER OutputFile
|
|
||||||
Путь к выходному DT-файлу
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$OutputFile
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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_dt_$(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 += "/DumpIB", "`"$OutputFile`""
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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 information base to DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- 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_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpIB", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
# db-dump-xml v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,206 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,191 +0,0 @@
|
|||||||
# db-load-cf v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,183 +0,0 @@
|
|||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка информационной базы 1С из DT-файла
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
|
|
||||||
ВНИМАНИЕ: операция полностью перезаписывает базу.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER InputFile
|
|
||||||
Путь к DT-файлу для загрузки
|
|
||||||
|
|
||||||
.PARAMETER JobsCount
|
|
||||||
Количество фоновых заданий для загрузки (0 = по числу процессоров)
|
|
||||||
|
|
||||||
.PARAMETER UnlockCode
|
|
||||||
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$InputFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[int]$JobsCount = 0,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UnlockCode
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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_dt_$(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`"" }
|
|
||||||
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
|
|
||||||
|
|
||||||
$arguments += "/RestoreIB", "`"$InputFile`""
|
|
||||||
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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 information base from DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-JobsCount", type=int, default=0)
|
|
||||||
parser.add_argument("-UnlockCode", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- 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_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
if args.UnlockCode:
|
|
||||||
arguments.append(f"/UC{args.UnlockCode}")
|
|
||||||
|
|
||||||
arguments.extend(["/RestoreIB", args.InputFile])
|
|
||||||
if args.JobsCount > 0:
|
|
||||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
# db-load-git v1.5 — 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) {
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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 = @()
|
|
||||||
$supportSkipped = @()
|
|
||||||
|
|
||||||
foreach ($file in $changedFiles) {
|
|
||||||
$file = $file.Trim().Replace('\', '/')
|
|
||||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
|
||||||
|
|
||||||
# Skip service files (not partially loadable). Support-state files are tracked
|
|
||||||
# to warn the user: support changes apply only via a full load.
|
|
||||||
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
|
|
||||||
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
|
|
||||||
|
|
||||||
$fullPath = Join-Path $ConfigDir $file
|
|
||||||
|
|
||||||
if ($file -match '\.xml$') {
|
|
||||||
# XML file — add directly if exists
|
|
||||||
if (Test-Path $fullPath) {
|
|
||||||
if ($configFiles -notcontains $file) {
|
|
||||||
$configFiles += $file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
|
||||||
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
|
|
||||||
if ($objectXml) {
|
|
||||||
$fullXmlPath = Join-Path $ConfigDir $objectXml
|
|
||||||
if (Test-Path $fullXmlPath) {
|
|
||||||
if ($configFiles -notcontains $objectXml) {
|
|
||||||
$configFiles += $objectXml
|
|
||||||
}
|
|
||||||
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
|
|
||||||
$configFiles += $file
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add all files from Ext/ directory of the object
|
|
||||||
$parts = $file -split '[\\/]'
|
|
||||||
if ($parts.Count -ge 2) {
|
|
||||||
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
|
|
||||||
if (Test-Path $extDir) {
|
|
||||||
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
|
|
||||||
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
|
|
||||||
if ($configFiles -notcontains $extRelPath) {
|
|
||||||
$configFiles += $extRelPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($supportSkipped.Count -gt 0) {
|
|
||||||
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
|
|
||||||
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
|
|
||||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configFiles.Count -eq 0) {
|
|
||||||
Write-Host "No configuration files found in changes"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Files for loading: $($configFiles.Count)"
|
|
||||||
foreach ($f in $configFiles) { Write-Host " $f" }
|
|
||||||
|
|
||||||
# --- DryRun: stop here ---
|
|
||||||
if ($DryRun) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "DryRun mode - no changes applied"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- 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,328 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-git v1.5 — Load Git changes into 1C database
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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 = []
|
|
||||||
support_skipped = []
|
|
||||||
|
|
||||||
for file in changed_files:
|
|
||||||
file = file.strip().replace("\\", "/")
|
|
||||||
if not file:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip service files (not partially loadable). Support-state files are
|
|
||||||
# tracked to warn: support changes apply only via a full load.
|
|
||||||
if file.endswith("ParentConfigurations.bin"):
|
|
||||||
support_skipped.append(file)
|
|
||||||
continue
|
|
||||||
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
full_path = os.path.join(args.ConfigDir, file)
|
|
||||||
|
|
||||||
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 support_skipped:
|
|
||||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
|
||||||
for sf in support_skipped:
|
|
||||||
print(f" - {sf}", file=sys.stderr)
|
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
|
||||||
|
|
||||||
if len(config_files) == 0:
|
|
||||||
print("No configuration files found in changes")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
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,315 +0,0 @@
|
|||||||
# db-load-xml v1.5 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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
|
|
||||||
$rawList = @()
|
|
||||||
if ($ListFile) {
|
|
||||||
if (-not (Test-Path $ListFile)) {
|
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
||||||
} else {
|
|
||||||
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
||||||
}
|
|
||||||
|
|
||||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
|
||||||
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
|
|
||||||
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
|
|
||||||
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
|
|
||||||
if ($supportFiles.Count -gt 0) {
|
|
||||||
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
|
|
||||||
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
|
|
||||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
if ($fileList.Count -eq 0) {
|
|
||||||
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
|
||||||
Write-Host "Files to load: $($fileList.Count)"
|
|
||||||
foreach ($f in $fileList) { Write-Host " $f" }
|
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$generatedListFile`""
|
|
||||||
$arguments += "-partial"
|
|
||||||
$arguments += "-updateConfigDumpInfo"
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$arguments += "/UpdateDBCfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Read log ---
|
|
||||||
$logContent = $null
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
$fatalLogPatterns = @(
|
|
||||||
'Неверное свойство объекта метаданных',
|
|
||||||
'не входит в состав объекта метаданных',
|
|
||||||
'Неизвестное имя типа',
|
|
||||||
'Неизвестный объект метаданных',
|
|
||||||
'Ни один из документов не является регистратором для регистра',
|
|
||||||
'Неверное значение перечисления',
|
|
||||||
'не может быть приведен к типу'
|
|
||||||
)
|
|
||||||
$silentFailures = @()
|
|
||||||
if ($logContent) {
|
|
||||||
foreach ($line in ($logContent -split "`r?`n")) {
|
|
||||||
foreach ($pat in $fatalLogPatterns) {
|
|
||||||
if ($line -match [regex]::Escape($pat)) {
|
|
||||||
$silentFailures += $line.Trim()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
|
||||||
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
|
||||||
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($silentFailures.Count -gt 0) {
|
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
|
||||||
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
|
|
||||||
Write-Host $msg -ForegroundColor Yellow
|
|
||||||
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
|
||||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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
|
|
||||||
if args.ListFile:
|
|
||||||
if not os.path.isfile(args.ListFile):
|
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
|
||||||
raw_list = [ln.strip() for ln in f if ln.strip()]
|
|
||||||
else:
|
|
||||||
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
|
||||||
|
|
||||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
|
||||||
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
|
|
||||||
support_files = [x for x in raw_list if support_re.search(x)]
|
|
||||||
file_list = [x for x in raw_list if not support_re.search(x)]
|
|
||||||
if support_files:
|
|
||||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
|
||||||
for sf in support_files:
|
|
||||||
print(f" - {sf}", file=sys.stderr)
|
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
|
||||||
if not file_list:
|
|
||||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(file_list))
|
|
||||||
print(f"Files to load: {len(file_list)}")
|
|
||||||
for fl in file_list:
|
|
||||||
print(f" {fl}")
|
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
|
||||||
arguments.append("-partial")
|
|
||||||
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,170 +0,0 @@
|
|||||||
# db-run v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,129 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-run v1.1 — Launch 1C:Enterprise
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,209 +0,0 @@
|
|||||||
# db-update v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-update v1.1 — Update 1C database configuration
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,198 +0,0 @@
|
|||||||
# epf-build v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,176 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-build v1.1 — 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 json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,192 +0,0 @@
|
|||||||
# epf-dump v1.1 — 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 ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 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,169 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-dump v1.1 — 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 json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if 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,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,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,852 +0,0 @@
|
|||||||
# mxl-compile v1.3 — 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
|
|
||||||
|
|
||||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
|
||||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
|
||||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
|
||||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
|
||||||
# throws — guard errors degrade to allow.
|
|
||||||
function Get-RootUuid([string]$xmlPath) {
|
|
||||||
if (-not (Test-Path $xmlPath)) { return $null }
|
|
||||||
try {
|
|
||||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
|
||||||
$el = $mx.DocumentElement.FirstChild
|
|
||||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
|
||||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Find-V8Project([string]$startDir) {
|
|
||||||
$d = $startDir
|
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
|
||||||
$pj = Join-Path $d ".v8-project.json"
|
|
||||||
if (Test-Path $pj) { return $pj }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Get-EditMode([string]$cfgDir) {
|
|
||||||
try {
|
|
||||||
$pj = Find-V8Project (Get-Location).Path
|
|
||||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
|
||||||
if (-not $pj) { return 'deny' }
|
|
||||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
|
||||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
|
||||||
if ($proj.databases) {
|
|
||||||
foreach ($db in $proj.databases) {
|
|
||||||
if ($db.configSrc) {
|
|
||||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
|
||||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
|
||||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
|
||||||
return 'deny'
|
|
||||||
} catch { return 'deny' }
|
|
||||||
}
|
|
||||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|
||||||
try {
|
|
||||||
$rp = $targetPath
|
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
|
||||||
$elemUuid = Get-RootUuid $rp
|
|
||||||
$cfgDir = $null; $binPath = $null
|
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
|
||||||
if (-not $cfgDir) {
|
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
|
||||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
|
||||||
}
|
|
||||||
if ($elemUuid -and $cfgDir) { break }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
# New object (no element file): fall back to config root uuid.
|
|
||||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
|
||||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
|
||||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
|
||||||
if ($bytes.Length -le 32) { return }
|
|
||||||
$start = 0
|
|
||||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
|
||||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
|
||||||
if (-not $hm.Success) { return }
|
|
||||||
$G = [int]$hm.Groups[1].Value
|
|
||||||
$K = [int]$hm.Groups[2].Value
|
|
||||||
if ($K -eq 0) { return }
|
|
||||||
$best = $null
|
|
||||||
if ($elemUuid) {
|
|
||||||
$u = [regex]::Escape($elemUuid.ToLower())
|
|
||||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
|
||||||
$f1 = [int]$m.Groups[1].Value
|
|
||||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$blocked = $false; $code = ""; $reason = ""
|
|
||||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
|
||||||
elseif ($require -eq 'removed') {
|
|
||||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
|
||||||
}
|
|
||||||
if (-not $blocked) { return }
|
|
||||||
$mode = Get-EditMode $cfgDir
|
|
||||||
if ($mode -eq 'off') { return }
|
|
||||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
|
||||||
# latter throws and would be swallowed by this function's own catch.
|
|
||||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
|
||||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if ($code -eq "capability-off") {
|
|
||||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
|
||||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
} elseif ($code -eq "not-removed") {
|
|
||||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
|
||||||
} else {
|
|
||||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
|
||||||
}
|
|
||||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
|
||||||
exit 1
|
|
||||||
} catch { return }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 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 }
|
|
||||||
Assert-EditAllowed $resolvedPath 'editable'
|
|
||||||
[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,799 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# mxl-compile v1.3 — 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
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
|
||||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
|
||||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def _sg_root_uuid(xml_path):
|
|
||||||
if not os.path.isfile(xml_path):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
mx = etree.parse(xml_path).getroot()
|
|
||||||
for child in mx:
|
|
||||||
if isinstance(child.tag, str) and child.get("uuid"):
|
|
||||||
return child.get("uuid")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
|
||||||
d = start_dir
|
|
||||||
for _ in range(20):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
pj = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pj):
|
|
||||||
return pj
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_get_edit_mode(cfg_dir):
|
|
||||||
try:
|
|
||||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
|
||||||
if not pj:
|
|
||||||
return "deny"
|
|
||||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
|
||||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
|
||||||
for db in proj.get("databases", []):
|
|
||||||
src = db.get("configSrc")
|
|
||||||
if src:
|
|
||||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
|
||||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
|
||||||
if db.get("editingAllowedCheck"):
|
|
||||||
return db["editingAllowedCheck"]
|
|
||||||
if proj.get("editingAllowedCheck"):
|
|
||||||
return proj["editingAllowedCheck"]
|
|
||||||
return "deny"
|
|
||||||
except Exception:
|
|
||||||
return "deny"
|
|
||||||
|
|
||||||
|
|
||||||
def assert_edit_allowed(target_path, require):
|
|
||||||
try:
|
|
||||||
rp = os.path.abspath(target_path)
|
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
|
||||||
cfg_dir = None
|
|
||||||
bin_path = None
|
|
||||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
|
||||||
for _ in range(12):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
if not elem_uuid:
|
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
|
||||||
if not cfg_dir:
|
|
||||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
|
||||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
|
||||||
cfg_dir = d
|
|
||||||
bin_path = cand
|
|
||||||
if elem_uuid and cfg_dir:
|
|
||||||
break
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
if not elem_uuid and cfg_dir:
|
|
||||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
|
||||||
if not bin_path or not os.path.exists(bin_path):
|
|
||||||
return
|
|
||||||
data = open(bin_path, "rb").read()
|
|
||||||
if len(data) <= 32:
|
|
||||||
return
|
|
||||||
if data[:3] == b"\xef\xbb\xbf":
|
|
||||||
data = data[3:]
|
|
||||||
text = data.decode("utf-8", "replace")
|
|
||||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
|
||||||
if not h:
|
|
||||||
return
|
|
||||||
g = int(h.group(1))
|
|
||||||
k = int(h.group(2))
|
|
||||||
if k == 0:
|
|
||||||
return
|
|
||||||
best = None
|
|
||||||
if elem_uuid:
|
|
||||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
|
||||||
f1 = int(m.group(1))
|
|
||||||
if best is None or f1 < best:
|
|
||||||
best = f1
|
|
||||||
blocked = False
|
|
||||||
code = ""
|
|
||||||
reason = ""
|
|
||||||
if g == 1:
|
|
||||||
blocked = True
|
|
||||||
code = "capability-off"
|
|
||||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
|
||||||
elif require == "removed":
|
|
||||||
if best is not None and best != 2:
|
|
||||||
blocked = True
|
|
||||||
code = "not-removed"
|
|
||||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
|
||||||
else:
|
|
||||||
if best is not None and best == 0:
|
|
||||||
blocked = True
|
|
||||||
code = "locked"
|
|
||||||
reason = "объект на замке — редактирование сломает обновления"
|
|
||||||
if not blocked:
|
|
||||||
return
|
|
||||||
mode = _sg_get_edit_mode(cfg_dir)
|
|
||||||
if mode == "off":
|
|
||||||
return
|
|
||||||
if mode == "warn":
|
|
||||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
|
||||||
return
|
|
||||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if code == "capability-off":
|
|
||||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
|
||||||
fix = (
|
|
||||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
|
||||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
|
||||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
|
||||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
)
|
|
||||||
elif code == "not-removed":
|
|
||||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
fix = (
|
|
||||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
fix = (
|
|
||||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
|
||||||
)
|
|
||||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
assert_edit_allowed(out_path, "editable")
|
|
||||||
|
|
||||||
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,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'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
+39
-6
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.7 — 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,
|
||||||
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
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) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
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
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
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 $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -150,10 +163,17 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
Assert-EditAllowed $resolvedPath 'editable'
|
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
|
||||||
@@ -196,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",
|
||||||
@@ -212,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"
|
||||||
@@ -673,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"
|
||||||
}
|
}
|
||||||
@@ -700,6 +722,7 @@ $script:ruTypeMap = @{
|
|||||||
"регистррасчёта" = "CalculationRegister"
|
"регистррасчёта" = "CalculationRegister"
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
"задача" = "Task"
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
@@ -850,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
|
||||||
@@ -861,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"
|
||||||
}
|
}
|
||||||
@@ -963,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)
|
||||||
+141
-21
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.7 — 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
|
||||||
@@ -12,6 +12,65 @@ 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
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -33,6 +92,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
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):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -72,6 +143,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
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)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -79,6 +153,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -182,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",
|
||||||
@@ -198,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",
|
||||||
@@ -307,13 +383,50 @@ 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:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -326,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)
|
||||||
@@ -357,6 +470,10 @@ def main():
|
|||||||
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
|
||||||
@@ -705,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)
|
||||||
@@ -771,6 +888,7 @@ def main():
|
|||||||
"регистррасчета": "CalculationRegister",
|
"регистррасчета": "CalculationRegister",
|
||||||
"регистррасчёта": "CalculationRegister",
|
"регистррасчёта": "CalculationRegister",
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
"хранилищенастроек": "SettingsStorage",
|
"хранилищенастроек": "SettingsStorage",
|
||||||
}
|
}
|
||||||
@@ -859,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)
|
||||||
@@ -905,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'
|
||||||
@@ -927,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:
|
||||||
@@ -937,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)
|
||||||
@@ -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 "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.3 — 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"="Регламентные задания"
|
||||||
+26
-3
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.3 — 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
|
||||||
@@ -12,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")
|
||||||
@@ -20,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 = []
|
||||||
@@ -95,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",
|
||||||
@@ -111,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": "Регламентные задания",
|
||||||
@@ -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>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
+452
-120
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.25 — Borrow objects from configuration into extension (CFE) (не переносить FoldersOnTop — платформа его не хранит)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -13,6 +13,59 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
||||||
|
function Strip-FormBindings {
|
||||||
|
param([string]$xml, [bool]$keepObjekt)
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
if ($keepObjekt) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
||||||
|
} else {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tag in $script:formBindingPictureTags) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||||
|
# в <xr:DataPath> и обычным стриппингом не снимается. Когда основной реквизит не заимствован,
|
||||||
|
# текстовый «Объект.X» в расширении не разрешается («Неверный путь к полю»), поэтому Конфигуратор
|
||||||
|
# переписывает его в непрозрачную форму «1/0:<uuid реквизита объекта>» — ссылка остаётся рабочей.
|
||||||
|
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
|
||||||
|
# Путь всегда односегментный (корпусная проверка: 285 из 285 у УТ), глубже — тоже вырезаем.
|
||||||
|
function Rewrite-ChoiceParameterLinks {
|
||||||
|
param([string]$xml, $attrUuids)
|
||||||
|
|
||||||
|
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
|
||||||
|
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
|
||||||
|
param($m)
|
||||||
|
$link = $m.Value
|
||||||
|
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>Объект\.([^<]+)</xr:DataPath>')
|
||||||
|
if (-not $dp.Success) { return $link }
|
||||||
|
$attrName = $dp.Groups[1].Value
|
||||||
|
if ($attrUuids.ContainsKey($attrName)) {
|
||||||
|
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)Объект\.[^<]+(</xr:DataPath>)', "`${1}1/0:$($attrUuids[$attrName])`${2}")
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# Опустевший контейнер платформе не нужен
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -100,7 +153,7 @@ $childTypeDirMap = @{
|
|||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 4b. Russian synonym → English type ---
|
# --- 4b. Russian synonym → English type ---
|
||||||
@@ -128,7 +181,7 @@ $synonymMap = @{
|
|||||||
$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",
|
||||||
@@ -184,7 +237,8 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
||||||
)
|
)
|
||||||
"AccountingRegister" = @(
|
"AccountingRegister" = @(
|
||||||
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
||||||
|
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
|
||||||
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
||||||
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
||||||
@{ prefix = "AccountingRegisterList"; category = "List" }
|
@{ prefix = "AccountingRegisterList"; category = "List" }
|
||||||
@@ -198,6 +252,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "CalculationRegisterList"; category = "List" }
|
@{ prefix = "CalculationRegisterList"; category = "List" }
|
||||||
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
||||||
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
||||||
|
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
|
||||||
)
|
)
|
||||||
"ChartOfAccounts" = @(
|
"ChartOfAccounts" = @(
|
||||||
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
||||||
@@ -205,12 +260,15 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
||||||
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
|
||||||
)
|
)
|
||||||
"ChartOfCharacteristicTypes" = @(
|
"ChartOfCharacteristicTypes" = @(
|
||||||
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
||||||
|
@{ prefix = "Characteristic"; category = "Characteristic" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
||||||
)
|
)
|
||||||
"ChartOfCalculationTypes" = @(
|
"ChartOfCalculationTypes" = @(
|
||||||
@@ -220,8 +278,11 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
||||||
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
||||||
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
||||||
|
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
|
||||||
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
||||||
|
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
|
||||||
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
||||||
|
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
|
||||||
)
|
)
|
||||||
"BusinessProcess" = @(
|
"BusinessProcess" = @(
|
||||||
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
||||||
@@ -229,6 +290,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
||||||
@{ prefix = "BusinessProcessList"; category = "List" }
|
@{ prefix = "BusinessProcessList"; category = "List" }
|
||||||
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
||||||
|
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
|
||||||
)
|
)
|
||||||
"Task" = @(
|
"Task" = @(
|
||||||
@{ prefix = "TaskObject"; category = "Object" }
|
@{ prefix = "TaskObject"; category = "Object" }
|
||||||
@@ -260,14 +322,36 @@ $script:generatedTypes = @{
|
|||||||
"DefinedType" = @(
|
"DefinedType" = @(
|
||||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||||
)
|
)
|
||||||
|
"Sequence" = @(
|
||||||
|
@{ prefix = "SequenceRecord"; category = "Record" }
|
||||||
|
@{ prefix = "SequenceManager"; category = "Manager" }
|
||||||
|
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
|
||||||
|
)
|
||||||
|
"FilterCriterion" = @(
|
||||||
|
@{ prefix = "FilterCriterionManager"; category = "Manager" }
|
||||||
|
@{ prefix = "FilterCriterionList"; category = "List" }
|
||||||
|
)
|
||||||
|
"SettingsStorage" = @(
|
||||||
|
@{ prefix = "SettingsStorageManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"IntegrationService" = @(
|
||||||
|
@{ prefix = "IntegrationServiceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"WSReference" = @(
|
||||||
|
@{ prefix = "WSReferenceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Types that need ChildObjects element
|
# Types that need ChildObjects element — fallback when the source object cannot be probed.
|
||||||
|
# The platform emits <ChildObjects> for every container type even when empty, and rejects
|
||||||
|
# the file without it ("ожидаемое ChildObjects"); primary signal is the source object itself.
|
||||||
$typesWithChildObjects = @(
|
$typesWithChildObjects = @(
|
||||||
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
||||||
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
||||||
"BusinessProcess","Task","Enum",
|
"BusinessProcess","Task","Enum",
|
||||||
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"
|
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister",
|
||||||
|
"DataProcessor","Report","DocumentJournal","FilterCriterion","SettingsStorage",
|
||||||
|
"Sequence","HTTPService","WebService","IntegrationService","Subsystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
# CommonModule properties to copy from source
|
# CommonModule properties to copy from source
|
||||||
@@ -321,9 +405,20 @@ function Expand-SelfClosingElement($container, $parentIndent) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -338,6 +433,20 @@ $script:formatVersion = Detect-FormatVersion $extDir
|
|||||||
# --- 8. Namespaces declaration for object XML ---
|
# --- 8. Namespaces declaration for object XML ---
|
||||||
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "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): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- 9. Parse -Object into items ---
|
# --- 9. Parse -Object into items ---
|
||||||
$items = @()
|
$items = @()
|
||||||
foreach ($part in $Object.Split(";;")) {
|
foreach ($part in $Object.Split(";;")) {
|
||||||
@@ -368,6 +477,37 @@ if ($BorrowMainAttribute) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 10. Helper: read source object XML ---
|
# --- 10. Helper: read source object XML ---
|
||||||
|
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||||
|
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||||
|
function Get-SourceAttributeUuids {
|
||||||
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$dirName = $childTypeDirMap[$typeName]
|
||||||
|
if (-not $dirName) { return $result }
|
||||||
|
$srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml"
|
||||||
|
if (-not (Test-Path $srcFile)) { return $result }
|
||||||
|
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
|
$doc.PreserveWhitespace = $false
|
||||||
|
$doc.Load($srcFile)
|
||||||
|
$objEl = $null
|
||||||
|
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||||
|
}
|
||||||
|
if (-not $objEl) { return $result }
|
||||||
|
$childObjects = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
|
||||||
|
if (-not $childObjects) { return $result }
|
||||||
|
foreach ($child in $childObjects.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||||
|
$uuid = $child.GetAttribute("uuid")
|
||||||
|
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Read-SourceObject {
|
function Read-SourceObject {
|
||||||
param([string]$typeName, [string]$objName)
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
@@ -419,8 +559,19 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if ($typeName -eq "DefinedType") {
|
||||||
|
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
||||||
|
if ($typeNode) {
|
||||||
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
|
$srcProps["__HasChildObjects"] = ($srcEl.SelectSingleNode("md:ChildObjects", $srcNs) -ne $null)
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
Uuid = $srcUuid
|
Uuid = $srcUuid
|
||||||
Properties = $srcProps
|
Properties = $srcProps
|
||||||
@@ -481,8 +632,23 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||||
|
|
||||||
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
# 3. Generate form metadata XML (ФормаЭлемента.xml).
|
||||||
$newFormUuid = [guid]::NewGuid().ToString()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
|
||||||
|
$newFormUuid = ""
|
||||||
|
if (Test-Path $formMetaFileExisting) {
|
||||||
|
try {
|
||||||
|
$existingDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$existingDoc.Load($formMetaFileExisting)
|
||||||
|
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
|
||||||
|
if ($existingFormNode) {
|
||||||
|
$existingUuid = $existingFormNode.GetAttribute("uuid")
|
||||||
|
if ($existingUuid) { $newFormUuid = $existingUuid }
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
|
||||||
$formMetaSb = New-Object System.Text.StringBuilder
|
$formMetaSb = New-Object System.Text.StringBuilder
|
||||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
@@ -516,8 +682,10 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
$formVersion = $srcFormEl.GetAttribute("version")
|
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
||||||
if (-not $formVersion) { $formVersion = $script:formatVersion }
|
# extension stays uniform — otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
||||||
|
$formVersion = $script:formatVersion
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
@@ -543,6 +711,11 @@ function Borrow-Form {
|
|||||||
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
||||||
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
||||||
|
|
||||||
|
# uuid реквизитов объекта — только для формы без заимствованного основного реквизита:
|
||||||
|
# там ссылки параметров выбора переводятся на непрозрачную форму пути
|
||||||
|
$srcAttrUuids = @{}
|
||||||
|
if (-not $BorrowMainAttr) { $srcAttrUuids = Get-SourceAttributeUuids $typeName $objName }
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
||||||
$autoCmdXml = ""
|
$autoCmdXml = ""
|
||||||
if ($srcAutoCmd) {
|
if ($srcAutoCmd) {
|
||||||
@@ -552,13 +725,9 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
# Keep only Объект.* DataPaths
|
if (-not $BorrowMainAttr) { $autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids }
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
} else {
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -568,17 +737,10 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
if (-not $BorrowMainAttr) { $childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids }
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
} else {
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
}
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -759,11 +921,22 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract the <Form ...> opening tag from source text (preserves namespace declarations)
|
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств имён,
|
||||||
|
# но version подставляем СВОЮ: форма обязана нести версию расширения, иначе платформа
|
||||||
|
# отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег копировался целиком,
|
||||||
|
# и версия источника молча побеждала.
|
||||||
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
$formTag = "<Form version=`"${formVersion}`">"
|
$formTag = "<Form version=`"${formVersion}`">"
|
||||||
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
||||||
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] }
|
if ($srcFormContent -match '(<Form[^>]*>)') {
|
||||||
|
$srcTag = $Matches[1]
|
||||||
|
$srcNs = $srcTag -replace '^<Form\s*', '' -replace '\s*/?>$', '' -replace '\s*version="[^"]*"', ''
|
||||||
|
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
|
||||||
|
if ((Get-FormatRank $formVersion) -ge 221 -and $srcNs -notmatch 'xmlns:pal=') {
|
||||||
|
$srcNs = $srcNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
$formTag = if ($srcNs) { "<Form $srcNs version=`"${formVersion}`">" } else { "<Form version=`"${formVersion}`">" }
|
||||||
|
}
|
||||||
|
|
||||||
# Build output Form.xml
|
# Build output Form.xml
|
||||||
$formXmlSb = New-Object System.Text.StringBuilder
|
$formXmlSb = New-Object System.Text.StringBuilder
|
||||||
@@ -785,6 +958,12 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t$childItemsXml") | Out-Null
|
$formXmlSb.Append("`t$childItemsXml") | Out-Null
|
||||||
$formXmlSb.Append("`r`n") | Out-Null
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
}
|
}
|
||||||
|
# Секции основного реквизита исходной формы (<UseAlways>, <Columns>) — их нельзя терять
|
||||||
|
$mainAttrExtra = @()
|
||||||
|
if ($BorrowMainAttr) {
|
||||||
|
$mainAttrExtra = @(Get-MainAttributeExtraXml $srcFormEl $nsStripPattern)
|
||||||
|
}
|
||||||
|
|
||||||
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
||||||
if ($BorrowMainAttr) {
|
if ($BorrowMainAttr) {
|
||||||
$objTypePrefix = ""
|
$objTypePrefix = ""
|
||||||
@@ -796,6 +975,9 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
||||||
|
foreach ($extraXml in $mainAttrExtra) {
|
||||||
|
$formXmlSb.Append("`t`t`t$extraXml`r`n") | Out-Null
|
||||||
|
}
|
||||||
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
@@ -836,6 +1018,15 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
||||||
|
foreach ($extraXml in $mainAttrExtra) {
|
||||||
|
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
|
||||||
|
$exLines = $extraXml -split "`r?`n"
|
||||||
|
for ($li = 0; $li -lt $exLines.Count; $li++) {
|
||||||
|
if ($li -eq 0) { $formXmlSb.Append("`t`t`t`t$($exLines[$li])") | Out-Null }
|
||||||
|
else { $formXmlSb.Append("`t$($exLines[$li])") | Out-Null }
|
||||||
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
@@ -852,17 +1043,30 @@ function Borrow-Form {
|
|||||||
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Здесь источник не XmlWriter, а OuterXml исходного документа — спацовывает так же.
|
||||||
|
$formXmlText = $formXmlSb.ToString()
|
||||||
|
$formXmlText = [regex]::Replace($formXmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Файл создаём мы — канон выгрузки: CRLF в разделителях строк.
|
||||||
|
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
if (Test-Path $moduleBslFile) {
|
||||||
Info " Created: $moduleBslFile"
|
Info " Preserved existing Module.bsl"
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
|
Info " Created: $moduleBslFile"
|
||||||
|
}
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -953,8 +1157,16 @@ function Register-FormInObject {
|
|||||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||||
Info " Registered form in: $objFile"
|
Info " Registered form in: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1001,6 +1213,24 @@ function Build-InternalInfoXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 11b. Collect DataPath references from source Form.xml ---
|
# --- 11b. Collect DataPath references from source Form.xml ---
|
||||||
|
# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData ---
|
||||||
|
# Это <UseAlways> и <Columns> (доп. колонки табличных частей, объявленные прямо в форме). Их
|
||||||
|
# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на
|
||||||
|
# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список OuterXml без xmlns.
|
||||||
|
function Get-MainAttributeExtraXml {
|
||||||
|
param($formEl, [string]$nsStripPattern)
|
||||||
|
|
||||||
|
$result = @()
|
||||||
|
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
|
||||||
|
if (-not $mainAttr) { return $result }
|
||||||
|
foreach ($child in $mainAttr.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -in @('Type','MainAttribute','SavedData')) { continue }
|
||||||
|
$result += [regex]::Replace($child.OuterXml, $nsStripPattern, '')
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Collect-FormDataPaths {
|
function Collect-FormDataPaths {
|
||||||
param([string]$formXmlPath)
|
param([string]$formXmlPath)
|
||||||
|
|
||||||
@@ -1010,8 +1240,29 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
foreach ($m in $matches2) {
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
|
foreach ($m in $bms) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
if ($segments.Count -ge 2) {
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardFields -contains $seg1) { continue }
|
||||||
|
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
||||||
|
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
||||||
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1024,12 +1275,12 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
|
||||||
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
|
||||||
foreach ($m in $matches3) {
|
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
|
||||||
$path = $m.Groups[1].Value
|
$acMatches = [regex]::Matches($content, '<AdditionalColumns table="Объект\.(\w+)"')
|
||||||
$segments = $path.Split(".")
|
foreach ($m in $acMatches) {
|
||||||
$seg0 = $segments[0]
|
$seg0 = $m.Groups[1].Value
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
$firstLevel[$seg0] = $true
|
$firstLevel[$seg0] = $true
|
||||||
}
|
}
|
||||||
@@ -1038,7 +1289,7 @@ function Collect-FormDataPaths {
|
|||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1142,11 +1393,20 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
$extraProps = @{}
|
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
||||||
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
# NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
|
||||||
"NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity")
|
# тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
|
||||||
|
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
|
||||||
|
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
|
||||||
|
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
|
||||||
|
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
|
||||||
|
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
|
||||||
|
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
|
||||||
|
$propsToExtract = @("Hierarchical","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
|
"NumberType","NumberLength","NumberAllowedLength")
|
||||||
foreach ($pName in $propsToExtract) {
|
foreach ($pName in $propsToExtract) {
|
||||||
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
||||||
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
||||||
@@ -1331,7 +1591,17 @@ function Merge-AttributesIntoObject {
|
|||||||
# Insert attributes before </ChildObjects>
|
# Insert attributes before </ChildObjects>
|
||||||
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
|
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
|
||||||
|
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
||||||
|
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
||||||
Info " Merged $added attribute(s) into: $objFile"
|
Info " Merged $added attribute(s) into: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1375,28 +1645,45 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
$existingChildNames = @{}
|
||||||
|
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
||||||
|
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
||||||
|
$existingChildNames[$nm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $srcAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $insertTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Read existing object XML and inject
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
|
$objPropsBlock = ""
|
||||||
|
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
|
if ($objPropsBlock -match "<$pName>") { continue }
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
if ($propsSb.Length -gt 0) {
|
||||||
|
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1422,6 +1709,20 @@ function Borrow-MainAttribute {
|
|||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $srcTS) {
|
||||||
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
||||||
}
|
}
|
||||||
|
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
|
||||||
|
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
|
||||||
|
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
|
||||||
|
# формы заказа поставщику).
|
||||||
|
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||||
|
if (Test-Path $srcFormForCols) {
|
||||||
|
$colsDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$colsDoc.PreserveWhitespace = $true
|
||||||
|
$colsDoc.Load($srcFormForCols)
|
||||||
|
foreach ($extraXml in (Get-MainAttributeExtraXml $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"')) {
|
||||||
|
if ($extraXml -like "<Columns*") { $allTypeXmls += $extraXml }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
||||||
Info " Reference types to borrow: $($refTypes.Count)"
|
Info " Reference types to borrow: $($refTypes.Count)"
|
||||||
|
|
||||||
@@ -1454,79 +1755,46 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
$realDeep = @()
|
$deepByAttr = @{}
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
|
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
||||||
}
|
}
|
||||||
|
if ($deepByAttr.Count -gt 0) {
|
||||||
if ($realDeep.Count -gt 0) {
|
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
||||||
Info " Processing $($realDeep.Count) deep path(s)..."
|
|
||||||
|
|
||||||
# Group by ObjectAttr → target catalog
|
|
||||||
$deepByAttr = @{}
|
|
||||||
foreach ($dp in $realDeep) {
|
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
|
||||||
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
# Find the attribute's type to determine target catalog
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
if (-not $attrInfo) { continue }
|
if (-not $attrInfo) { continue }
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$targetTypeName = $catMatch.Groups[1].Value
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
$targetObjName = $catMatch.Groups[2].Value
|
$tsDeepByCol = @{}
|
||||||
|
foreach ($dp in $deepPaths) {
|
||||||
# Ensure target is borrowed
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
if (-not $dp.SubSubAttr) { continue }
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
||||||
if (-not (Test-Path $tTargetDir)) {
|
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
||||||
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
}
|
||||||
}
|
if ($tsDeepByCol.Count -gt 0) {
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
foreach ($k in $tsDeepByCol.Keys) {
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
$parts = $k.Split("|")
|
||||||
$script:borrowedFiles += $tTargetFile
|
$tsName = $parts[0]; $colName = $parts[1]
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
||||||
}
|
if (-not $tsInfo) { continue }
|
||||||
|
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
||||||
# Resolve sub-attributes in target catalog
|
if (-not $colInfo) { continue }
|
||||||
$subNames = @{}
|
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
if (-not $catMatch.Success) { continue }
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
||||||
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
|
||||||
$subTypeXmls = @()
|
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
|
||||||
foreach ($srt in $subRefTypes) {
|
|
||||||
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
|
||||||
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
|
||||||
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
|
||||||
if (-not (Test-Path $sSrcFile)) { continue }
|
|
||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
|
||||||
if (-not (Test-Path $sTargetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
|
||||||
$script:borrowedFiles += $sTargetFile
|
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1534,6 +1802,57 @@ function Borrow-MainAttribute {
|
|||||||
Info " Main attribute borrowing complete"
|
Info " Main attribute borrowing complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
||||||
|
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
||||||
|
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
||||||
|
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
||||||
|
function Borrow-DeepTargetAttrs {
|
||||||
|
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
||||||
|
|
||||||
|
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# Ensure target is borrowed (shell)
|
||||||
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
|
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
||||||
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
|
$script:borrowedFiles += $tTargetFile
|
||||||
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve sub-attributes in target catalog and merge them in
|
||||||
|
$subNames = @{}
|
||||||
|
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
||||||
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
|
# Borrow ref types referenced by the sub-attributes
|
||||||
|
$subTypeXmls = @()
|
||||||
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
|
foreach ($srt in $subRefTypes) {
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
||||||
|
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
||||||
|
if (-not (Test-Path $sSrcFile)) { continue }
|
||||||
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
|
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
||||||
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
|
$script:borrowedFiles += $sTargetFile
|
||||||
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
param(
|
param(
|
||||||
@@ -1572,10 +1891,15 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
if ($typesWithChildObjects -contains $typeName) {
|
if ($sourceProps["__HasChildObjects"] -or ($typesWithChildObjects -contains $typeName)) {
|
||||||
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,8 +2079,16 @@ $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 } })
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
||||||
Info "Saved: $extResolvedPath"
|
Info "Saved: $extResolvedPath"
|
||||||
|
|
||||||
+528
-136
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
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cfe-validate
|
name: cfe-validate
|
||||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -17,13 +17,24 @@ allowed-tools:
|
|||||||
| Параметр | Обяз. | Умолч. | Описание |
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|---------------|:-----:|---------|-------------------------------------------------|
|
|---------------|:-----:|---------|-------------------------------------------------|
|
||||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||||
|
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
|
||||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
| OutFile | нет | — | Записать результат в файл |
|
||||||
|
|
||||||
|
### ConfigPath
|
||||||
|
|
||||||
|
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
|
||||||
|
|
||||||
|
Если пользователь не указал путь — определи сам:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default`)
|
||||||
|
3. Возьми её поле `configSrc`
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
python ".opencode/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
python ".opencode/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
||||||
|
python ".opencode/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
+235
-6
@@ -1,4 +1,4 @@
|
|||||||
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.9 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -9,7 +9,11 @@ param(
|
|||||||
|
|
||||||
[int]$MaxErrors = 30,
|
[int]$MaxErrors = 30,
|
||||||
|
|
||||||
[string]$OutFile
|
[string]$OutFile,
|
||||||
|
|
||||||
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
[string]$ConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -89,6 +93,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_]*$'
|
||||||
@@ -108,7 +125,7 @@ $validClassIds = @(
|
|||||||
$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",
|
||||||
@@ -122,7 +139,7 @@ $childObjectTypes = @(
|
|||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
$childTypeDirMap = @{
|
$childTypeDirMap = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
@@ -144,6 +161,46 @@ $childTypeDirMap = @{
|
|||||||
"IntegrationService"="IntegrationServices"
|
"IntegrationService"="IntegrationServices"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
$generatedTypeCategories = @{
|
||||||
|
"Catalog" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Document" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Enum" = @("Ref","Manager","List")
|
||||||
|
"Constant" = @("Manager","ValueManager","ValueKey")
|
||||||
|
"Report" = @("Object","Manager")
|
||||||
|
"DataProcessor" = @("Object","Manager")
|
||||||
|
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Task" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
|
||||||
|
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
|
||||||
|
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
|
||||||
|
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
|
||||||
|
"InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager")
|
||||||
|
"AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey")
|
||||||
|
"AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions")
|
||||||
|
"CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs")
|
||||||
|
"DocumentJournal" = @("Selection","List","Manager")
|
||||||
|
"Sequence" = @("Record","Manager","RecordSet")
|
||||||
|
"FilterCriterion" = @("Manager","List")
|
||||||
|
"SettingsStorage" = @("Manager")
|
||||||
|
"IntegrationService" = @("Manager")
|
||||||
|
"WSReference" = @("Manager")
|
||||||
|
"DefinedType" = @("DefinedType")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
$script:standardObjectFields = @(
|
||||||
|
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
|
||||||
|
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
|
||||||
|
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
|
||||||
|
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
|
||||||
|
"Движения","ВерсияДанных","КоличествоСтрок"
|
||||||
|
)
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
$validEnumValues = @{
|
$validEnumValues = @{
|
||||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
||||||
@@ -195,10 +252,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
|
||||||
@@ -536,6 +598,7 @@ if ($script:stopped) { & $finalize; exit 1 }
|
|||||||
|
|
||||||
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
||||||
$script:enumValuesIndex = @{}
|
$script:enumValuesIndex = @{}
|
||||||
|
$script:borrowedTSIndex = @{}
|
||||||
$script:formList = @()
|
$script:formList = @()
|
||||||
|
|
||||||
# Helper: check if sub-item has explicit borrowed metadata
|
# Helper: check if sub-item has explicit borrowed metadata
|
||||||
@@ -639,6 +702,25 @@ if ($childObjNode) {
|
|||||||
} else {
|
} else {
|
||||||
$borrowedOk++
|
$borrowedOk++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
$expectedCats = $generatedTypeCategories[$typeName]
|
||||||
|
if ($expectedCats) {
|
||||||
|
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
|
$foundCats = @{}
|
||||||
|
if ($objInfo) {
|
||||||
|
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
|
||||||
|
$cat = $gt.GetAttribute("category")
|
||||||
|
if ($cat) { $foundCats[$cat] = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
|
||||||
|
if ($missingCats.Count -gt 0) {
|
||||||
|
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
|
||||||
|
$check9Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
@@ -666,6 +748,12 @@ if ($childObjNode) {
|
|||||||
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
||||||
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ($tsName) {
|
||||||
|
$tsKey = "${typeName}.${childName}"
|
||||||
|
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
|
||||||
|
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
|
||||||
|
}
|
||||||
if (-not $tsInfo) {
|
if (-not $tsInfo) {
|
||||||
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
||||||
$check10Ok = $false
|
$check10Ok = $false
|
||||||
@@ -895,6 +983,28 @@ foreach ($bf in $script:borrowedFormsWithTree) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
$acTables = @{}
|
||||||
|
foreach ($m in [regex]::Matches($raw, '<AdditionalColumns table="Объект\.(\w+)"')) {
|
||||||
|
$acTables[$m.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
if ($acTables.Count -gt 0) {
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerTS = $script:borrowedTSIndex[$ownerKey]
|
||||||
|
foreach ($tblName in $acTables.Keys) {
|
||||||
|
$depCheckCount++
|
||||||
|
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
|
||||||
|
Report-Error "12. ${ctx}: <AdditionalColumns table=`"Объект.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
|
||||||
|
$check12Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($mi in $missingItems) {
|
foreach ($mi in $missingItems) {
|
||||||
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
||||||
$check12Ok = $false
|
$check12Ok = $false
|
||||||
@@ -930,6 +1040,125 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
|
|||||||
Report-OK "13. TypeLink: clean"
|
Report-OK "13. TypeLink: clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
|
||||||
|
if (-not $ConfigPath) {
|
||||||
|
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
|
||||||
|
} else {
|
||||||
|
$cfgRoot = $ConfigPath
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
|
||||||
|
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
|
||||||
|
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
|
||||||
|
} else {
|
||||||
|
$check14Ok = $true
|
||||||
|
$pathCheckCount = 0
|
||||||
|
|
||||||
|
foreach ($bf in $script:borrowedFormsWithTree) {
|
||||||
|
$raw = $bf.RawText
|
||||||
|
$ctx = $bf.Context
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerParts = $ownerKey -split '\.', 2
|
||||||
|
if ($ownerParts.Count -lt 2) { continue }
|
||||||
|
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
|
||||||
|
$ownerDir = $childTypeDirMap[$ownerType]
|
||||||
|
if (-not $ownerDir) { continue }
|
||||||
|
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
|
||||||
|
if (-not (Test-Path $srcObjFile)) {
|
||||||
|
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
$srcNames = @{}
|
||||||
|
$srcTSColumns = @{}
|
||||||
|
$srcDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$srcDoc.PreserveWhitespace = $false
|
||||||
|
$srcDoc.Load($srcObjFile)
|
||||||
|
$srcObjEl = $null
|
||||||
|
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
|
||||||
|
}
|
||||||
|
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
|
||||||
|
if ($srcChildObjects) {
|
||||||
|
foreach ($sub in $srcChildObjects.ChildNodes) {
|
||||||
|
if ($sub.NodeType -ne 'Element') { continue }
|
||||||
|
if ($sub.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||||
|
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if (-not $nameNode) { continue }
|
||||||
|
$subName = $nameNode.InnerText.Trim()
|
||||||
|
$srcNames[$subName] = $true
|
||||||
|
if ($sub.LocalName -ne 'TabularSection') { continue }
|
||||||
|
$cols = @{}
|
||||||
|
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
|
||||||
|
$cols[$colName.InnerText.Trim()] = $true
|
||||||
|
}
|
||||||
|
$srcTSColumns[$subName] = $cols
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
foreach ($acm in [regex]::Matches($raw, '(?s)<AdditionalColumns table="Объект\.(\w+)">(.*?)</AdditionalColumns>')) {
|
||||||
|
$tbl = $acm.Groups[1].Value
|
||||||
|
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
|
||||||
|
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
|
||||||
|
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$badPaths = @{}
|
||||||
|
foreach ($m in [regex]::Matches($raw, '<(?:\w+:)?\w*DataPath[^>]*>Объект\.([^<]+)</(?:\w+:)?\w*DataPath>')) {
|
||||||
|
$segments = $m.Groups[1].Value -split '\.'
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
$pathCheckCount++
|
||||||
|
if ($script:standardObjectFields -contains $seg0) { continue }
|
||||||
|
if (-not $srcNames.ContainsKey($seg0)) {
|
||||||
|
$badPaths["Объект.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardObjectFields -contains $seg1) { continue }
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
|
||||||
|
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
|
||||||
|
$badPaths["Объект.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
|
||||||
|
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
|
||||||
|
$check14Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($check14Ok) {
|
||||||
|
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:stopped) { & $finalize; exit 1 }
|
||||||
|
|
||||||
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
|
$extRootDir = Split-Path $resolvedPath -Parent
|
||||||
|
$ctrlCount = 0
|
||||||
|
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
|
||||||
|
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
|
||||||
|
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
|
||||||
|
}
|
||||||
|
if ($ctrlCount -gt 0) {
|
||||||
|
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
& $finalize
|
& $finalize
|
||||||
|
|
||||||
+254
-5
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.9 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
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',
|
||||||
@@ -37,7 +59,7 @@ VALID_CLASS_IDS = [
|
|||||||
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',
|
||||||
@@ -73,6 +96,46 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'IntegrationService': 'IntegrationServices',
|
'IntegrationService': 'IntegrationServices',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
GENERATED_TYPE_CATEGORIES = {
|
||||||
|
'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Enum': ['Ref', 'Manager', 'List'],
|
||||||
|
'Constant': ['Manager', 'ValueManager', 'ValueKey'],
|
||||||
|
'Report': ['Object', 'Manager'],
|
||||||
|
'DataProcessor': ['Object', 'Manager'],
|
||||||
|
'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'],
|
||||||
|
'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'],
|
||||||
|
'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'],
|
||||||
|
'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'],
|
||||||
|
'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'],
|
||||||
|
'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'],
|
||||||
|
'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'],
|
||||||
|
'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'],
|
||||||
|
'DocumentJournal': ['Selection', 'List', 'Manager'],
|
||||||
|
'Sequence': ['Record', 'Manager', 'RecordSet'],
|
||||||
|
'FilterCriterion': ['Manager', 'List'],
|
||||||
|
'SettingsStorage': ['Manager'],
|
||||||
|
'IntegrationService': ['Manager'],
|
||||||
|
'WSReference': ['Manager'],
|
||||||
|
'DefinedType': ['DefinedType'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
STANDARD_OBJECT_FIELDS = {
|
||||||
|
'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber',
|
||||||
|
'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount',
|
||||||
|
'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный',
|
||||||
|
'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных',
|
||||||
|
'Движения', 'ВерсияДанных', 'КоличествоСтрок',
|
||||||
|
}
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
VALID_ENUM_VALUES = {
|
VALID_ENUM_VALUES = {
|
||||||
'ConfigurationExtensionCompatibilityMode': [
|
'ConfigurationExtensionCompatibilityMode': [
|
||||||
@@ -94,6 +157,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):
|
||||||
@@ -154,11 +231,15 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default='')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
out_file = args.OutFile
|
out_file = args.OutFile
|
||||||
|
config_path_arg = args.ConfigPath
|
||||||
|
|
||||||
# --- Resolve path ---
|
# --- Resolve path ---
|
||||||
if not os.path.isabs(extension_path):
|
if not os.path.isabs(extension_path):
|
||||||
@@ -214,10 +295,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
|
||||||
@@ -536,6 +624,7 @@ def main():
|
|||||||
MD = NS['md']
|
MD = NS['md']
|
||||||
XR = NS['xr']
|
XR = NS['xr']
|
||||||
enum_values_index = {}
|
enum_values_index = {}
|
||||||
|
borrowed_ts_index = {}
|
||||||
form_list = []
|
form_list = []
|
||||||
|
|
||||||
def is_borrowed_sub_item(sub_item):
|
def is_borrowed_sub_item(sub_item):
|
||||||
@@ -635,6 +724,23 @@ def main():
|
|||||||
else:
|
else:
|
||||||
borrowed_ok_count += 1
|
borrowed_ok_count += 1
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name)
|
||||||
|
if expected_cats:
|
||||||
|
obj_info = obj_el.find(f'{{{MD}}}InternalInfo')
|
||||||
|
found_cats = set()
|
||||||
|
if obj_info is not None:
|
||||||
|
for gt in obj_info.findall(f'{{{XR}}}GeneratedType'):
|
||||||
|
cat = gt.get('category')
|
||||||
|
if cat:
|
||||||
|
found_cats.add(cat)
|
||||||
|
missing_cats = [c for c in expected_cats if c not in found_cats]
|
||||||
|
if missing_cats:
|
||||||
|
word = 'category' if len(missing_cats) == 1 else 'categories'
|
||||||
|
r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}")
|
||||||
|
check9_ok = False
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
||||||
if obj_child_objects is not None:
|
if obj_child_objects is not None:
|
||||||
@@ -662,6 +768,9 @@ def main():
|
|||||||
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
||||||
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ts_name_el is not None and ts_name_el.text:
|
||||||
|
borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True
|
||||||
if ts_info is None:
|
if ts_info is None:
|
||||||
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
||||||
check10_ok = False
|
check10_ok = False
|
||||||
@@ -854,6 +963,22 @@ def main():
|
|||||||
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
||||||
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
ac_tables = set(re.findall(r'<AdditionalColumns table="Объект\.(\w+)"', raw))
|
||||||
|
if ac_tables:
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_ts = borrowed_ts_index.get(owner_key, {})
|
||||||
|
for tbl_name in sorted(ac_tables):
|
||||||
|
dep_check_count += 1
|
||||||
|
if tbl_name not in owner_ts:
|
||||||
|
r.error(f'12. {ctx}: <AdditionalColumns table="Объект.{tbl_name}"> — TabularSection.{tbl_name} not borrowed in extension')
|
||||||
|
check12_ok = False
|
||||||
|
|
||||||
for mi in missing_items:
|
for mi in missing_items:
|
||||||
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
||||||
check12_ok = False
|
check12_ok = False
|
||||||
@@ -885,6 +1010,130 @@ def main():
|
|||||||
elif check13_ok:
|
elif check13_ok:
|
||||||
r.ok('13. TypeLink: clean')
|
r.ok('13. TypeLink: clean')
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if not r.stopped and borrowed_forms_with_tree:
|
||||||
|
if not config_path_arg:
|
||||||
|
r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath')
|
||||||
|
else:
|
||||||
|
cfg_root = config_path_arg
|
||||||
|
if not os.path.isabs(cfg_root):
|
||||||
|
cfg_root = os.path.join(os.getcwd(), cfg_root)
|
||||||
|
if os.path.exists(cfg_root) and not os.path.isdir(cfg_root):
|
||||||
|
cfg_root = os.path.dirname(cfg_root)
|
||||||
|
|
||||||
|
if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')):
|
||||||
|
r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена")
|
||||||
|
else:
|
||||||
|
check14_ok = True
|
||||||
|
path_check_count = 0
|
||||||
|
|
||||||
|
for bf in borrowed_forms_with_tree:
|
||||||
|
raw = bf['RawText']
|
||||||
|
ctx = bf['Context']
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_parts = owner_key.split('.', 1)
|
||||||
|
if len(owner_parts) < 2:
|
||||||
|
continue
|
||||||
|
owner_type, owner_name = owner_parts
|
||||||
|
owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type)
|
||||||
|
if not owner_dir:
|
||||||
|
continue
|
||||||
|
src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml')
|
||||||
|
if not os.path.isfile(src_obj_file):
|
||||||
|
r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
src_names = set()
|
||||||
|
src_ts_columns = {}
|
||||||
|
src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True))
|
||||||
|
src_obj_el = None
|
||||||
|
for c in src_tree.getroot():
|
||||||
|
if isinstance(c.tag, str):
|
||||||
|
src_obj_el = c
|
||||||
|
break
|
||||||
|
src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None
|
||||||
|
if src_child_objects is not None:
|
||||||
|
for sub in src_child_objects:
|
||||||
|
if not isinstance(sub.tag, str):
|
||||||
|
continue
|
||||||
|
sub_ln = etree.QName(sub.tag).localname
|
||||||
|
if sub_ln not in ('Attribute', 'TabularSection'):
|
||||||
|
continue
|
||||||
|
name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
|
if name_el is None or not name_el.text:
|
||||||
|
continue
|
||||||
|
sub_name = name_el.text.strip()
|
||||||
|
src_names.add(sub_name)
|
||||||
|
if sub_ln != 'TabularSection':
|
||||||
|
continue
|
||||||
|
cols = set()
|
||||||
|
for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'):
|
||||||
|
if col_name.text:
|
||||||
|
cols.add(col_name.text.strip())
|
||||||
|
src_ts_columns[sub_name] = cols
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
for acm in re.finditer(r'<AdditionalColumns table="Объект\.(\w+)">(.*?)</AdditionalColumns>', raw, re.DOTALL):
|
||||||
|
tbl = acm.group(1)
|
||||||
|
cols = src_ts_columns.setdefault(tbl, set())
|
||||||
|
for cm in re.finditer(r'<Column name="(\w+)"', acm.group(2)):
|
||||||
|
cols.add(cm.group(1))
|
||||||
|
|
||||||
|
bad_paths = {}
|
||||||
|
for m in re.finditer(r'<(?:\w+:)?\w*DataPath[^>]*>Объект\.([^<]+)</(?:\w+:)?\w*DataPath>', raw):
|
||||||
|
segments = m.group(1).split('.')
|
||||||
|
seg0 = segments[0]
|
||||||
|
path_check_count += 1
|
||||||
|
if seg0 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
if seg0 not in src_names:
|
||||||
|
bad_paths[f'Объект.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части'
|
||||||
|
continue
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if len(segments) < 2 or seg0 not in src_ts_columns:
|
||||||
|
continue
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]:
|
||||||
|
continue
|
||||||
|
if seg1 not in src_ts_columns[seg0]:
|
||||||
|
bad_paths[f'Объект.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и <Columns> формы её не объявляет'
|
||||||
|
|
||||||
|
for bad in sorted(bad_paths):
|
||||||
|
r.error(f"14. {ctx}: путь '{bad}' — {bad_paths[bad]}")
|
||||||
|
check14_ok = False
|
||||||
|
|
||||||
|
if check14_ok:
|
||||||
|
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
||||||
|
|
||||||
|
if r.stopped:
|
||||||
|
r.finalize(out_file)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
|
ctrl_count = 0
|
||||||
|
for dp, _dn, files in os.walk(config_dir):
|
||||||
|
for fn in files:
|
||||||
|
if fn.endswith('.bsl'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
|
||||||
|
for ln in f:
|
||||||
|
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
|
||||||
|
ctrl_count += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if ctrl_count > 0:
|
||||||
|
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
@@ -25,26 +25,28 @@ allowed-tools:
|
|||||||
## Параметры подключения
|
## Параметры подключения
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
|
python ".opencode/skills/db-create/scripts/db-create.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
||||||
| `-AddToList` | нет | Добавить в список баз 1С |
|
| `-AddToList` | нет | Добавить в список баз 1С |
|
||||||
| `-ListName <имя>` | нет | Имя базы в списке |
|
| `-ListName <имя>` | нет | Имя базы в списке |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
@@ -57,14 +59,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user