mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-24 13:41:01 +03:00
Compare commits
219
Commits
w-2026-06-21
..
main
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.10 — 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"
|
||||||
@@ -196,7 +209,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 +225,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"
|
||||||
@@ -700,6 +713,7 @@ $script:ruTypeMap = @{
|
|||||||
"регистррасчёта" = "CalculationRegister"
|
"регистррасчёта" = "CalculationRegister"
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
"задача" = "Task"
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.10 — 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
|
||||||
@@ -33,6 +33,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 +84,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 +94,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 +199,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 +215,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 +324,49 @@ def parse_batch_value(val):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
if not xml_bytes.endswith(b"\n"):
|
try:
|
||||||
|
raw = open(path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
body = raw[3:] if bom else raw
|
||||||
|
crlf = b"\r\n" in body
|
||||||
|
m = re.search(rb'encoding="([^"]+)"', body[:200])
|
||||||
|
enc = m.group(1).decode("ascii") if m else "utf-8"
|
||||||
|
final_nl = body.endswith(b"\n")
|
||||||
|
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
|
||||||
|
|
||||||
|
|
||||||
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
|
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -771,6 +824,7 @@ def main():
|
|||||||
"регистррасчета": "CalculationRegister",
|
"регистррасчета": "CalculationRegister",
|
||||||
"регистррасчёта": "CalculationRegister",
|
"регистррасчёта": "CalculationRegister",
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
"хранилищенастроек": "SettingsStorage",
|
"хранилищенастроек": "SettingsStorage",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][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"="Регламентные задания"
|
||||||
|
|||||||
@@ -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.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -111,6 +111,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.3 — Validate 1C configuration root structure
|
# cf-validate v1.4 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -104,11 +104,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
|
|||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
|
"Bot"="Bots"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.3 — Validate 1C configuration XML structure
|
# cf-validate v1.4 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
||||||
|
function Strip-FormBindings {
|
||||||
|
param([string]$xml, [bool]$keepObjekt)
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
if ($keepObjekt) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
||||||
|
} else {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tag in $script:formBindingPictureTags) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -419,6 +444,14 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if ($typeName -eq "DefinedType") {
|
||||||
|
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
||||||
|
if ($typeNode) {
|
||||||
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
@@ -481,8 +514,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 +564,10 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
$formVersion = $srcFormEl.GetAttribute("version")
|
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
||||||
if (-not $formVersion) { $formVersion = $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
|
||||||
@@ -552,13 +602,8 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
} else {
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -568,17 +613,9 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
} else {
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
}
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -855,14 +892,19 @@ function Borrow-Form {
|
|||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
if (Test-Path $moduleBslFile) {
|
||||||
Info " Created: $moduleBslFile"
|
Info " Preserved existing Module.bsl"
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
|
Info " Created: $moduleBslFile"
|
||||||
|
}
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -1010,8 +1052,29 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
foreach ($m in $matches2) {
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
|
foreach ($m in $bms) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
if ($segments.Count -ge 2) {
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardFields -contains $seg1) { continue }
|
||||||
|
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
||||||
|
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
||||||
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1024,21 +1087,11 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
|
||||||
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
|
||||||
foreach ($m in $matches3) {
|
|
||||||
$path = $m.Groups[1].Value
|
|
||||||
$segments = $path.Split(".")
|
|
||||||
$seg0 = $segments[0]
|
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
|
||||||
$firstLevel[$seg0] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1142,7 +1195,8 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
$extraProps = @{}
|
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
||||||
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
@@ -1375,28 +1429,45 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
$existingChildNames = @{}
|
||||||
|
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
||||||
|
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
||||||
|
$existingChildNames[$nm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $srcAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $insertTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Read existing object XML and inject
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
|
$objPropsBlock = ""
|
||||||
|
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
|
if ($objPropsBlock -match "<$pName>") { continue }
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
if ($propsSb.Length -gt 0) {
|
||||||
|
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1454,79 +1525,46 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
$realDeep = @()
|
$deepByAttr = @{}
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
|
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
||||||
}
|
}
|
||||||
|
if ($deepByAttr.Count -gt 0) {
|
||||||
if ($realDeep.Count -gt 0) {
|
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
||||||
Info " Processing $($realDeep.Count) deep path(s)..."
|
|
||||||
|
|
||||||
# Group by ObjectAttr → target catalog
|
|
||||||
$deepByAttr = @{}
|
|
||||||
foreach ($dp in $realDeep) {
|
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
|
||||||
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
# Find the attribute's type to determine target catalog
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
if (-not $attrInfo) { continue }
|
if (-not $attrInfo) { continue }
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$targetTypeName = $catMatch.Groups[1].Value
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
$targetObjName = $catMatch.Groups[2].Value
|
$tsDeepByCol = @{}
|
||||||
|
foreach ($dp in $deepPaths) {
|
||||||
# Ensure target is borrowed
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
if (-not $dp.SubSubAttr) { continue }
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
||||||
if (-not (Test-Path $tTargetDir)) {
|
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
||||||
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
}
|
||||||
}
|
if ($tsDeepByCol.Count -gt 0) {
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
foreach ($k in $tsDeepByCol.Keys) {
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
$parts = $k.Split("|")
|
||||||
$script:borrowedFiles += $tTargetFile
|
$tsName = $parts[0]; $colName = $parts[1]
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
||||||
}
|
if (-not $tsInfo) { continue }
|
||||||
|
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
||||||
# Resolve sub-attributes in target catalog
|
if (-not $colInfo) { continue }
|
||||||
$subNames = @{}
|
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
if (-not $catMatch.Success) { continue }
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
||||||
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
|
||||||
$subTypeXmls = @()
|
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
|
||||||
foreach ($srt in $subRefTypes) {
|
|
||||||
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
|
||||||
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
|
||||||
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
|
||||||
if (-not (Test-Path $sSrcFile)) { continue }
|
|
||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
|
||||||
if (-not (Test-Path $sTargetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
|
||||||
$script:borrowedFiles += $sTargetFile
|
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1534,6 +1572,57 @@ function Borrow-MainAttribute {
|
|||||||
Info " Main attribute borrowing complete"
|
Info " Main attribute borrowing complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
||||||
|
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
||||||
|
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
||||||
|
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
||||||
|
function Borrow-DeepTargetAttrs {
|
||||||
|
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
||||||
|
|
||||||
|
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# Ensure target is borrowed (shell)
|
||||||
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
|
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
||||||
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
|
$script:borrowedFiles += $tTargetFile
|
||||||
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve sub-attributes in target catalog and merge them in
|
||||||
|
$subNames = @{}
|
||||||
|
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
||||||
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
|
# Borrow ref types referenced by the sub-attributes
|
||||||
|
$subTypeXmls = @()
|
||||||
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
|
foreach ($srt in $subRefTypes) {
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
||||||
|
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
||||||
|
if (-not (Test-Path $sSrcFile)) { continue }
|
||||||
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
|
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
||||||
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
|
$script:borrowedFiles += $sTargetFile
|
||||||
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
param(
|
param(
|
||||||
@@ -1572,6 +1661,11 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||||
|
|
||||||
|
|
||||||
|
def strip_form_bindings(xml, keep_objekt):
|
||||||
|
"""Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
if keep_objekt:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
||||||
|
else:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
for tag in FORM_BINDING_PICTURE_TAGS:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def decode_numeric_entities(s):
|
||||||
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
|
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||||
|
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
||||||
|
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
||||||
|
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
@@ -319,13 +349,49 @@ def expand_self_closing(container, parent_indent):
|
|||||||
container.text = "\r\n" + parent_indent
|
container.text = "\r\n" + parent_indent
|
||||||
|
|
||||||
|
|
||||||
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -462,6 +528,13 @@ def main():
|
|||||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||||
if prop_node is not None:
|
if prop_node is not None:
|
||||||
src_props[prop_name] = (prop_node.text or "").strip()
|
src_props[prop_name] = (prop_node.text or "").strip()
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if type_name == "DefinedType":
|
||||||
|
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
||||||
|
if type_node is not None:
|
||||||
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
@@ -533,6 +606,10 @@ def main():
|
|||||||
prop_val = source_props.get(prop_name, "false")
|
prop_val = source_props.get(prop_name, "false")
|
||||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
||||||
|
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
@@ -644,7 +721,26 @@ def main():
|
|||||||
first_level = {}
|
first_level = {}
|
||||||
deep_paths = []
|
deep_paths = []
|
||||||
|
|
||||||
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
|
||||||
|
path = m.group(1)
|
||||||
|
segments = path.split(".")
|
||||||
|
seg0 = segments[0]
|
||||||
|
if seg0 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
first_level[seg0] = True
|
||||||
|
if len(segments) >= 2:
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
@@ -655,22 +751,14 @@ def main():
|
|||||||
seg1 = segments[1]
|
seg1 = segments[1]
|
||||||
if seg1 in STANDARD_FIELDS:
|
if seg1 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
# Also collect from TitleDataPath
|
|
||||||
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
|
||||||
path = m.group(1)
|
|
||||||
segments = path.split(".")
|
|
||||||
seg0 = segments[0]
|
|
||||||
if seg0 in STANDARD_FIELDS:
|
|
||||||
continue
|
|
||||||
first_level[seg0] = True
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
for dp in deep_paths:
|
for dp in deep_paths:
|
||||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_deep.append(dp)
|
unique_deep.append(dp)
|
||||||
@@ -941,26 +1029,40 @@ def main():
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
content_parts = []
|
|
||||||
for attr in src_attrs:
|
|
||||||
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
||||||
content_parts.append(attr_xml)
|
|
||||||
for ts in src_ts:
|
|
||||||
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
|
||||||
content_parts.append(ts_xml)
|
|
||||||
adopted_content = "\n".join(content_parts).rstrip()
|
|
||||||
|
|
||||||
# Read existing object XML and inject
|
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
existing_child_names = set()
|
||||||
|
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
||||||
|
if m_co:
|
||||||
|
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
|
||||||
|
existing_child_names.add(nm)
|
||||||
|
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
||||||
|
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
||||||
|
|
||||||
|
# Generate full object XML with attributes and TS
|
||||||
|
content_parts = []
|
||||||
|
for attr in insert_attrs:
|
||||||
|
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
||||||
|
for ts in insert_ts:
|
||||||
|
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
|
||||||
|
adopted_content = "\n".join(content_parts).rstrip()
|
||||||
|
|
||||||
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
if extra_props:
|
if extra_props:
|
||||||
|
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||||
|
obj_props_block = m_props.group(1) if m_props else ""
|
||||||
props_xml = ""
|
props_xml = ""
|
||||||
for p_name, p_val in extra_props.items():
|
for p_name, p_val in extra_props.items():
|
||||||
|
if f"<{p_name}>" in obj_props_block:
|
||||||
|
continue
|
||||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
if props_xml:
|
||||||
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
@@ -1012,79 +1114,93 @@ def main():
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if mode == "Form" and deep_paths:
|
if mode == "Form" and deep_paths:
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
deep_by_attr = {}
|
||||||
|
for dp in deep_paths:
|
||||||
if real_deep:
|
if dp["ObjectAttr"] in ts_names:
|
||||||
info(f" Processing {len(real_deep)} deep path(s)...")
|
continue
|
||||||
|
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||||
# Group by ObjectAttr -> target catalog
|
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||||
deep_by_attr = {}
|
|
||||||
for dp in real_deep:
|
|
||||||
if dp["ObjectAttr"] not in deep_by_attr:
|
|
||||||
deep_by_attr[dp["ObjectAttr"]] = []
|
|
||||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||||
|
if deep_by_attr:
|
||||||
|
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
||||||
for attr_name, sub_attr_names in deep_by_attr.items():
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
||||||
# Find the attribute's type to determine target catalog
|
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||||
attr_info = None
|
|
||||||
for a in src_attrs:
|
|
||||||
if a["Name"] == attr_name:
|
|
||||||
attr_info = a
|
|
||||||
break
|
|
||||||
if not attr_info:
|
if not attr_info:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||||
if not cat_match:
|
if not cat_match:
|
||||||
continue
|
continue
|
||||||
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
|
|
||||||
target_type_name = cat_match.group(1)
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
target_obj_name = cat_match.group(2)
|
ts_deep_by_col = {}
|
||||||
|
for dp in deep_paths:
|
||||||
# Ensure target is borrowed
|
if dp["ObjectAttr"] not in ts_names:
|
||||||
if not test_object_borrowed(target_type_name, target_obj_name):
|
continue
|
||||||
t_src = read_source_object(target_type_name, target_obj_name)
|
if not dp.get("SubSubAttr"):
|
||||||
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
continue
|
||||||
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||||
os.makedirs(t_target_dir, exist_ok=True)
|
continue
|
||||||
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||||
save_text_bom(t_target_file, t_borrowed_xml)
|
ts_deep_by_col.setdefault(k, [])
|
||||||
add_to_child_objects(target_type_name, target_obj_name)
|
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||||
borrowed_files.append(t_target_file)
|
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
if ts_deep_by_col:
|
||||||
|
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||||
# Resolve sub-attributes in target catalog
|
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||||
sub_names = {sn: True for sn in sub_attr_names}
|
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||||
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
if not ts_info:
|
||||||
|
continue
|
||||||
if sub_resolved["Attributes"]:
|
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||||
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
if not col_info:
|
||||||
|
continue
|
||||||
# Collect and borrow ref types from deep attributes
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||||
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
if not cat_match:
|
||||||
sub_ref_types = collect_reference_types(sub_type_xmls)
|
continue
|
||||||
for srt in sub_ref_types:
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
||||||
continue
|
|
||||||
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
|
||||||
continue
|
|
||||||
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
|
||||||
if not os.path.isfile(s_src_file):
|
|
||||||
continue
|
|
||||||
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
|
||||||
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
|
||||||
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
|
||||||
os.makedirs(s_target_dir, exist_ok=True)
|
|
||||||
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
|
||||||
save_text_bom(s_target_file, s_borrowed_xml)
|
|
||||||
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
|
||||||
borrowed_files.append(s_target_file)
|
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
|
||||||
|
|
||||||
info(" Main attribute borrowing complete")
|
info(" Main attribute borrowing complete")
|
||||||
|
|
||||||
|
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
||||||
|
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
||||||
|
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
||||||
|
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
||||||
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
||||||
|
t_src = read_source_object(target_type_name, target_obj_name)
|
||||||
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
||||||
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
||||||
|
os.makedirs(t_target_dir, exist_ok=True)
|
||||||
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
||||||
|
save_text_bom(t_target_file, t_borrowed_xml)
|
||||||
|
add_to_child_objects(target_type_name, target_obj_name)
|
||||||
|
borrowed_files.append(t_target_file)
|
||||||
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
|
|
||||||
|
sub_names = {sn: True for sn in sub_attr_names}
|
||||||
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
||||||
|
if sub_resolved["Attributes"]:
|
||||||
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
||||||
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
||||||
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
||||||
|
for srt in sub_ref_types:
|
||||||
|
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
||||||
|
continue
|
||||||
|
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
||||||
|
if not os.path.isfile(s_src_file):
|
||||||
|
continue
|
||||||
|
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
||||||
|
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
||||||
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
||||||
|
os.makedirs(s_target_dir, exist_ok=True)
|
||||||
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
||||||
|
save_text_bom(s_target_file, s_borrowed_xml)
|
||||||
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
||||||
|
borrowed_files.append(s_target_file)
|
||||||
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
|
|
||||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
|
||||||
@@ -1100,8 +1216,22 @@ def main():
|
|||||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
src_form_content = fh.read()
|
src_form_content = fh.read()
|
||||||
|
|
||||||
# 3. Generate form metadata XML
|
# 3. Generate form metadata XML.
|
||||||
new_form_uuid = new_guid()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
||||||
|
new_form_uuid = ""
|
||||||
|
if os.path.isfile(existing_wrapper):
|
||||||
|
try:
|
||||||
|
existing_root = etree.parse(existing_wrapper).getroot()
|
||||||
|
for c in existing_root:
|
||||||
|
if isinstance(c.tag, str) and localname(c) == "Form":
|
||||||
|
new_form_uuid = c.get("uuid", "") or ""
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
new_form_uuid = ""
|
||||||
|
if not new_form_uuid:
|
||||||
|
new_form_uuid = new_guid()
|
||||||
form_meta_lines = [
|
form_meta_lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
||||||
@@ -1131,7 +1261,10 @@ def main():
|
|||||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||||
src_form_el = src_form_tree.getroot()
|
src_form_el = src_form_tree.getroot()
|
||||||
|
|
||||||
form_version = src_form_el.get("version", format_version)
|
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
||||||
|
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
||||||
|
form_version = format_version
|
||||||
|
|
||||||
src_auto_cmd = None
|
src_auto_cmd = None
|
||||||
form_props = []
|
form_props = []
|
||||||
@@ -1149,25 +1282,21 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(etree.tostring(fc, encoding="unicode"))
|
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||||
auto_cmd_xml = ""
|
auto_cmd_xml = ""
|
||||||
if src_auto_cmd is not None:
|
if src_auto_cmd is not None:
|
||||||
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
|
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
||||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
else:
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1178,20 +1307,12 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
||||||
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||||
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
else:
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1428,12 +1549,16 @@ def main():
|
|||||||
save_text_bom(form_xml_file, "".join(parts))
|
save_text_bom(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
module_dir = os.path.join(form_xml_dir, "Form")
|
module_dir = os.path.join(form_xml_dir, "Form")
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||||
save_text_bom(module_bsl_file, "")
|
if os.path.isfile(module_bsl_file):
|
||||||
info(f" Created: {module_bsl_file}")
|
info(" Preserved existing Module.bsl")
|
||||||
|
else:
|
||||||
|
save_text_bom(module_bsl_file, "")
|
||||||
|
info(f" Created: {module_bsl_file}")
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
register_form_in_object(type_name, obj_name, form_name)
|
register_form_in_object(type_name, obj_name, form_name)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
$formatVersion = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -75,6 +79,11 @@ if ($ConfigPath) {
|
|||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if ($fmtVer) {
|
||||||
|
$formatVersion = $fmtVer
|
||||||
|
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||||
|
}
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -138,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -203,7 +212,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -220,7 +229,7 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -50,6 +50,10 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
format_version = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -88,6 +92,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
|
fmt_ver = base_cfg_root.get("version")
|
||||||
|
if fmt_ver:
|
||||||
|
format_version = fmt_ver
|
||||||
|
print(f"[INFO] Base config format version: {format_version}")
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -155,7 +163,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -190,7 +198,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -205,7 +213,7 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cfe-patch-method
|
name: cfe-patch-method
|
||||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
|
||||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -10,22 +10,31 @@ allowed-tools:
|
|||||||
|
|
||||||
# /cfe-patch-method — Генерация перехватчика метода
|
# /cfe-patch-method — Генерация перехватчика метода
|
||||||
|
|
||||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
|
||||||
|
|
||||||
## Предусловие
|
## Предусловие
|
||||||
|
|
||||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
|
||||||
|
|
||||||
|
### Авто-определение ConfigPath
|
||||||
|
|
||||||
|
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||||
|
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||||
|
4. Если `configSrc` нет — спроси у пользователя
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Описание | По умолчанию |
|
| Параметр | Описание | По умолчанию |
|
||||||
|----------|----------|--------------|
|
|----------|----------|--------------|
|
||||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|
||||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
|
||||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
|
||||||
| `Context` | Директива контекста | `НаСервере` |
|
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
|
||||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
|
||||||
|
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
|
||||||
|
|
||||||
## Формат ModulePath
|
## Формат ModulePath
|
||||||
|
|
||||||
@@ -40,39 +49,97 @@ allowed-tools:
|
|||||||
|
|
||||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||||
|
|
||||||
|
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
|
||||||
|
|
||||||
## Типы перехвата
|
## Типы перехвата
|
||||||
|
|
||||||
| InterceptorType | Декоратор | Назначение |
|
| InterceptorType | Декоратор | Назначение | Применим к |
|
||||||
|-----------------|-----------|------------|
|
|-----------------|-----------|------------|------------|
|
||||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
|
||||||
| `After` | `&После` | Код после вызова оригинального метода |
|
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
|
||||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
| `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
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Перехват &Перед на сервере
|
# Код перед записью
|
||||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
|
||||||
# Перехват &После на клиенте
|
# Перехват После на форме
|
||||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||||
|
|
||||||
# ИзменениеИКонтроль для функции
|
# Замена функции (ПродолжитьВызов)
|
||||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||||
|
|
||||||
|
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# Проверить все контролируемые методы расширения на дрейф
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
|
||||||
|
|
||||||
|
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
|
||||||
```
|
```
|
||||||
|
|
||||||
## Генерируемый код (Before)
|
## Верификация
|
||||||
|
|
||||||
```bsl
|
```
|
||||||
&НаСервере
|
/cfe-validate <ExtensionPath>
|
||||||
&Перед("ПриЗаписи")
|
|
||||||
Процедура Расш1_ПриЗаписи()
|
|
||||||
// TODO: код перед вызовом оригинального метода
|
|
||||||
КонецПроцедуры
|
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -930,6 +930,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
|
|||||||
Report-OK "13. TypeLink: clean"
|
Report-OK "13. TypeLink: clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
|
||||||
|
|||||||
@@ -885,6 +885,21 @@ def main():
|
|||||||
elif check13_ok:
|
elif check13_ok:
|
||||||
r.ok('13. TypeLink: clean')
|
r.ok('13. TypeLink: clean')
|
||||||
|
|
||||||
|
# --- 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,7 +25,7 @@ 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`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -38,7 +38,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-create v1.1 — Create 1C information base
|
# db-create v1.7 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -96,7 +97,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,12 +106,55 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-FileIbCreated {
|
||||||
|
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$IbPath)
|
||||||
|
$f = Join-Path $IbPath "1Cv8.1CD"
|
||||||
|
return (Test-Path $f) -and ((Get-Item $f -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -126,6 +170,35 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
|
||||||
|
if ($UseTemplate) {
|
||||||
|
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
|
||||||
|
$arguments += "--restore=$UseTemplate"
|
||||||
|
} else {
|
||||||
|
$arguments += "--load=$UseTemplate", "--apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
@@ -160,12 +233,18 @@ try {
|
|||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not ($InfoBaseServer -and $InfoBaseRef) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
|
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.1 — Create 1C information base
|
# db-create v1.7 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,76 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def file_ib_created(ib_path):
|
||||||
|
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
f = os.path.join(ib_path, "1Cv8.1CD")
|
||||||
|
return os.path.isfile(f) and os.path.getsize(f) > 0
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -82,9 +123,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -93,6 +139,39 @@ def main():
|
|||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
|
||||||
|
if args.UseTemplate:
|
||||||
|
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
|
||||||
|
arguments.append(f"--restore={args.UseTemplate}")
|
||||||
|
else:
|
||||||
|
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— information base was not created",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -102,9 +181,11 @@ def main():
|
|||||||
arguments = ["CREATEINFOBASE"]
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
||||||
|
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
||||||
|
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
||||||
else:
|
else:
|
||||||
arguments.append(f'File="{args.InfoBasePath}"')
|
arguments.append(f'File={args.InfoBasePath}')
|
||||||
|
|
||||||
# --- Template ---
|
# --- Template ---
|
||||||
if args.UseTemplate:
|
if args.UseTemplate:
|
||||||
@@ -132,11 +213,23 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
|
||||||
|
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if is_server:
|
||||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||||
else:
|
else:
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— information base was not created",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -75,6 +76,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +113,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,12 +122,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -135,6 +185,36 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -162,13 +242,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -84,9 +133,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -95,6 +149,40 @@ def main():
|
|||||||
if out_dir and not os.path.isdir(out_dir):
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -127,7 +215,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -136,8 +224,14 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка информационной базы 1С в DT-файл
|
Выгрузка информационной базы 1С в DT-файл
|
||||||
@@ -59,6 +60,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -89,7 +97,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,12 +106,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -119,6 +169,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -139,13 +215,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -82,9 +131,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -93,6 +147,35 @@ def main():
|
|||||||
if out_dir and not os.path.isdir(out_dir):
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -119,7 +202,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -128,8 +211,14 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
@@ -44,7 +44,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -98,6 +99,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -128,7 +136,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,12 +145,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-DirNonEmpty {
|
||||||
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
|
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -164,6 +214,48 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "UpdateInfo") {
|
||||||
|
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
} elseif ($Mode -eq "Partial") {
|
||||||
|
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
$arguments = @("infobase", "config", "export", "objects") + $objList
|
||||||
|
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not exported" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -219,14 +311,19 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
Write-Host "Dump completed successfully" -ForegroundColor Green
|
||||||
Write-Host "Configuration dumped to: $ConfigDir"
|
Write-Host "Configuration dumped to: $ConfigDir"
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def dir_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have written files into the output directory.
|
||||||
|
Exit code 0 with an empty dir (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isdir(path) and any(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -98,9 +147,14 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -114,6 +168,52 @@ def main():
|
|||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
print(f"Created output directory: {args.ConfigDir}")
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "UpdateInfo":
|
||||||
|
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif args.Mode == "Partial":
|
||||||
|
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||||
|
arguments = ["infobase", "config", "export", "objects"] + obj_list
|
||||||
|
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -169,7 +269,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -178,9 +278,15 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Dump completed successfully")
|
print("Dump completed successfully")
|
||||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
# db-load-cf v1.10 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -75,6 +76,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +130,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,12 +139,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -135,6 +195,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -162,7 +248,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -170,7 +256,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
# db-load-cf v1.10 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,102 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -84,9 +151,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -95,6 +167,34 @@ def main():
|
|||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -127,7 +227,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -139,7 +239,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
# db-load-dt v1.9 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка информационной базы 1С из DT-файла
|
Загрузка информационной базы 1С из DT-файла
|
||||||
@@ -72,6 +73,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -102,7 +127,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,12 +136,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -132,6 +192,29 @@ $tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
|
||||||
|
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -154,7 +237,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -162,7 +245,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
# db-load-dt v1.9 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,102 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -84,9 +151,14 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -95,6 +167,31 @@ def main():
|
|||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
|
||||||
|
arguments.append("--create-database")
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -125,7 +222,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -137,7 +234,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||||
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-git v1.5 — Load Git changes into 1C database
|
# db-load-git v1.15 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -107,6 +108,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||||
function Get-ObjectXmlFromSubFile {
|
function Get-ObjectXmlFromSubFile {
|
||||||
param([string]$RelativePath)
|
param([string]$RelativePath)
|
||||||
@@ -149,7 +174,7 @@ if (-not $DryRun) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,14 +183,48 @@ if (-not $DryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -192,6 +251,15 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
|
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
||||||
|
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
||||||
|
function Invoke-GitLines {
|
||||||
|
param([string[]]$GitArgs)
|
||||||
|
$out = git -c core.quotePath=false @GitArgs 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $out }
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -201,29 +269,22 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -319,6 +380,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $configFiles
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
$listFile = Join-Path $tempDir "load_list.txt"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -362,7 +470,7 @@ try {
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
@@ -372,7 +480,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.5 — Load Git changes into 1C database
|
# db-load-git v1.15 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,69 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -76,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git"] + git_args,
|
["git", "-c", "core.quotePath=false"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -87,6 +121,39 @@ def run_git(config_dir, git_args):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -125,9 +192,15 @@ def main():
|
|||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
engine = "1cv8"
|
||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -248,6 +321,58 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
@@ -290,7 +415,7 @@ def main():
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print("")
|
print("")
|
||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
@@ -304,7 +429,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
@@ -45,7 +45,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
# db-load-xml v1.16 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
Загрузка конфигурации 1С из XML-файлов
|
||||||
@@ -107,6 +108,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -137,7 +162,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,12 +171,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -173,6 +233,73 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
$fileList = @()
|
||||||
|
if ($ListFile) {
|
||||||
|
if (-not (Test-Path $ListFile)) {
|
||||||
|
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
} elseif ($Files) {
|
||||||
|
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
}
|
||||||
|
if ($fileList.Count -eq 0) {
|
||||||
|
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $fileList
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -248,7 +375,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -289,7 +416,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($logContent) {
|
if ($logContent) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
# db-load-xml v1.16 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,102 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -106,8 +173,14 @@ def main():
|
|||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -121,6 +194,77 @@ def main():
|
|||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
if args.ListFile:
|
||||||
|
if not os.path.isfile(args.ListFile):
|
||||||
|
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
|
file_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
|
elif args.Files:
|
||||||
|
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
|
||||||
|
else:
|
||||||
|
file_list = []
|
||||||
|
if not file_list:
|
||||||
|
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + file_list
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -197,7 +341,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -241,7 +385,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if log_content:
|
if log_content:
|
||||||
print("--- Log ---")
|
print("--- Log ---")
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.4 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Запуск 1С:Предприятие
|
Запуск 1С:Предприятие
|
||||||
@@ -78,6 +79,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -108,7 +116,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +125,7 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +172,23 @@ if ($URL) {
|
|||||||
|
|
||||||
$argString += " /DisableStartupDialogs"
|
$argString += " /DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# --- Execute (background) ---
|
||||||
Write-Host "Running: 1cv8.exe $argString"
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
$displayArg = Protect-Secrets $argString @($Password, $UserName)
|
||||||
|
Write-Host "Running: 1cv8.exe $displayArg"
|
||||||
|
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||||
|
while ((Get-Date) -lt $deadline -and -not $proc.HasExited) {
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
if ($proc.HasExited) {
|
||||||
|
Write-Host "Error: 1C:Enterprise exited immediately (code: $($proc.ExitCode))" -ForegroundColor Red
|
||||||
|
if ($proc.ExitCode -ne 0) { exit $proc.ExitCode } else { exit 1 }
|
||||||
|
}
|
||||||
|
Write-Host "PID: $($proc.Id)"
|
||||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.4 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -9,6 +9,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
@@ -32,36 +33,57 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -119,9 +141,23 @@ def main():
|
|||||||
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# --- Execute (background) ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
subprocess.Popen([v8path] + arguments)
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
proc = subprocess.Popen([v8path] + arguments)
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
deadline = time.monotonic() + 1.5
|
||||||
|
while time.monotonic() < deadline and proc.poll() is None:
|
||||||
|
time.sleep(0.2)
|
||||||
|
rc = proc.poll()
|
||||||
|
if rc is not None:
|
||||||
|
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
|
||||||
|
sys.exit(rc if rc and rc > 0 else 1)
|
||||||
|
print(f"PID: {proc.pid}")
|
||||||
print("1C:Enterprise launched")
|
print("1C:Enterprise launched")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-update v1.1 — Update 1C database configuration
|
# db-update v1.10 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Обновление конфигурации базы данных 1С
|
Обновление конфигурации базы данных 1С
|
||||||
@@ -88,6 +89,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -118,7 +143,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,12 +152,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -142,6 +202,33 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
|
||||||
|
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -180,7 +267,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -188,7 +275,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.1 — Update 1C database configuration
|
# db-update v1.10 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,102 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -87,11 +154,48 @@ def main():
|
|||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.Dynamic == "+":
|
||||||
|
arguments.append("--dynamic=auto")
|
||||||
|
elif args.Dynamic == "-":
|
||||||
|
arguments.append("--dynamic=disable")
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -132,7 +236,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -144,7 +248,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -47,7 +47,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||||
@@ -69,6 +70,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -99,7 +107,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +116,48 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +195,31 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
$srcDir = Split-Path $SourceFile -Parent
|
||||||
|
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -166,13 +240,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -84,6 +133,10 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
# --- Auto-create stub database if no connection specified ---
|
||||||
auto_created_base = None
|
auto_created_base = None
|
||||||
@@ -117,6 +170,35 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||||
|
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"External data processor/report built successfully: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
@@ -138,7 +220,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -147,8 +229,14 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Build completed successfully: {args.OutputFile}")
|
print(f"Build completed successfully: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -1252,6 +1252,57 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($stubEngine -eq "ibcmd") {
|
||||||
|
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
||||||
|
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
||||||
|
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||||
|
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||||
|
$ibArgs += "--data=$ibData"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
|
||||||
|
$ibOut = $__ib.Output
|
||||||
|
$ibRc = $__ib.ExitCode
|
||||||
|
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ibRc -ne 0) {
|
||||||
|
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
||||||
|
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
Write-Host "[OK] Stub database created: $TempBasePath"
|
||||||
|
Write-Host $TempBasePath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,27 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -1034,6 +1055,32 @@ def main():
|
|||||||
if register_columns:
|
if register_columns:
|
||||||
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
||||||
|
|
||||||
|
# Stub via ibcmd (one call: create [--import --apply])
|
||||||
|
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if stub_engine == "ibcmd":
|
||||||
|
import shutil
|
||||||
|
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
||||||
|
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
||||||
|
if has_ref_types:
|
||||||
|
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||||
|
ib_args.append(f'--data={ib_data}')
|
||||||
|
result = run_ibcmd(ib_args, warn_no_user=False)
|
||||||
|
shutil.rmtree(ib_data, ignore_errors=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if has_ref_types:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
||||||
|
print(f'[OK] Stub database created: {temp_base}')
|
||||||
|
print(temp_base)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -46,7 +46,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
|
|||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||||
@@ -106,7 +107,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +127,60 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-DirNonEmpty {
|
||||||
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
|
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if (-not (Test-Path $InputFile)) {
|
if (-not (Test-Path $InputFile)) {
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||||
@@ -142,6 +197,30 @@ $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
||||||
|
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -163,13 +242,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def dir_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have written files into the output directory.
|
||||||
|
Exit code 0 with an empty dir (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isdir(path) and any(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -90,12 +139,20 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate database connection ---
|
# --- Validate database connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
@@ -111,6 +168,34 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
||||||
|
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
@@ -133,7 +218,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -142,8 +227,14 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
print(f"Dump completed successfully to: {args.OutputDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.7 — Add managed form to 1C config object
|
# form-add v1.11 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -33,6 +33,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++) {
|
||||||
@@ -69,10 +79,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"
|
||||||
@@ -195,7 +208,7 @@ $supportedTypes = @(
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task"
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
||||||
)
|
)
|
||||||
|
|
||||||
$objectType = $null
|
$objectType = $null
|
||||||
@@ -471,7 +484,10 @@ if (-not $childObjects) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавить <Form>$FormName</Form>
|
# Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
|
||||||
|
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
|
||||||
|
|
||||||
|
if (-not $alreadyRegistered) {
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
$formElem.InnerText = $FormName
|
$formElem.InnerText = $FormName
|
||||||
|
|
||||||
@@ -525,6 +541,7 @@ if ($insertBefore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- SetDefault ---
|
# --- SetDefault ---
|
||||||
|
|
||||||
@@ -590,7 +607,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
|
|||||||
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
||||||
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
if ($alreadyRegistered) {
|
||||||
|
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
|
||||||
|
} else {
|
||||||
|
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
||||||
|
}
|
||||||
if ($defaultUpdated) {
|
if ($defaultUpdated) {
|
||||||
Write-Host "${defaultPropName}: $defaultValue"
|
Write-Host "${defaultPropName}: $defaultValue"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.7 — Add managed form to 1C config object
|
# form-add v1.11 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -32,6 +32,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):
|
||||||
@@ -71,6 +83,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
|
||||||
@@ -78,6 +93,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:
|
||||||
@@ -193,14 +210,50 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_with_bom(tree, path):
|
||||||
|
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -256,7 +309,7 @@ def main():
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task",
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
||||||
]
|
]
|
||||||
|
|
||||||
object_type = None
|
object_type = None
|
||||||
@@ -539,47 +592,50 @@ def main():
|
|||||||
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Add <Form>$FormName</Form>
|
# Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
# Find first <Template> to insert before it
|
if not already_registered:
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||||
# Find first <TabularSection> to insert before it (if no Template)
|
form_elem.text = form_name
|
||||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
|
||||||
|
|
||||||
# Determine insertion point: before Template, before TabularSection, or at end
|
# Find first <Template> to insert before it
|
||||||
insert_before = None
|
first_template = child_objects.find("md:Template", NSMAP)
|
||||||
if first_template is not None:
|
# Find first <TabularSection> to insert before it (if no Template)
|
||||||
insert_before = first_template
|
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
||||||
elif first_tabular is not None:
|
|
||||||
insert_before = first_tabular
|
|
||||||
|
|
||||||
if insert_before is not None:
|
# Determine insertion point: before Template, before TabularSection, or at end
|
||||||
# Insert before the found element
|
insert_before = None
|
||||||
idx = list(child_objects).index(insert_before)
|
if first_template is not None:
|
||||||
child_objects.insert(idx, form_elem)
|
insert_before = first_template
|
||||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
elif first_tabular is not None:
|
||||||
form_elem.tail = "\n\t\t\t"
|
insert_before = first_tabular
|
||||||
else:
|
|
||||||
# Add to end of ChildObjects
|
if insert_before is not None:
|
||||||
children = list(child_objects)
|
# Insert before the found element
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
idx = list(child_objects).index(insert_before)
|
||||||
# Empty ChildObjects (self-closing)
|
child_objects.insert(idx, form_elem)
|
||||||
child_objects.text = "\n\t\t\t"
|
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
||||||
child_objects.append(form_elem)
|
form_elem.tail = "\n\t\t\t"
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
else:
|
else:
|
||||||
if len(children) > 0:
|
# Add to end of ChildObjects
|
||||||
last_child = children[-1]
|
children = list(child_objects)
|
||||||
old_tail = last_child.tail
|
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||||
last_child.tail = "\n\t\t\t"
|
# Empty ChildObjects (self-closing)
|
||||||
child_objects.append(form_elem)
|
child_objects.text = "\n\t\t\t"
|
||||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
child_objects.append(form_elem)
|
||||||
form_elem.tail = "\n\t\t"
|
form_elem.tail = "\n\t\t"
|
||||||
|
else:
|
||||||
|
if len(children) > 0:
|
||||||
|
last_child = children[-1]
|
||||||
|
old_tail = last_child.tail
|
||||||
|
last_child.tail = "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||||
|
else:
|
||||||
|
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = "\n\t\t"
|
||||||
|
|
||||||
# --- SetDefault ---
|
# --- SetDefault ---
|
||||||
|
|
||||||
@@ -624,7 +680,10 @@ def main():
|
|||||||
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
||||||
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
||||||
print()
|
print()
|
||||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
if already_registered:
|
||||||
|
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
|
||||||
|
else:
|
||||||
|
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
||||||
if default_updated:
|
if default_updated:
|
||||||
print(f"{default_prop_name}: {default_value}")
|
print(f"{default_prop_name}: {default_value}")
|
||||||
print()
|
print()
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
|||||||
| `showTitle: true` | Показывать заголовок группы |
|
| `showTitle: true` | Показывать заголовок группы |
|
||||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||||
|
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
|
||||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||||
| `children: [...]` | Вложенные элементы |
|
| `children: [...]` | Вложенные элементы |
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.175 — Compile 1C managed form from JSON or object metadata
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$JsonPath,
|
[string]$JsonPath,
|
||||||
@@ -1362,6 +1362,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++) {
|
||||||
@@ -1398,10 +1408,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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.175 — Compile 1C managed form from JSON or object metadata
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
@@ -34,6 +34,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):
|
||||||
@@ -73,6 +85,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
|
||||||
@@ -80,6 +95,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:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.3 — Edit 1C managed form elements
|
# form-edit v1.5 — Edit 1C managed form elements
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -27,6 +27,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++) {
|
||||||
@@ -63,10 +73,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"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.3 — Edit 1C managed form elements (Python port)
|
# form-edit v1.5 — Edit 1C managed form elements (Python port)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -31,6 +31,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):
|
||||||
@@ -70,6 +82,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
|
||||||
@@ -77,6 +92,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:
|
||||||
@@ -1458,14 +1475,40 @@ if elem_events_list:
|
|||||||
|
|
||||||
# ── 13. Save ────────────────────────────────────────────────
|
# ── 13. Save ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Round-trip: определить стиль исходного файла (на диске он ещё не перезаписан).
|
||||||
|
try:
|
||||||
|
_fe_raw = open(resolved_form_path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
_fe_raw = None
|
||||||
|
if _fe_raw is not None:
|
||||||
|
_fe_bom = _fe_raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
_fe_body = _fe_raw[3:] if _fe_bom else _fe_raw
|
||||||
|
_fe_crlf = b"\r\n" in _fe_body
|
||||||
|
_fe_enc_m = re.search(rb'encoding="([^"]+)"', _fe_body[:200])
|
||||||
|
_fe_enc = _fe_enc_m.group(1).decode("ascii") if _fe_enc_m else "utf-8"
|
||||||
|
_fe_final_nl = _fe_body.endswith(b"\n")
|
||||||
|
else:
|
||||||
|
_fe_bom, _fe_crlf, _fe_enc, _fe_final_nl = True, False, "utf-8", True
|
||||||
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
# Fix XML declaration quotes
|
# Восстановить регистр encoding как в оригинале.
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
xml_bytes = xml_bytes.replace(
|
||||||
if not xml_bytes.endswith(b"\n"):
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
|
b'<?xml version="1.0" encoding="' + _fe_enc.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"))
|
||||||
|
# Финальный перенос — как в оригинале.
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if _fe_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# Write with BOM
|
# EOL — как в оригинале.
|
||||||
|
if _fe_crlf:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
# Write preserving BOM as in original.
|
||||||
with open(resolved_form_path, "wb") as f:
|
with open(resolved_form_path, "wb") as f:
|
||||||
f.write(b'\xef\xbb\xbf')
|
if _fe_bom:
|
||||||
|
f.write(b'\xef\xbb\xbf')
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
# ── 14. Summary ─────────────────────────────────────────────
|
# ── 14. Summary ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-info v1.4 — Analyze 1C managed form structure
|
# form-info v1.5 — Analyze 1C managed form structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
@@ -372,6 +372,16 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
|
|||||||
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
||||||
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
||||||
# bin. Never throws — degrades to "не на поддержке".
|
# bin. Never throws — degrades to "не на поддержке".
|
||||||
|
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 Get-SupportStatusForPath([string]$targetPath) {
|
function Get-SupportStatusForPath([string]$targetPath) {
|
||||||
try {
|
try {
|
||||||
$rp = (Resolve-Path $targetPath).Path
|
$rp = (Resolve-Path $targetPath).Path
|
||||||
@@ -390,8 +400,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
|||||||
}
|
}
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
$d = [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 $null }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $binPath) {
|
if (-not $binPath) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
|
|||||||
if ($objectContext) { $header += " ($objectContext)" }
|
if ($objectContext) { $header += " ($objectContext)" }
|
||||||
$header += " ==="
|
$header += " ==="
|
||||||
$lines += $header
|
$lines += $header
|
||||||
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
|
$support = Get-SupportStatusForPath $FormPath
|
||||||
|
if ($null -ne $support) { $lines += "Поддержка: $support" }
|
||||||
|
|
||||||
# --- Form properties (Title excluded — shown in header) ---
|
# --- Form properties (Title excluded — shown in header) ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-info v1.4 — Analyze 1C managed form structure
|
# form-info v1.5 — Analyze 1C managed form structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
def 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
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
elem_uuid = root_uuid(rp)
|
elem_uuid = root_uuid(rp)
|
||||||
|
if is_external_root(rp):
|
||||||
|
return None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
d = os.path.dirname(rp)
|
d = os.path.dirname(rp)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if is_external_root(d + ".xml"):
|
||||||
|
return None
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = root_uuid(d + ".xml")
|
elem_uuid = root_uuid(d + ".xml")
|
||||||
if not bin_path:
|
if not bin_path:
|
||||||
@@ -513,7 +528,9 @@ def main():
|
|||||||
header += f" ({object_context})"
|
header += f" ({object_context})"
|
||||||
header += " ==="
|
header += " ==="
|
||||||
lines.append(header)
|
lines.append(header)
|
||||||
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
|
_support = get_support_status_for_path(form_path)
|
||||||
|
if _support is not None:
|
||||||
|
lines.append(f"Поддержка: {_support}")
|
||||||
|
|
||||||
# --- Form properties (Title excluded -- shown in header) ---
|
# --- Form properties (Title excluded -- shown in header) ---
|
||||||
prop_names = [
|
prop_names = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-remove v1.2 — Remove form from 1C object
|
# form-remove v1.4 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -68,10 +68,14 @@ foreach ($node in $formNodes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Очистить DefaultForm если указывала на эту форму
|
# Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
# (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
|
||||||
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") {
|
# DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
|
||||||
$defaultForm.InnerText = ""
|
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
||||||
|
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
||||||
|
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
||||||
|
$node.InnerText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Сохранить с BOM
|
# Сохранить с BOM
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# remove-form v1.1 — Remove form from 1C object
|
# remove-form v1.4 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -13,14 +13,50 @@ from lxml import etree
|
|||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_with_bom(tree, path):
|
||||||
|
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -85,11 +121,15 @@ def main():
|
|||||||
parent.remove(node)
|
parent.remove(node)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Clear DefaultForm if it pointed to removed form
|
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
# (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
|
||||||
if default_form is not None and default_form.text:
|
# DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
|
||||||
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
|
ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
|
||||||
default_form.text = ""
|
for el in root.iter():
|
||||||
|
if not isinstance(el.tag, str):
|
||||||
|
continue
|
||||||
|
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
||||||
|
el.text = ""
|
||||||
|
|
||||||
# Save with BOM
|
# Save with BOM
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
save_xml_with_bom(tree, root_xml_full)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-validate v1.7 — Validate 1C managed form
|
# form-validate v1.8 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -382,6 +382,10 @@ if (-not $stopped) {
|
|||||||
$pathChecked = 0
|
$pathChecked = 0
|
||||||
$pathBaseSkipped = 0
|
$pathBaseSkipped = 0
|
||||||
|
|
||||||
|
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
|
||||||
|
$bindingTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath',
|
||||||
|
'MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
foreach ($el in $allElements) {
|
foreach ($el in $allElements) {
|
||||||
if ($stopped) { break }
|
if ($stopped) { break }
|
||||||
$tag = $el.Tag
|
$tag = $el.Tag
|
||||||
@@ -398,60 +402,63 @@ if (-not $stopped) {
|
|||||||
try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {}
|
try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
$dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr)
|
foreach ($bTag in $bindingTags) {
|
||||||
if (-not $dpNode) { continue }
|
if ($stopped) { break }
|
||||||
|
$dpNode = $node.SelectSingleNode("f:$bTag", $nsMgr)
|
||||||
|
if (-not $dpNode) { continue }
|
||||||
|
|
||||||
$dataPath = $dpNode.InnerText.Trim()
|
$dataPath = $dpNode.InnerText.Trim()
|
||||||
if (-not $dataPath) { continue }
|
if (-not $dataPath) { continue }
|
||||||
|
|
||||||
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
|
# Opaque platform-internal shapes — not validatable from Form.xml alone:
|
||||||
# - bare numeric (e.g. "10", "1000003") — internal index
|
# - bare numeric (e.g. "10", "1000003") — internal index
|
||||||
# - "N/M:<uuid>" — metadata reference by UUID
|
# - "N/M:<uuid>" — metadata reference by UUID
|
||||||
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
|
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$pathChecked++
|
|
||||||
|
|
||||||
# Extract root segment of path, strip array indices like [0]
|
|
||||||
$cleanPath = $dataPath -replace '\[\d+\]', ''
|
|
||||||
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
|
|
||||||
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
|
|
||||||
$segments = $cleanPath -split '\.'
|
|
||||||
$rootAttr = $segments[0]
|
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
|
||||||
if ($rootAttr -eq 'Items') {
|
|
||||||
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
|
||||||
Report-Warn "[$tag] '$elName': DataPath='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$tableName = $segments[1]
|
|
||||||
$tableEl = $null
|
$pathChecked++
|
||||||
foreach ($candidate in $allElements) {
|
|
||||||
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
|
# Extract root segment of path, strip array indices like [0]
|
||||||
$tableEl = $candidate
|
$cleanPath = $dataPath -replace '\[\d+\]', ''
|
||||||
break
|
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
|
||||||
|
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
|
||||||
|
$segments = $cleanPath -split '\.'
|
||||||
|
$rootAttr = $segments[0]
|
||||||
|
|
||||||
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
||||||
|
if ($rootAttr -eq 'Items') {
|
||||||
|
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
||||||
|
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
$tableName = $segments[1]
|
||||||
|
$tableEl = $null
|
||||||
|
foreach ($candidate in $allElements) {
|
||||||
|
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
|
||||||
|
$tableEl = $candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $tableEl) {
|
||||||
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
|
||||||
|
$pathErrors++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
||||||
|
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
||||||
|
# Table without DataPath — can't resolve further, accept silently
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
||||||
|
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
||||||
|
$rootAttr = ($tableDp -split '\.')[0]
|
||||||
}
|
}
|
||||||
if (-not $tableEl) {
|
|
||||||
Report-Error "[$tag] '$elName': DataPath='$dataPath' — table element '$tableName' not found"
|
|
||||||
$pathErrors++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
|
||||||
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
|
||||||
# Table without DataPath — can't resolve further, accept silently
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
|
||||||
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
|
||||||
$rootAttr = ($tableDp -split '\.')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $attrMap.ContainsKey($rootAttr)) {
|
if (-not $attrMap.ContainsKey($rootAttr)) {
|
||||||
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found"
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
|
||||||
$pathErrors++
|
$pathErrors++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,9 +469,9 @@ if (-not $stopped) {
|
|||||||
$pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote }
|
$pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote }
|
||||||
}
|
}
|
||||||
if ($pathErrors -eq 0 -and $pathMsg) {
|
if ($pathErrors -eq 0 -and $pathMsg) {
|
||||||
Report-OK "DataPath references: $pathMsg"
|
Report-OK "Data bindings: $pathMsg"
|
||||||
} elseif ($pathErrors -eq 0) {
|
} elseif ($pathErrors -eq 0) {
|
||||||
Report-OK "DataPath references: none"
|
Report-OK "Data bindings: none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-validate v1.7 — Validate 1C managed form
|
# form-validate v1.8 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -379,6 +379,10 @@ def main():
|
|||||||
path_checked = 0
|
path_checked = 0
|
||||||
path_base_skipped = 0
|
path_base_skipped = 0
|
||||||
|
|
||||||
|
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
|
||||||
|
binding_tags = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath",
|
||||||
|
"MultipleValueDataPath", "MultipleValuePresentDataPath", "RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||||
|
|
||||||
skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"}
|
skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"}
|
||||||
|
|
||||||
for el in all_elements:
|
for el in all_elements:
|
||||||
@@ -399,55 +403,58 @@ def main():
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
dp_node = node.find(f"{{{F_NS}}}DataPath")
|
for b_tag in binding_tags:
|
||||||
if dp_node is None:
|
if stopped:
|
||||||
continue
|
break
|
||||||
|
dp_node = node.find(f"{{{F_NS}}}{b_tag}")
|
||||||
data_path = (dp_node.text or "").strip()
|
if dp_node is None:
|
||||||
if not data_path:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
|
|
||||||
# - bare numeric (e.g. "10", "1000003") — internal index
|
|
||||||
# - "N/M:<uuid>" — metadata reference by UUID
|
|
||||||
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
path_checked += 1
|
|
||||||
|
|
||||||
clean_path = re.sub(r'\[\d+\]', '', data_path)
|
|
||||||
# Strip leading '~' (current row of DynamicList: ~\u0421\u043f\u0438\u0441\u043e\u043a.\u041f\u043e\u043b\u0435)
|
|
||||||
if clean_path.startswith('~'):
|
|
||||||
clean_path = clean_path[1:]
|
|
||||||
segments = clean_path.split(".")
|
|
||||||
root_attr = segments[0]
|
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... \u2014 table element, not attribute
|
|
||||||
if root_attr == 'Items':
|
|
||||||
if len(segments) < 3 or segments[2] != 'CurrentData':
|
|
||||||
report_warn(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
|
||||||
continue
|
continue
|
||||||
table_name = segments[1]
|
|
||||||
table_el = None
|
data_path = (dp_node.text or "").strip()
|
||||||
for candidate in all_elements:
|
if not data_path:
|
||||||
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
|
continue
|
||||||
table_el = candidate
|
|
||||||
break
|
# Opaque platform-internal shapes — not validatable from Form.xml alone:
|
||||||
if table_el is None:
|
# - bare numeric (e.g. "10", "1000003") — internal index
|
||||||
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 table element '{table_name}' not found")
|
# - "N/M:<uuid>" — metadata reference by UUID
|
||||||
|
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path_checked += 1
|
||||||
|
|
||||||
|
clean_path = re.sub(r'\[\d+\]', '', data_path)
|
||||||
|
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
|
||||||
|
if clean_path.startswith('~'):
|
||||||
|
clean_path = clean_path[1:]
|
||||||
|
segments = clean_path.split(".")
|
||||||
|
root_attr = segments[0]
|
||||||
|
|
||||||
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
||||||
|
if root_attr == 'Items':
|
||||||
|
if len(segments) < 3 or segments[2] != 'CurrentData':
|
||||||
|
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
||||||
|
continue
|
||||||
|
table_name = segments[1]
|
||||||
|
table_el = None
|
||||||
|
for candidate in all_elements:
|
||||||
|
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
|
||||||
|
table_el = candidate
|
||||||
|
break
|
||||||
|
if table_el is None:
|
||||||
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
|
||||||
|
path_errors += 1
|
||||||
|
continue
|
||||||
|
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
||||||
|
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
||||||
|
continue
|
||||||
|
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
||||||
|
if table_dp.startswith('~'):
|
||||||
|
table_dp = table_dp[1:]
|
||||||
|
root_attr = table_dp.split(".")[0]
|
||||||
|
|
||||||
|
if root_attr not in attr_map:
|
||||||
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
|
||||||
path_errors += 1
|
path_errors += 1
|
||||||
continue
|
|
||||||
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
|
||||||
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
|
||||||
continue
|
|
||||||
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
|
||||||
if table_dp.startswith('~'):
|
|
||||||
table_dp = table_dp[1:]
|
|
||||||
root_attr = table_dp.split(".")[0]
|
|
||||||
|
|
||||||
if root_attr not in attr_map:
|
|
||||||
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
|
|
||||||
path_errors += 1
|
|
||||||
|
|
||||||
path_msg = ""
|
path_msg = ""
|
||||||
if path_checked > 0:
|
if path_checked > 0:
|
||||||
@@ -456,7 +463,7 @@ def main():
|
|||||||
skip_note = f"{path_base_skipped} base skipped"
|
skip_note = f"{path_base_skipped} base skipped"
|
||||||
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
||||||
if path_errors == 0 and path_msg:
|
if path_errors == 0 and path_msg:
|
||||||
report_ok(f"DataPath references: {path_msg}")
|
report_ok(f"Data bindings: {path_msg}")
|
||||||
|
|
||||||
# --- Check 6: Button command references ---
|
# --- Check 6: Button command references ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -724,7 +731,7 @@ def main():
|
|||||||
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
|
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
|
||||||
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
|
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
|
||||||
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
|
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
|
||||||
type_invalid += 1
|
type_error_count += 1
|
||||||
else:
|
else:
|
||||||
report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
|
report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
|
||||||
type_warn_count += 1
|
type_warn_count += 1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# help-add v1.7 — Add built-in help to 1C object
|
# help-add v1.9 — Add built-in help to 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -28,6 +28,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++) {
|
||||||
@@ -64,10 +74,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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# add-help v1.7 — Add built-in help to 1C object
|
# add-help v1.9 — Add built-in help to 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -33,6 +33,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 +84,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 +94,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:
|
||||||
@@ -188,14 +205,50 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_with_bom(tree, path):
|
||||||
|
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# img-grid v1.1 — Overlay numbered grid on image
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
||||||
|
|
||||||
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
|
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
|
||||||
@@ -29,6 +32,11 @@ def main():
|
|||||||
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.cols <= 0:
|
||||||
|
parser.error("--cols must be greater than 0")
|
||||||
|
if args.rows < 0:
|
||||||
|
parser.error("--rows must be greater than or equal to 0")
|
||||||
|
|
||||||
src = Image.open(args.image).convert("RGBA")
|
src = Image.open(args.image).convert("RGBA")
|
||||||
sw, sh = src.size
|
sw, sh = src.size
|
||||||
|
|
||||||
@@ -36,7 +44,7 @@ def main():
|
|||||||
step_x = sw / cols
|
step_x = sw / cols
|
||||||
rows = args.rows
|
rows = args.rows
|
||||||
if rows == 0:
|
if rows == 0:
|
||||||
rows = round(sh / step_x)
|
rows = max(1, round(sh / step_x))
|
||||||
step_y = sh / rows
|
step_y = sh / rows
|
||||||
|
|
||||||
# Canvas with margins for labels
|
# Canvas with margins for labels
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
# interface-edit v1.8 — Edit 1C CommandInterface.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]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -39,6 +39,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++) {
|
||||||
@@ -75,10 +85,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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
# interface-edit v1.8 — Edit 1C CommandInterface.xml
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -31,6 +31,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):
|
||||||
@@ -70,6 +82,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
|
||||||
@@ -77,6 +92,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:
|
||||||
@@ -270,13 +287,49 @@ def parse_value_list(val):
|
|||||||
return [val]
|
return [val]
|
||||||
|
|
||||||
|
|
||||||
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,19 @@ allowed-tools:
|
|||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /meta-compile — генерация объектов метаданных из JSON DSL
|
# /meta-compile — генерация объектов метаданных из JSON
|
||||||
|
|
||||||
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
|
Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
|
||||||
|
регистрирует объект в `Configuration.xml`.
|
||||||
|
|
||||||
|
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
|
||||||
|
платформа (для инкрементальной выгрузки).
|
||||||
|
|
||||||
## Порядок работы
|
## Порядок работы
|
||||||
|
|
||||||
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
|
1. Составь JSON по синтаксису ниже → запиши во временный файл.
|
||||||
2. Запусти скрипт meta-compile
|
2. Запусти скрипт.
|
||||||
3. Если нужно изменить созданный объект — `/meta-edit`
|
3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
|
||||||
4. Если нужно проверить — `/meta-validate`
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
||||||
@@ -28,92 +29,110 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
|
|||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
|
| `JsonPath` | Путь к JSON-файлу |
|
||||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
|
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
|
||||||
|
|
||||||
## JSON DSL
|
## Формат JSON
|
||||||
|
|
||||||
### Общая структура
|
**Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
|
{ "type": "Catalog", "name": "Номенклатура", "...свойства типа...": "..." }
|
||||||
```
|
```
|
||||||
|
|
||||||
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
|
`type` и `name` — обязательные. Остальное — по типу (см. индекс ниже). `synonym` по умолчанию выводится из
|
||||||
|
`name` (CamelCase → слова через пробел); можно задать явно строкой или мультиязычно: `"synonym": { "ru": "…", "en": "…" }`.
|
||||||
|
|
||||||
### Shorthand реквизитов
|
## Реквизиты (shorthand)
|
||||||
|
|
||||||
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
|
Массивы `attributes`, `dimensions`, `resources` и колонки в `tabularSections` задаются строками:
|
||||||
|
|
||||||
```
|
```
|
||||||
"ИмяРеквизита" → String(10) по умолчанию
|
"Имя" → String(10)
|
||||||
"ИмяРеквизита: Тип" → с типом
|
"Имя: Тип" → с типом
|
||||||
"ИмяРеквизита: Тип | req, index" → с флагами
|
"Имя: Тип | req, index" → с флагами
|
||||||
```
|
```
|
||||||
|
|
||||||
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
|
**Типы:** `String(100)`, `String(10, fixed)` (фикс. длина), `Number(15,2)`, `Boolean`, `Date`, `DateTime`,
|
||||||
|
`Time`, ссылочные `CatalogRef.Xxx` / `DocumentRef.Xxx` / `EnumRef.Xxx` / `DefinedType.Xxx` и т.п.
|
||||||
|
Составной тип — через `+`: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
||||||
|
|
||||||
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
**Флаги** (после `|`, через запятую):
|
||||||
|
|
||||||
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
|
| Флаг | Значение | Где |
|
||||||
|
|------|----------|-----|
|
||||||
|
| `req` | обязательное заполнение | attributes, dimensions, resources |
|
||||||
|
| `index` | индексировать | attributes, dimensions |
|
||||||
|
| `indexAdditional` | индекс с доп. упорядочиванием | attributes |
|
||||||
|
| `multiline` | многострочное поле | attributes |
|
||||||
|
| `nonneg` | неотрицательное (Number) | attributes, resources |
|
||||||
|
| `master` | ведущее измерение | dimensions (регистры) |
|
||||||
|
| `mainFilter` | основной отбор | dimensions (регистры) |
|
||||||
|
| `denyIncomplete` | запрет незаполненных | dimensions |
|
||||||
|
| `useInTotals` | использовать в итогах | dimensions (регистр накопления) |
|
||||||
|
|
||||||
### Свойства по типам
|
Реквизиту нужны свойства сверх shorthand (значение заполнения, параметры выбора, формат, подсказка, …) —
|
||||||
|
задаётся **объектной формой**, см. `reference/attributes.md`.
|
||||||
|
|
||||||
Примеров и 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
|
```json
|
||||||
{ "type": "Catalog", "name": "Валюты" }
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
### С реквизитами
|
Ключ — имя ТЧ, значение — массив колонок (shorthand) ЛИБО объект со свойствами ТЧ (см. `reference/attributes.md`).
|
||||||
|
|
||||||
|
## Индекс: свойства по типам
|
||||||
|
|
||||||
|
Для каждого типа — свой reference-файл со свойствами, дефолтами и допустимыми значениями:
|
||||||
|
|
||||||
|
| Тип(ы) | Файл |
|
||||||
|
|--------|------|
|
||||||
|
| Catalog (справочник) | `reference/catalog.md` |
|
||||||
|
| Document, DocumentJournal, Sequence, DocumentNumerator | `reference/document.md` |
|
||||||
|
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister | `reference/registers.md` |
|
||||||
|
| ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | `reference/charts.md` |
|
||||||
|
| ExchangePlan | `reference/exchangeplan.md` |
|
||||||
|
| BusinessProcess, Task | `reference/process.md` |
|
||||||
|
| Report, DataProcessor | `reference/report-dataprocessor.md` |
|
||||||
|
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
|
||||||
|
| HTTPService, WebService | `reference/web.md` |
|
||||||
|
| Enum, Constant, DefinedType | `reference/simple.md` |
|
||||||
|
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
|
||||||
|
|
||||||
|
Кросс-типовые детали:
|
||||||
|
- **`reference/attributes.md`** — объектная форма реквизита и колонки ТЧ (значение заполнения, параметры
|
||||||
|
выбора, формат, подсказка, границы, …) + свойства самой ТЧ.
|
||||||
|
- **`reference/blocks.md`** — блоки объекта: представления, команды (+ характеристики/стандартные реквизиты).
|
||||||
|
|
||||||
|
Эта инструкция и reference-файлы — полная документация. Не ищи примеры XML в выгрузках конфигураций.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
Справочник с реквизитами:
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
|
||||||
"type": "Catalog", "name": "Организации",
|
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
|
||||||
"descriptionLength": 100,
|
|
||||||
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### С табличной частью
|
Документ с движениями и ТЧ:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "Document", "name": "ПриходнаяНакладная",
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Регистровый паттерн (измерения + ресурсы)
|
Регистр сведений:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch — несколько объектов в одном файле
|
Batch:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
|
||||||
{ "type": "Catalog", "name": "Валюты" },
|
{ "type": "Catalog", "name": "Валюты" },
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Объектная форма реквизита и табличной части
|
||||||
|
|
||||||
|
Когда реквизиту (в `attributes` / `dimensions` / `resources` / колонках ТЧ) нужны свойства сверх
|
||||||
|
shorthand — вместо строки задаётся объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Цена", "type": "Number(15,2)", "tooltip": "Цена за единицу", "fillValue": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` и `type` обязательны (тип можно задать и раздельно: `"type": "Number", "length": 15, "precision": 2`).
|
||||||
|
Остальные ключи — ниже, все со значением по умолчанию (не задавать, если устраивает дефолт).
|
||||||
|
|
||||||
|
## Свойства реквизита
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML (строка или `{ru,en}`) |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (то же, что флаг `req`) |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `fillFromFillingValue` | `false` | bool |
|
||||||
|
| `fillValue` | по типу (см. ниже) | значение заполнения |
|
||||||
|
| `createOnInput` | `Auto` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `quickChoice` | `Auto` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
|
||||||
|
| `dataHistory` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (реквизит иерархического справочника) |
|
||||||
|
| `passwordMode` | `false` | bool |
|
||||||
|
| `multiLine` | `false` | bool (то же, что флаг `multiline`) |
|
||||||
|
| `extendedEdit` | `false` | bool (расширенное редактирование — многострочный ввод) |
|
||||||
|
| `mask` | пусто | строка маски ввода |
|
||||||
|
| `format` / `editFormat` | пусто | форматная строка 1С (ML) |
|
||||||
|
| `markNegatives` | `false` | bool (выделять отрицательные, для Number) |
|
||||||
|
| `minValue` / `maxValue` | не задано | граница диапазона (см. ниже) |
|
||||||
|
| `choiceParameterLinks` | пусто | связи параметров выбора (см. ниже) |
|
||||||
|
| `choiceParameters` | пусто | параметры выбора (см. ниже) |
|
||||||
|
| `choiceForm` | пусто | ссылка на форму выбора `Тип.Объект.Form.ИмяФормы` |
|
||||||
|
| `choiceFoldersAndItems` | `Items` | `Items` / `Folders` / `FoldersAndItems` (что выбирать в иерарх. справочнике) |
|
||||||
|
|
||||||
|
Индексирование задаётся флагом `index` / `indexAdditional` в shorthand, либо в объекте — как и в строковой форме,
|
||||||
|
через `"type": "… | index"`.
|
||||||
|
|
||||||
|
### `fillValue` — значение заполнения
|
||||||
|
|
||||||
|
Пустое значение по типу компилятор подставляет сам — ключ **не задают**:
|
||||||
|
|
||||||
|
| Тип реквизита | Пустое значение |
|
||||||
|
|---------------|-----------------|
|
||||||
|
| String | пустая строка |
|
||||||
|
| Number | `0` |
|
||||||
|
| Boolean, Date, ссылочный, составной | не задано (nil) |
|
||||||
|
|
||||||
|
Ключ `fillValue` задают для **конкретного** значения — интерпретируется по типу реквизита:
|
||||||
|
|
||||||
|
- **Boolean** — `true` / `false`.
|
||||||
|
- **Number** — число (`21`, `1.5`).
|
||||||
|
- **String** — строка.
|
||||||
|
- **Date** — ISO-строка `"2020-01-01T00:00:00"`.
|
||||||
|
- **Ссылочный** — путь: `"Catalog.Валюты.EmptyRef"` (пустая ссылка), `"Enum.Периодичность.EnumValue.Месяц"`
|
||||||
|
(значение перечисления), `"Catalog.СтраныМира.Россия"` (предопределённый элемент).
|
||||||
|
- **`null`** — явно «значение не задано» (nil), когда нужно перекрыть непустой дефолт типа.
|
||||||
|
- **`{ "emptyRef": true }`** — пустая ссылка для реквизита типа `DefinedType.X` (когда тип из пути не выводится).
|
||||||
|
|
||||||
|
> Пустая ссылка (`EmptyRef`) и `null` — разное: платформа хранит их отдельно.
|
||||||
|
|
||||||
|
### `minValue` / `maxValue` — границы диапазона
|
||||||
|
|
||||||
|
Число → числовая граница; строка → строковая (напр. год `"2000"`). Без ключа — граница не задана.
|
||||||
|
|
||||||
|
### `choiceParameterLinks` — связи параметров выбора
|
||||||
|
|
||||||
|
Связывают параметр выбора этого реквизита с другим реквизитом объекта. Массив строк или объектов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameterLinks": ["Отбор.Организация=Организация", "Отбор.Договор=Договор:DontChange"]
|
||||||
|
"choiceParameterLinks": [{ "name": "Отбор.Организация", "dataPath": "Организация", "valueChange": "Clear" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `dataPath` — реквизит **того же объекта**: имя обычного реквизита (`"Организация"`) или стандартного
|
||||||
|
(`"Владелец"`, `"Ссылка"`).
|
||||||
|
- `valueChange` — `Clear` (по умолчанию) / `DontChange`.
|
||||||
|
|
||||||
|
### `choiceParameters` — параметры выбора
|
||||||
|
|
||||||
|
Фиксируют параметр выбора значением. Массив строк или объектов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameters": ["Отбор.ЭтоГруппа=false"]
|
||||||
|
"choiceParameters": [{ "name": "Отбор.Владелец", "value": "Catalog.Организации.EmptyRef" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `value` — bool / число / строка / ссылочный путь (несёт тип) ИЛИ массив (список фиксированных значений).
|
||||||
|
- Для набора голых имён-значений добавьте `type` (тип поля-фильтра), чтобы они стали ссылками:
|
||||||
|
`{ "name": "Отбор.Тип", "type": "EnumRef.ТипыВЕТИС", "value": ["EmptyRef", "ТТН"] }`.
|
||||||
|
|
||||||
|
### Редкие ключи
|
||||||
|
|
||||||
|
`linkByType` — связь по типу (тип реквизита-Характеристики берётся из другого реквизита):
|
||||||
|
`{ "dataPath": "Свойство", "linkItem": 0 }` или строка-путь. Применяется для реквизитов-характеристик.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Табличная часть — объектная форма
|
||||||
|
|
||||||
|
Значение в `tabularSections` — массив колонок ЛИБО объект со свойствами самой ТЧ:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"tabularSections": {
|
||||||
|
"Товары": {
|
||||||
|
"synonym": { "ru": "Товары", "en": "Goods" },
|
||||||
|
"tooltip": "Строки заказа",
|
||||||
|
"fillChecking": "ShowError",
|
||||||
|
"attributes": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (обязательность заполнения ТЧ) |
|
||||||
|
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
|
||||||
|
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
|
||||||
|
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
|
||||||
|
|
||||||
|
### `lineNumber` — стандартный реквизит НомерСтроки
|
||||||
|
|
||||||
|
У каждой ТЧ есть стандартный реквизит НомерСтроки. По умолчанию все его свойства типовые. Ключ `lineNumber`
|
||||||
|
на объектной форме ТЧ их переопределяет:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Строки": { "lineNumber": { "synonym": "Номер п/п", "fullTextSearch": "DontUse" }, "attributes": [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Переопределяемые: `synonym`, `comment`, `fullTextSearch` (`Use`/`DontUse`), `tooltip`, `format`, `editFormat`,
|
||||||
|
`choiceHistoryOnInput` (`Auto`/`DontUse`).
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Блоки объекта
|
||||||
|
|
||||||
|
Кросс-типовые блоки уровня объекта (применимы к ссылочным типам — Catalog, Document, ChartOf*, ExchangePlan,
|
||||||
|
BusinessProcess, Task и др.).
|
||||||
|
|
||||||
|
## Представления
|
||||||
|
|
||||||
|
Тексты представления объекта в интерфейсе (ML — строка или `{ru,en}`, по умолчанию пусто):
|
||||||
|
|
||||||
|
| Ключ | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `objectPresentation` | представление объекта |
|
||||||
|
| `extendedObjectPresentation` | расширенное представление объекта |
|
||||||
|
| `listPresentation` | представление списка |
|
||||||
|
| `extendedListPresentation` | расширенное представление списка |
|
||||||
|
| `explanation` | пояснение |
|
||||||
|
|
||||||
|
Набор доступных ключей зависит от типа (у списочных без формы объекта нет `objectPresentation` и т.п.).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"listPresentation": "Организации", "objectPresentation": { "ru": "Организация", "en": "Company" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
Команды объекта. Ключ — имя команды, значение — объект свойств (map `имя → объект` или массив `[{name, …}]`).
|
||||||
|
Для каждой команды создаётся заготовка модуля с обработчиком `ОбработкаКоманды`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `group` | **обязательно** | группа размещения (см. ниже) |
|
||||||
|
| `commandParameterType` | пусто | тип параметра (напр. `CatalogRef.Номенклатура`) — **только для групп формы** |
|
||||||
|
| `parameterUseMode` | `Single` | `Single` / `Multiple` |
|
||||||
|
| `modifiesData` | `false` | bool |
|
||||||
|
| `representation` | `Auto` | вид отображения |
|
||||||
|
| `picture` | пусто | ссылка на картинку (`StdPicture.Print`, `CommonPicture.Загрузка`) |
|
||||||
|
| `shortcut` | пусто | сочетание клавиш |
|
||||||
|
|
||||||
|
```json
|
||||||
|
"commands": {
|
||||||
|
"ПечатьЭтикеток": { "synonym": "Печать этикеток", "group": "FormCommandBarImportant",
|
||||||
|
"commandParameterType": "CatalogRef.Номенклатура", "picture": "StdPicture.Print" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Группа (`group`) обязательна** — каждая команда размещается в группе командного интерфейса:
|
||||||
|
|
||||||
|
- **Командный интерфейс раздела** (панель навигации / панель действий; `commandParameterType` **недоступен**):
|
||||||
|
`NavigationPanelImportant` / `NavigationPanelOrdinary` / `NavigationPanelSeeAlso`,
|
||||||
|
`ActionsPanelCreate` / `ActionsPanelReports` / `ActionsPanelTools`.
|
||||||
|
- **Командный интерфейс формы** (`commandParameterType` допустим): `FormCommandBarImportant` /
|
||||||
|
`FormCommandBarCreateBasedOn`, `FormNavigationPanelImportant` / `FormNavigationPanelGoTo` / `FormNavigationPanelSeeAlso`.
|
||||||
|
- **Кастомная группа:** `CommandGroup.<Имя>` (параметр допустим).
|
||||||
|
|
||||||
|
Группа раздела вместе с `commandParameterType` → ошибка.
|
||||||
|
|
||||||
|
## `inputByString` / `dataLockFields` / `basedOn`
|
||||||
|
|
||||||
|
Списки полей/объектов уровня объекта. Поля — по имени реквизита объекта (обычного или стандартного).
|
||||||
|
|
||||||
|
- **`inputByString`** — поля быстрого ввода по строке. По умолчанию выводятся из Кода/Наименования — ключ не нужен;
|
||||||
|
задать при другом наборе/порядке, либо `[]` для отключения.
|
||||||
|
```json
|
||||||
|
"inputByString": ["Код", "Наименование", "Контрагент"]
|
||||||
|
```
|
||||||
|
- **`dataLockFields`** — поля управляемой блокировки данных (по умолчанию пусто).
|
||||||
|
```json
|
||||||
|
"dataLockFields": ["Организация", "Контрагент"]
|
||||||
|
```
|
||||||
|
- **`basedOn`** — «ввод на основании»: список ссылок на объекты метаданных (по умолчанию пусто).
|
||||||
|
```json
|
||||||
|
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `standardAttributes` — кастомизация стандартных реквизитов
|
||||||
|
|
||||||
|
Стандартные реквизиты объекта (Наименование, Код, Владелец, …) переопределяются блоком
|
||||||
|
`standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена — как в 1С: `Description`, `Code`,
|
||||||
|
`Owner`, `Parent`, `DeletionMark`, `Ref` и т.д. (для Document — `Date`, `Number`, `Posted`).
|
||||||
|
|
||||||
|
Переопределяемые поля — как у обычного реквизита (`synonym`, `tooltip`, `fillChecking`, `fillValue`,
|
||||||
|
`choiceParameters`, `comment`, `mask`, `choiceForm`; полный набор — `attributes.md`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"standardAttributes": {
|
||||||
|
"Description": { "synonym": "Наименование контрагента" },
|
||||||
|
"Code": { "fillChecking": "ShowError" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `characteristics` — «Дополнительные реквизиты и сведения»
|
||||||
|
|
||||||
|
Привязка плана видов характеристик. Массив; каждый элемент связывает **источник типов** (где определены
|
||||||
|
характеристики) и **источник значений** (где хранятся значения).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"characteristics": [{
|
||||||
|
"types": { "from": "Catalog.НаборыДопРеквизитов.ДополнительныеРеквизиты",
|
||||||
|
"key": "Свойство", "filterField": "Ссылка", "filterValue": "Справочник_Организации" },
|
||||||
|
"values": { "from": "Catalog.Организации.TabularSection.ДополнительныеРеквизиты",
|
||||||
|
"object": "Ссылка", "type": "Свойство", "value": "Значение" }
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `from` — таблица-источник; `key`/`filterField`/`object`/`type`/`value` — поля источника (по имени реквизита).
|
||||||
|
- `filterValue` — значение фильтра типов: имя предопределённого набора (строка) или путь к элементу.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Catalog (Справочник)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
|
||||||
|
"attributes": ["ИНН: String(12)", "КПП: String(9)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Свойства
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `hierarchical` | `false` | bool |
|
||||||
|
| `hierarchyType` | `HierarchyFoldersAndItems` | `HierarchyFoldersAndItems` / `HierarchyOfItems` |
|
||||||
|
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
|
||||||
|
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
|
||||||
|
| `foldersOnTop` | `true` | bool (группы сверху) |
|
||||||
|
| `owners` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` |
|
||||||
|
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
|
||||||
|
| `codeLength` | `9` | длина кода (0 — без кода) |
|
||||||
|
| `codeType` | `String` | `String` / `Number` |
|
||||||
|
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
|
||||||
|
| `autonumbering` | `true` | bool (автонумерация) |
|
||||||
|
| `checkUnique` | `false` | bool (контроль уникальности кода) |
|
||||||
|
| `descriptionLength` | `25` | длина наименования |
|
||||||
|
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
|
||||||
|
| `quickChoice` | `true` | bool (быстрый выбор) |
|
||||||
|
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
|
||||||
|
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
|
||||||
|
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
|
||||||
|
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
|
||||||
|
| `tabularSections` | `{}` | табличные части |
|
||||||
|
|
||||||
|
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
|
||||||
|
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
|
||||||
|
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
|
||||||
|
|
||||||
|
## `predefined` — предопределённые элементы
|
||||||
|
|
||||||
|
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
|
||||||
|
|
||||||
|
**Строка:** `"(Код) Имя [Наименование]"` — `Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
|
||||||
|
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
"Основной",
|
||||||
|
"(1) ДокументОПриемке [Документ о приемке]",
|
||||||
|
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
|
||||||
|
"childItems": ["Факс", "(7) Скайп"] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
|
||||||
|
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- Свойства реквизитов и табличных частей — `attributes.md`.
|
||||||
|
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
|
||||||
|
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
|
||||||
|
`basedOn` — `blocks.md`.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
|
||||||
|
|
||||||
|
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
|
||||||
|
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
|
||||||
|
специальными свойствами.
|
||||||
|
|
||||||
|
## ChartOfCharacteristicTypes (План видов характеристик)
|
||||||
|
|
||||||
|
Хранит определения характеристик (видов). Иерархический (папки+элементы).
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
|
||||||
|
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
|
||||||
|
| `hierarchical` | `false` | bool |
|
||||||
|
| `foldersOnTop` | `true` | bool |
|
||||||
|
| `codeLength` | `9` | длина кода |
|
||||||
|
| `descriptionLength` | `100` | длина наименования |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
|
||||||
|
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
"(000001) Цвет: CatalogRef.Цвета",
|
||||||
|
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
|
||||||
|
{ "name": "Группа", "isFolder": true, "type": "" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ChartOfAccounts (План счетов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
|
||||||
|
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
|
||||||
|
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
|
||||||
|
| `codeLength` | `9` | длина кода |
|
||||||
|
| `descriptionLength` | `25` | длина наименования |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
|
||||||
|
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
|
||||||
|
| `autoOrderByCode` | `true` | bool |
|
||||||
|
| `orderLength` | `9` | длина строки упорядочивания |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
|
||||||
|
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
|
||||||
|
| `predefined` | `[]` | предопределённые счета (см. ниже) |
|
||||||
|
|
||||||
|
**Предопределённый счёт** (объектная форма):
|
||||||
|
|
||||||
|
| Поле | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `name` | — | имя (обязательно) |
|
||||||
|
| `code` | пусто | код счёта |
|
||||||
|
| `description` | из имени | наименование |
|
||||||
|
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
|
||||||
|
| `offBalance` | `false` | bool (забалансовый) |
|
||||||
|
| `order` | — | строка сортировки |
|
||||||
|
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
|
||||||
|
| `subconto` | `[]` | виды субконто (см. ниже) |
|
||||||
|
| `childItems` | `[]` | подчинённые счета |
|
||||||
|
|
||||||
|
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover` —
|
||||||
|
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
|
||||||
|
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
|
||||||
|
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ChartOfCalculationTypes (План видов расчёта)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `codeLength` | `5` | длина кода |
|
||||||
|
| `descriptionLength` | `100` | длина наименования |
|
||||||
|
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
|
||||||
|
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
|
||||||
|
| `actionPeriodUse` | `false` | bool (использовать период действия) |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
|
||||||
|
|
||||||
|
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
|
||||||
|
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
|
||||||
|
> соответствующий план (см. `registers.md`).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
|
||||||
|
|
||||||
|
## CommonModule (Общий модуль)
|
||||||
|
|
||||||
|
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `context` | — | шорткат флагов (см. ниже) |
|
||||||
|
| `global` | `false` | bool |
|
||||||
|
| `server` | `false` | bool |
|
||||||
|
| `serverCall` | `false` | bool (вызов сервера) |
|
||||||
|
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
|
||||||
|
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
|
||||||
|
| `externalConnection` | `false` | bool |
|
||||||
|
| `privileged` | `false` | bool |
|
||||||
|
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
|
||||||
|
|
||||||
|
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
|
||||||
|
`"serverClient"` → Server+ClientManagedApplication.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## ScheduledJob (Регламентное задание)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
|
||||||
|
| `description` | пусто | наименование задания |
|
||||||
|
| `key` | пусто | ключ |
|
||||||
|
| `use` | `false` | bool (использование) |
|
||||||
|
| `predefined` | `false` | bool (предопределённое) |
|
||||||
|
| `restartCountOnFailure` | `3` | число повторов при сбое |
|
||||||
|
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## EventSubscription (Подписка на событие)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
|
||||||
|
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
|
||||||
|
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
|
||||||
|
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
|
||||||
|
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Document, DocumentJournal, Sequence, DocumentNumerator
|
||||||
|
|
||||||
|
## Document (Документ)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Document", "name": "ПриходнаяНакладная",
|
||||||
|
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||||
|
"attributes": ["Организация: CatalogRef.Организации"],
|
||||||
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
|
||||||
|
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
|
||||||
|
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
|
||||||
|
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
|
||||||
|
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
|
||||||
|
| `postInPrivilegedMode` | `true` | bool |
|
||||||
|
| `unpostInPrivilegedMode` | `true` | bool |
|
||||||
|
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
|
||||||
|
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
|
||||||
|
`inputByString`, `dataLockFields` — `blocks.md`.
|
||||||
|
|
||||||
|
## DocumentJournal (Журнал документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
|
||||||
|
| `columns` | `[]` | графы журнала (см. ниже) |
|
||||||
|
|
||||||
|
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing` — `Index`/`DontIndex`,
|
||||||
|
`references` — пути к реквизитам документов, отображаемым в графе.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DocumentJournal", "name": "Взаимодействия",
|
||||||
|
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
|
||||||
|
"columns": [{ "name": "Организация", "indexing": "Index",
|
||||||
|
"references": ["Document.Встреча.Attribute.Организация"] }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequence (Последовательность документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
|
||||||
|
| `documents` | `[]` | документы последовательности (список ссылок) |
|
||||||
|
| `registerRecords` | `[]` | движения (список ссылок) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
|
||||||
|
|
||||||
|
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
|
||||||
|
|
||||||
|
## DocumentNumerator (Нумератор документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# ExchangePlan (План обмена)
|
||||||
|
|
||||||
|
Близок к справочнику (без иерархии/владельцев), плюс состав объектов обмена. Наследует слой Catalog:
|
||||||
|
`codeLength`, `codeAllowedLength`, `descriptionLength`, `defaultPresentation`, `editType`, `quickChoice`,
|
||||||
|
`choiceMode`, формы, `standardAttributes`, `characteristics`, `inputByString`, `basedOn`, представления —
|
||||||
|
см. `catalog.md` / `attributes.md` / `blocks.md`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `distributedInfoBase` | `false` | bool (распределённая ИБ — РИБ) |
|
||||||
|
| `includeConfigurationExtensions` | `false` | bool (включать расширения конфигурации) |
|
||||||
|
| `descriptionLength` | `150` | длина наименования |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `content` | `[]` | состав обмена (см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
## `content` — состав плана обмена
|
||||||
|
|
||||||
|
Список объектов-участников обмена; у каждого — признак авторегистрации изменений (по умолчанию выключена).
|
||||||
|
Элемент — ссылка на объект метаданных (строка) или объект с признаком:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"content": [
|
||||||
|
"Catalog.Организации", // авторегистрация выключена
|
||||||
|
"InformationRegister.Курсы: autoRecord", // авторегистрация включена (токен)
|
||||||
|
{ "metadata": "Document.РеализацияТоваров", "autoRecord": true }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Строка `"Тип.Имя"` — авторегистрация выключена; суффикс `: autoRecord` — включена.
|
||||||
|
- Объект: `metadata` (ссылка), `autoRecord` (bool или `Allow`/`Deny`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "ExchangePlan", "name": "ОбменССайтом", "distributedInfoBase": false,
|
||||||
|
"content": ["Catalog.Номенклатура: autoRecord", "Catalog.Контрагенты: autoRecord"],
|
||||||
|
"attributes": ["АдресСервера: String(200)"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Прочие типы
|
||||||
|
|
||||||
|
Редкие/служебные объекты. Каждый — минимальный набор свойств.
|
||||||
|
|
||||||
|
## FunctionalOption (Функциональная опция)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `location` | пусто | где хранится значение: `Constant.X` / `InformationRegister.X.Resource.Y` / `<Тип>.X.Attribute.Y` |
|
||||||
|
| `content` | `[]` | реквизиты/измерения/ресурсы, зависящие от опции (полные пути к объектам) |
|
||||||
|
| `privilegedGetMode` | `true` | bool |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "FunctionalOption", "name": "ВестиУчетПоСкладам", "location": "Constant.ВестиУчетПоСкладам",
|
||||||
|
"content": ["Document.РеализацияТоваров.TabularSection.Товары.Attribute.Склад"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## FilterCriterion (Критерий отбора)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | — | тип значения отбора (составной через `+`) |
|
||||||
|
| `content` | `[]` | реквизиты, по которым идёт отбор (пути к объектам) |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | формы |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "FilterCriterion", "name": "ДокументыПоКонтрагенту", "valueType": "CatalogRef.Контрагенты",
|
||||||
|
"content": ["Document.Реализация.Attribute.Контрагент"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## SettingsStorage (Хранилище настроек)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `defaultSaveForm` / `defaultLoadForm` | пусто | формы сохранения / загрузки |
|
||||||
|
| `auxiliarySaveForm` / `auxiliaryLoadForm` | пусто | вспомогательные формы |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
## CommonForm (Общая форма)
|
||||||
|
|
||||||
|
Создаёт метаданные + заготовку формы. Содержимое формы наполняется `/form-compile` или `/form-edit`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `formType` | `Managed` | тип формы |
|
||||||
|
| `usePurposes` | `[PlatformApplication, MobilePlatformApplication]` | назначение (массив) |
|
||||||
|
| `useStandardCommands` | `false` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonForm", "name": "НастройкиОбмена", "usePurposes": ["PlatformApplication"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## CommonPicture / CommonTemplate (Общие картинки и макеты)
|
||||||
|
|
||||||
|
Только метаданные + регистрация; содержимое (`Ext/Picture*`, `Ext/Template.*`) импортируется отдельно
|
||||||
|
(для табличного макета — `/mxl-compile`).
|
||||||
|
|
||||||
|
- **CommonPicture** — `availabilityForChoice` / `availabilityForAppearance` (bool, по умолчанию `false`).
|
||||||
|
- **CommonTemplate** — `templateType` (`SpreadsheetDocument` по умолчанию / `TextDocument` / `HTMLDocument` /
|
||||||
|
`BinaryData` / `AddIn` / `DataCompositionSchema` / `DataCompositionAppearanceTemplate` / `GraphicalSchema`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonTemplate", "name": "ПечатьЗаказа", "templateType": "SpreadsheetDocument" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Служебные типы
|
||||||
|
|
||||||
|
- **SessionParameter** (параметр сеанса) — `valueType` (тип значения, составной через `+`).
|
||||||
|
- **FunctionalOptionsParameter** (параметр функциональной опции) — `use` (массив измерений/реквизитов).
|
||||||
|
- **WSReference** (WS-ссылка) — `locationURL` (URL WSDL).
|
||||||
|
- **CommandGroup** (группа команд) — `category` (по умолч. `NavigationPanel`) — где размещается группа:
|
||||||
|
`NavigationPanel` / `ActionsPanel` (командный интерфейс раздела) или `FormCommandBar` / `FormNavigationPanel`
|
||||||
|
(командный интерфейс формы); `representation` (`Auto`), `tooltip` (ML), `picture`. Команды объекта ссылаются на
|
||||||
|
группу через `group: "CommandGroup.<Имя>"` (см. `blocks.md`).
|
||||||
|
- **CommonCommand** (общая команда) — `group`, `representation`, `tooltip`, `picture`, `shortcut`,
|
||||||
|
`commandParameterType`, `parameterUseMode` (`Single`/`Multiple`), `modifiesData`, `includeHelpInContents`.
|
||||||
|
Создаёт `Ext/CommandModule.bsl`.
|
||||||
|
- **CommonAttribute** (общий реквизит) — `valueType` (по умолчанию `String(0)`) + свойства реквизита
|
||||||
|
(`attributes.md`) + `content` (объекты, куда входит реквизит) + свойства разделения данных
|
||||||
|
(`dataSeparation`, `separatedDataUse`, `usersSeparation`, … — по умолчанию `DontUse`/`Independently`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonAttribute", "name": "Организация", "valueType": "CatalogRef.Организации",
|
||||||
|
"autoUse": "Use", "content": ["Document.РеализацияТоваров", "Document.ПоступлениеТоваров"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# BusinessProcess, Task (Бизнес-процессы и Задачи)
|
||||||
|
|
||||||
|
Ссылочные типы. Наследуют слой Catalog (нумерация, формы, `standardAttributes`, `characteristics`, `basedOn`,
|
||||||
|
представления — см. `catalog.md` / `attributes.md` / `blocks.md`). Бизнес-процесс всегда связан с задачей.
|
||||||
|
|
||||||
|
## BusinessProcess (Бизнес-процесс)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `task` | пусто | ссылка на задачу `Task.X` (обязательна для рабочего БП) |
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
Создаётся с картой маршрута (`Ext/Flowchart.xml`) и модулем объекта.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "BusinessProcess", "name": "Согласование", "task": "Task.ЗадачаИсполнителя",
|
||||||
|
"attributes": ["Документ: DocumentRef.ЗаявкаНаРасход"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task (Задача)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `14` | длина номера |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `descriptionLength` | `150` | длина наименования |
|
||||||
|
| `addressing` | пусто | ссылка на регистр сведений адресации `InformationRegister.X` |
|
||||||
|
| `mainAddressingAttribute` | пусто | основной реквизит адресации (имя реквизита адресации) |
|
||||||
|
| `currentPerformer` | пусто | реквизит текущего исполнителя |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `addressingAttributes` | `[]` | реквизиты адресации (см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
**Реквизит адресации** — shorthand `"Имя: Тип"` или объект `{ name, type, addressingDimension }`
|
||||||
|
(`addressingDimension` — измерение регистра адресации).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Task", "name": "ЗадачаИсполнителя",
|
||||||
|
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи", "Роль: CatalogRef.Роли"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Регистры: Information, Accumulation, Accounting, Calculation
|
||||||
|
|
||||||
|
**Измерения и ресурсы** задаются как реквизиты (shorthand `"Имя: Тип | флаги"` или объектная форма, см.
|
||||||
|
`attributes.md`). Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (регистр накопления).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"],
|
||||||
|
"resources": ["Сумма: Number(15,2)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## InformationRegister (Регистр сведений)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `writeMode` | `Independent` | `Independent` / `RecorderSubordinate` |
|
||||||
|
| `periodicity` | `Nonperiodical` | `Nonperiodical` / `Second` / `Day` / `Month` / `Quarter` / `Year` / `RecorderPosition` |
|
||||||
|
| `mainFilterOnPeriod` | `false` | bool (основной отбор по периоду) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||||
|
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||||
|
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## AccumulationRegister (Регистр накопления)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `registerType` | `Balance` | `Balance` (остатки) / `Turnovers` (обороты) |
|
||||||
|
| `enableTotalsSplitting` | `true` | bool (разделение итогов) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
|
||||||
|
"dimensions": ["Номенклатура: CatalogRef.Номенклатура", "Склад: CatalogRef.Склады"],
|
||||||
|
"resources": ["Количество: Number(15,3)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## AccountingRegister (Регистр бухгалтерии)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `chartOfAccounts` | — | **обязательно**: ссылка на план счетов `ChartOfAccounts.X` |
|
||||||
|
| `correspondence` | `false` | bool (корреспонденция) |
|
||||||
|
| `periodAdjustmentLength` | `0` | длина периода корректировки |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "AccountingRegister", "name": "Хозрасчетный",
|
||||||
|
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
|
||||||
|
"dimensions": ["Организация: CatalogRef.Организации"], "resources": ["Сумма: Number(15,2)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## CalculationRegister (Регистр расчёта)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `chartOfCalculationTypes` | — | **обязательно**: ссылка на ПВР `ChartOfCalculationTypes.X` |
|
||||||
|
| `periodicity` | `Month` | периодичность |
|
||||||
|
| `actionPeriod` | `false` | bool (период действия) |
|
||||||
|
| `basePeriod` | `false` | bool (базовый период) |
|
||||||
|
| `schedule` | пусто | ссылка на регистр сведений графиков |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CalculationRegister", "name": "Начисления",
|
||||||
|
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления", "periodicity": "Month",
|
||||||
|
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"], "resources": ["Сумма: Number(15,2)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **AccountingRegister** требует план счетов, **CalculationRegister** — план видов расчёта (и оба — документ-регистратор).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Report, DataProcessor (Отчёты и Обработки)
|
||||||
|
|
||||||
|
Почти идентичны по составу: реквизиты, табличные части, формы, макеты, команды. Модуль объекта — `Ext/ObjectModule.bsl`.
|
||||||
|
Реквизиты и ТЧ — `attributes.md`; команды — `blocks.md`.
|
||||||
|
|
||||||
|
Ссылки на формы/схемы/хранилища пишутся **как есть** (имя формы может быть буквально «Форма»).
|
||||||
|
|
||||||
|
## Report (Отчёт)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `useStandardCommands` | `true` | bool (доступность через стандартный командный интерфейс) |
|
||||||
|
| `mainDataCompositionSchema` | пусто | основной макет СКД (`Report.X.Template.ОсновнаяСхемаКомпоновкиДанных`) |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
|
||||||
|
| `defaultSettingsForm` / `auxiliarySettingsForm` / `defaultVariantForm` | пусто | формы настроек / вариантов |
|
||||||
|
| `variantsStorage` / `settingsStorage` | пусто | хранилища вариантов / настроек (`SettingsStorage.X`) |
|
||||||
|
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Report", "name": "АнализПродаж", "useStandardCommands": false,
|
||||||
|
"mainDataCompositionSchema": "Report.АнализПродаж.Template.ОсновнаяСхемаКомпоновкиДанных",
|
||||||
|
"attributes": ["Период: StandardPeriod"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## DataProcessor (Обработка)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
|
||||||
|
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DataProcessor", "name": "ЗагрузкаТаблиц", "useStandardCommands": false,
|
||||||
|
"attributes": [{ "name": "Таблица", "type": "ValueTree" }, { "name": "Произвольные", "type": "" }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
> Реквизиты отчётов/обработок допускают платформенные типы-коллекции: `ValueTable`, `ValueTree`, `ValueList`,
|
||||||
|
> `StandardPeriod`, `SpreadsheetDocument` и др., а также `"type": ""` — реквизит без типа.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Enum, Constant, DefinedType
|
||||||
|
|
||||||
|
## Enum (Перечисление)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `values` | `[]` | значения перечисления (массив имён или объектов) |
|
||||||
|
|
||||||
|
Значение — строка `"ИмяЗначения"` или объект `{ name, synonym }`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constant (Константа)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | `String` | тип значения (shorthand типа) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
|
||||||
|
`valueType` принимает shorthand: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`,
|
||||||
|
составной через `+`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## DefinedType (Определяемый тип)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueTypes` | `[]` | состав типа (массив shorthand-типов) |
|
||||||
|
| `valueType` | — | то же одной строкой (`"A + B"`) или строкой одного типа |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DefinedType", "name": "ДенежныеСредства",
|
||||||
|
"valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
|
||||||
|
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
|
||||||
|
```
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# HTTPService, WebService (Веб-сервисы)
|
||||||
|
|
||||||
|
Модуль обоих — `Ext/Module.bsl`, в нём реализуются обработчики.
|
||||||
|
|
||||||
|
## HTTPService (HTTP-сервис)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
|
||||||
|
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
|
||||||
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
|
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
|
||||||
|
|
||||||
|
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
|
||||||
|
- строка — URL-путь без методов: `"/health"`;
|
||||||
|
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `methods` — `{ "ИмяМетода": "HTTPMethod" }`.
|
||||||
|
|
||||||
|
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
||||||
|
Обработчик метода в модуле именуется `{ИмяШаблона}{ИмяМетода}`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "HTTPService", "name": "API", "rootURL": "api",
|
||||||
|
"urlTemplates": {
|
||||||
|
"Users": { "template": "/v1/users/{id}", "methods": { "Get": "GET", "Create": "POST", "Delete": "DELETE" } },
|
||||||
|
"Health": "/health"
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebService (Веб-сервис, SOAP)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `namespace` | пусто | URI пространства имён WSDL |
|
||||||
|
| `xdtoPackages` | пусто | XDTO-пакеты |
|
||||||
|
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
|
||||||
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
|
| `operations` | `{}` | операции (см. ниже) |
|
||||||
|
|
||||||
|
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
|
||||||
|
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
|
||||||
|
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
|
||||||
|
`handler` (имя процедуры, по умолчанию = имя операции), `parameters`.
|
||||||
|
|
||||||
|
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
|
||||||
|
- строка — XDTO-тип (`direction` = `In`);
|
||||||
|
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`), `direction` (`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
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: meta-decompile
|
||||||
|
description: Декомпиляция объекта метаданных 1С в JSON-заготовку формата meta-compile. Используй когда нужно получить черновик DSL-описания нового объекта по образцу другого. Не сохраняет UUID/модули/формы.
|
||||||
|
argument-hint: <ObjectPath> [-OutputPath <out.json>]
|
||||||
|
disable-model-invocation: true
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /meta-decompile — DSL-заготовка из XML объекта метаданных
|
||||||
|
|
||||||
|
Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `/meta-compile`. Назначение — **взять существующий объект образцом и собрать по нему НОВЫЙ**: декомпилировать → поправить → скомпилировать под другим именем.
|
||||||
|
|
||||||
|
## ⚠️ Главное: это НЕ обратимая выгрузка
|
||||||
|
|
||||||
|
Компиляция черновика создаёт **новый объект с новой идентичностью**, а не копию исходного. В JSON **не** попадают: UUID (идентичность самого объекта и всех дочерних), тела модулей, формы, макеты, права. Захватываются только структура и свойства.
|
||||||
|
|
||||||
|
Отсюда правило: **никогда не компилируй черновик поверх объекта-источника и не выдавай его за «реимпорт»** — у пересобранного объекта другие UUID, поэтому все ссылки на исходный объект (из кода, других объектов, состава подсистем, предопределённых данных) сломаются, а код модулей и формы пропадут.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
**Собрать новый объект по образцу существующего** — получить DSL-заготовку рабочего объекта, переименовать и адаптировать состав, скомпилировать в новый. Быстрее, чем писать DSL с нуля для богатого объекта.
|
||||||
|
|
||||||
|
## Когда **не** использовать
|
||||||
|
|
||||||
|
- **Точечная правка существующего объекта** (добавить реквизит, ТЧ, свойство) → `/meta-edit`. Цикл decompile→compile тут вреден: даёт объект с новой идентичностью и теряет модули/формы.
|
||||||
|
- **Сохранить / восстановить / перенести тот же объект** (бэкап, миграция между конфигурациями с сохранением ссылок) → штатная выгрузка 1С (`/db-dump-xml` ↔ `/db-load-xml`, CF), а не decompile.
|
||||||
|
- **Просто понять структуру** объекта (реквизиты, ТЧ, типы) без пересборки → `/meta-info` (дешевле, не плодит файл).
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `ObjectPath` | Путь к XML объекта (`Catalogs/Имя.xml`), обязательный |
|
||||||
|
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr.
|
||||||
|
|
||||||
|
## Workflow (сборка нового объекта по образцу)
|
||||||
|
|
||||||
|
1. `/meta-decompile <Образец.xml> -OutputPath draft.json` — получить заготовку.
|
||||||
|
2. В `draft.json` **сменить `name`** на имя нового объекта и адаптировать состав (реквизиты/ТЧ/свойства). Ссылки на *другие* объекты (владельцы, ввод на основании, типы) — по имени, сохраняются как есть.
|
||||||
|
3. `/meta-compile -JsonPath draft.json -OutputDir <ConfigDir>` — собрать (объект получит свежие UUID).
|
||||||
|
4. `/meta-validate` + `/meta-info` — проверить.
|
||||||
|
5. Модули, формы, макеты, права — добавить отдельно (в черновик они не попадают).
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,9 @@ Batch через `;;` во всех операциях. Подробный си
|
|||||||
|
|
||||||
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
||||||
|
|
||||||
|
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
|
||||||
|
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
|
||||||
|
|
||||||
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
||||||
|
|
||||||
| Операция | Формат Value | Пример |
|
| Операция | Формат Value | Пример |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
|
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
|
||||||
|
|
||||||
Флаги: `req` (FillChecking=ShowError), `index` (Indexing=Index), `master` (Master=true, только dimensions), `mainFilter` (MainFilterOperand, только dimensions).
|
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
|
||||||
|
|
||||||
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
|
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
|
||||||
```powershell
|
```powershell
|
||||||
@@ -84,6 +84,27 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
|
|||||||
|
|
||||||
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
|
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
|
||||||
|
|
||||||
|
## add-predefined
|
||||||
|
|
||||||
|
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
|
||||||
|
идентификаторы сохраняются, новые получают свежий id.
|
||||||
|
|
||||||
|
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
|
||||||
|
```powershell
|
||||||
|
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON — строки и/или объекты (для групп с вложенными):
|
||||||
|
```json
|
||||||
|
{ "add": { "predefined": [
|
||||||
|
"(001) Основной",
|
||||||
|
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
|
||||||
|
] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
|
||||||
|
берётся из объекта автоматически.
|
||||||
|
|
||||||
## add-enumValue / add-form / add-template / add-command
|
## add-enumValue / add-form / add-template / add-command
|
||||||
|
|
||||||
Просто имена (batch через `;;`):
|
Просто имена (batch через `;;`):
|
||||||
@@ -107,10 +128,34 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
|
|||||||
|
|
||||||
Формат: `ИмяЭлемента: ключ=значение, ключ=значение`
|
Формат: `ИмяЭлемента: ключ=значение, ключ=значение`
|
||||||
|
|
||||||
Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др.
|
**Спец-операции** (строчные ключи): `name` (переименование), `type` (смена типа), `synonym`.
|
||||||
|
|
||||||
|
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
|
||||||
|
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
|
||||||
|
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка
|
||||||
|
(правка не теряется молча).
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||||
-Operation modify-attribute -Value "Комментарий: indexing=Index"
|
-Operation modify-attribute -Value "Комментарий: Indexing=Index, FullTextSearch=Use"
|
||||||
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
|
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Структурные свойства реквизита
|
||||||
|
|
||||||
|
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
|
||||||
|
|
||||||
|
| Ключ | Значение | Пример (JSON) |
|
||||||
|
|------|----------|---------------|
|
||||||
|
| `Format` / `EditFormat` / `ToolTip` | строка (мультиязычная) | `"Format": "ДФ=dd.MM.yyyy"` |
|
||||||
|
| `ChoiceForm` | путь формы выбора | `"ChoiceForm": "Catalog.Товары.Form.ФормаВыбора"` |
|
||||||
|
| `MinValue` / `MaxValue` | число или строка | `"MinValue": 0, "MaxValue": 100` |
|
||||||
|
| `FillValue` | значение заполнения | `"FillValue": "EmptyRef"` · `true` · `10` · `{"nil": true}` |
|
||||||
|
| `LinkByType` | `{dataPath, linkItem?}` | `"LinkByType": {"dataPath": "Вид", "linkItem": 0}` |
|
||||||
|
| `ChoiceParameterLinks` | `[{name, dataPath, valueChange?}]` | `["Отбор.Организация=Организация"]` |
|
||||||
|
| `ChoiceParameters` | `[{name, type?, value?}]` | `[{"name": "Отбор.ЭтоГруппа", "value": false}]` |
|
||||||
|
|
||||||
|
- `FillValue`: `"EmptyRef"` — пустая ссылка по типу реквизита; `{"emptyRef": true}` / `{"nil": true}` — явные маркеры.
|
||||||
|
- `ChoiceParameters` value — булево/число/строка/ссылочный путь или массив; укажите `type` (напр.
|
||||||
|
`EnumRef.СтавкиНДС`), чтобы задавать значения короткими именами (`"Оптовая"` вместо полного пути).
|
||||||
|
- В путях данных (`LinkByType`/`ChoiceParameterLinks`) можно писать короткое имя реквизита вместо полного пути.
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
# Свойства объекта и complex properties
|
# Свойства объекта и свойства-списки
|
||||||
|
|
||||||
Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString).
|
Справочник операций для обычных свойств объекта и свойств-списков (Owners, RegisterRecords, BasedOn, InputByString).
|
||||||
|
|
||||||
## modify-property
|
## modify-property
|
||||||
|
|
||||||
Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`):
|
Изменение свойств объекта по имени свойства 1С (PascalCase, как в конфигураторе). Формат: `Ключ=Значение`
|
||||||
|
(batch через `;;`):
|
||||||
```powershell
|
```powershell
|
||||||
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
|
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
|
||||||
-Operation modify-property -Value "Hierarchical=true"
|
-Operation modify-property -Value "Hierarchical=true"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Complex properties
|
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
||||||
|
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта.
|
||||||
|
|
||||||
Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
### Type — тип значения (Константа, ПВХ)
|
||||||
|
|
||||||
|
`Type=...` перестраивает дескриптор типа значения. Значение — тип 1С в том же синтаксисе,
|
||||||
|
что у реквизитов: составной через `+`, с квалификаторами и ссылочными типами:
|
||||||
|
```powershell
|
||||||
|
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
||||||
|
```
|
||||||
|
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать
|
||||||
|
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
|
||||||
|
|
||||||
|
## Свойства-списки
|
||||||
|
|
||||||
|
Свойства, значение которых — список ссылок. Управляются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
||||||
|
|
||||||
| Свойство | Объекты | Inline-значение |
|
| Свойство | Объекты | Inline-значение |
|
||||||
|----------|---------|-----------------|
|
|----------|---------|-----------------|
|
||||||
@@ -20,31 +34,36 @@
|
|||||||
| RegisterRecords | Document | `AccumulationRegister.XXX` |
|
| RegisterRecords | Document | `AccumulationRegister.XXX` |
|
||||||
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
|
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
|
||||||
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
|
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
|
||||||
|
| DataLockFields | Catalog, Document, регистры и др. | `Организация` (короткое имя реквизита → полный путь) |
|
||||||
|
| RegisteredDocuments | DocumentJournal | `Document.XXX` |
|
||||||
|
|
||||||
### add-owner / add-registerRecord / add-basedOn
|
### add-owner / add-registerRecord / add-basedOn / add-registeredDocument
|
||||||
|
|
||||||
Полное имя метаданных `MetaType.Name`:
|
Полное имя метаданных `MetaType.Name`:
|
||||||
```powershell
|
```powershell
|
||||||
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
||||||
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
|
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
|
||||||
-Operation add-basedOn -Value "Document.ЗаказКлиента"
|
-Operation add-basedOn -Value "Document.ЗаказКлиента"
|
||||||
|
-Operation add-registeredDocument -Value "Document.РасходныйОрдер"
|
||||||
```
|
```
|
||||||
|
|
||||||
### add-inputByString
|
### add-inputByString / add-dataLockField
|
||||||
|
|
||||||
Пути полей (префикс `MetaType.Name.` добавляется автоматически):
|
Пути полей (короткое имя реквизита разворачивается в полный путь автоматически):
|
||||||
```powershell
|
```powershell
|
||||||
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
||||||
|
-Operation add-dataLockField -Value "Организация ;; Контрагент"
|
||||||
```
|
```
|
||||||
|
|
||||||
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString
|
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString / remove-dataLockField / remove-registeredDocument
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
-Operation remove-owner -Value "Catalog.Контрагенты"
|
-Operation remove-owner -Value "Catalog.Контрагенты"
|
||||||
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
|
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
|
||||||
|
-Operation remove-dataLockField -Value "Организация"
|
||||||
```
|
```
|
||||||
|
|
||||||
### set-owners / set-registerRecords / set-basedOn / set-inputByString
|
### set-owners / set-registerRecords / set-basedOn / set-inputByString / set-dataLockFields / set-registeredDocuments
|
||||||
|
|
||||||
Заменяют **весь список** (в отличие от add/remove):
|
Заменяют **весь список** (в отличие от add/remove):
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
|
# meta-edit v1.22 — Edit existing 1C metadata object XML
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
@@ -12,14 +12,17 @@ param(
|
|||||||
"add-attribute", "add-ts", "add-dimension", "add-resource",
|
"add-attribute", "add-ts", "add-dimension", "add-resource",
|
||||||
"add-enumValue", "add-column", "add-form", "add-template", "add-command",
|
"add-enumValue", "add-column", "add-form", "add-template", "add-command",
|
||||||
"add-owner", "add-registerRecord", "add-basedOn", "add-inputByString",
|
"add-owner", "add-registerRecord", "add-basedOn", "add-inputByString",
|
||||||
|
"add-dataLockField", "add-registeredDocument", "add-predefined",
|
||||||
"remove-attribute", "remove-ts", "remove-dimension", "remove-resource",
|
"remove-attribute", "remove-ts", "remove-dimension", "remove-resource",
|
||||||
"remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command",
|
"remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command",
|
||||||
"remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString",
|
"remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString",
|
||||||
|
"remove-dataLockField", "remove-registeredDocument",
|
||||||
"add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts",
|
"add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts",
|
||||||
"modify-attribute", "modify-dimension", "modify-resource",
|
"modify-attribute", "modify-dimension", "modify-resource",
|
||||||
"modify-enumValue", "modify-column",
|
"modify-enumValue", "modify-column",
|
||||||
"modify-property",
|
"modify-property",
|
||||||
"set-owners", "set-registerRecords", "set-basedOn", "set-inputByString"
|
"set-owners", "set-registerRecords", "set-basedOn", "set-inputByString",
|
||||||
|
"set-dataLockFields", "set-registeredDocuments"
|
||||||
)]
|
)]
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[string]$Value,
|
[string]$Value,
|
||||||
@@ -78,7 +81,7 @@ $script:validEnumValues = @{
|
|||||||
"Posting" = @("Allow","Deny")
|
"Posting" = @("Allow","Deny")
|
||||||
"RealTimePosting" = @("Allow","Deny")
|
"RealTimePosting" = @("Allow","Deny")
|
||||||
"EditType" = @("InDialog","InList","BothWays")
|
"EditType" = @("InDialog","InList","BothWays")
|
||||||
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly")
|
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyOfItems")
|
||||||
"CodeType" = @("String","Number")
|
"CodeType" = @("String","Number")
|
||||||
"CodeAllowedLength" = @("Variable","Fixed")
|
"CodeAllowedLength" = @("Variable","Fixed")
|
||||||
"NumberType" = @("String","Number")
|
"NumberType" = @("String","Number")
|
||||||
@@ -167,6 +170,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++) {
|
||||||
@@ -203,10 +216,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"
|
||||||
@@ -607,6 +623,8 @@ function Import-Fragment([string]$xmlString) {
|
|||||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
||||||
xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"
|
xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"
|
||||||
xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"
|
xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"
|
||||||
|
xmlns:app="http://v8.1c.ru/8.2/managed-application/core"
|
||||||
|
xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString</_W>
|
xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString</_W>
|
||||||
"@
|
"@
|
||||||
$frag = New-Object System.Xml.XmlDocument
|
$frag = New-Object System.Xml.XmlDocument
|
||||||
@@ -890,20 +908,33 @@ $script:reservedAttrNames = @{
|
|||||||
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
|
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты по типу объекта (ключи из reservedAttrNames). Имя реквизита, совпадающее
|
||||||
|
# с ними (англ. ИЛИ рус.), платформа не позволит — жёсткий отказ. Контексты вне карты → предупреждение.
|
||||||
|
$script:reservedByContext = @{
|
||||||
|
"catalog" = @("Ref","DeletionMark","Predefined","PredefinedDataName","Code","Description","Owner","Parent","IsFolder")
|
||||||
|
"document" = @("Ref","DeletionMark","Date","Number","Posted")
|
||||||
|
}
|
||||||
|
|
||||||
function Build-AttributeFragment {
|
function Build-AttributeFragment {
|
||||||
param($parsed, [string]$context, [string]$indent)
|
param($parsed, [string]$context, [string]$indent)
|
||||||
|
|
||||||
if (-not $context) { $context = Get-AttributeContext }
|
if (-not $context) { $context = Get-AttributeContext }
|
||||||
|
|
||||||
# Check reserved attribute names
|
# Check reserved attribute names (типозависимо: catalog/document — жёсткий отказ; прочее — предупреждение)
|
||||||
$attrName = $parsed.name
|
$attrName = $parsed.name
|
||||||
if ($script:reservedAttrNames.ContainsKey($attrName)) {
|
$ctxReserved = $script:reservedByContext[$context]
|
||||||
|
if ($ctxReserved) {
|
||||||
|
foreach ($en in $ctxReserved) {
|
||||||
|
$ru = $script:reservedAttrNames[$en]
|
||||||
|
if (($attrName -ieq $en) -or ($ru -and $attrName -ieq $ru)) {
|
||||||
|
Write-Error "Имя реквизита '$attrName' зарезервировано стандартным реквизитом ($en/$ru) объекта '$context'. Выберите другое имя."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($context -notin @("tabular", "processor-tabular") -and
|
||||||
|
($script:reservedAttrNames.ContainsKey($attrName) -or ($script:reservedAttrNames.Values -contains $attrName))) {
|
||||||
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
|
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
|
||||||
}
|
}
|
||||||
$ruValues = $script:reservedAttrNames.Values
|
|
||||||
if ($ruValues -contains $attrName) {
|
|
||||||
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name (Russian). This may cause errors when loading into 1C."
|
|
||||||
}
|
|
||||||
|
|
||||||
$uuid = New-Guid-String
|
$uuid = New-Guid-String
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
@@ -1475,6 +1506,8 @@ function Convert-InlineToDefinition([string]$operation, [string]$value) {
|
|||||||
"registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords"
|
"registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords"
|
||||||
"basedOn" = "BasedOn"
|
"basedOn" = "BasedOn"
|
||||||
"inputByString" = "InputByString"
|
"inputByString" = "InputByString"
|
||||||
|
"dataLockField" = "DataLockFields"; "dataLockFields" = "DataLockFields"
|
||||||
|
"registeredDocument" = "RegisteredDocuments"; "registeredDocuments" = "RegisteredDocuments"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($complexTargetMap.ContainsKey($target)) {
|
if ($complexTargetMap.ContainsKey($target)) {
|
||||||
@@ -1499,6 +1532,16 @@ function Convert-InlineToDefinition([string]$operation, [string]$value) {
|
|||||||
return $def
|
return $def
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Предопределённые (Ext/Predefined.xml) — отдельный файл; строим { <op>: { predefined: [...] } }.
|
||||||
|
if ($target -eq 'predefined') {
|
||||||
|
$items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
$inner = New-Object PSCustomObject
|
||||||
|
$inner | Add-Member -NotePropertyName 'predefined' -NotePropertyValue $items
|
||||||
|
$def = New-Object PSCustomObject
|
||||||
|
$def | Add-Member -NotePropertyName $op -NotePropertyValue $inner
|
||||||
|
return $def
|
||||||
|
}
|
||||||
|
|
||||||
# TS attribute operations: dot notation "TSName.AttrDef"
|
# TS attribute operations: dot notation "TSName.AttrDef"
|
||||||
if ($target -eq "ts-attribute") {
|
if ($target -eq "ts-attribute") {
|
||||||
$items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
$items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
@@ -1741,6 +1784,10 @@ function Process-Add($addDef) {
|
|||||||
$addDef.PSObject.Properties | ForEach-Object {
|
$addDef.PSObject.Properties | ForEach-Object {
|
||||||
$rawKey = $_.Name
|
$rawKey = $_.Name
|
||||||
$items = $_.Value
|
$items = $_.Value
|
||||||
|
if ($rawKey -in @('predefined','предопределенные','предопределённые')) {
|
||||||
|
Add-PredefinedItems $items
|
||||||
|
return
|
||||||
|
}
|
||||||
$childType = Resolve-ChildTypeKey $rawKey
|
$childType = Resolve-ChildTypeKey $rawKey
|
||||||
|
|
||||||
if (-not $childType) {
|
if (-not $childType) {
|
||||||
@@ -1961,8 +2008,19 @@ function Modify-Properties($propsDef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $propEl) {
|
if (-not $propEl) {
|
||||||
Warn "Property '$propName' not found in Properties"
|
# create-if-missing: известное свойство создаём (порядок 1С терпит — append); неизвестное → ошибка (опечатка)
|
||||||
return
|
if ($script:knownObjectProps -notcontains $propName) {
|
||||||
|
Write-Error "modify-property: неизвестное свойство '$propName' — нет такого свойства объекта (опечатка?)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$newNodes = Import-Fragment "<$propName/>"
|
||||||
|
if ($newNodes.Count -gt 0) {
|
||||||
|
Insert-PropertyInOrder $script:propertiesEl $newNodes[0] $null $propName
|
||||||
|
$propEl = $newNodes[0]
|
||||||
|
} else {
|
||||||
|
Warn "Property '$propName': could not create element"
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Complex property: Owners, RegisterRecords, BasedOn, InputByString
|
# Complex property: Owners, RegisterRecords, BasedOn, InputByString
|
||||||
@@ -1983,6 +2041,32 @@ function Modify-Properties($propsDef) {
|
|||||||
$valueStr = if ($propValue) { "true" } else { "false" }
|
$valueStr = if ($propValue) { "true" } else { "false" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Structural value-type property (корневой <Type> у Константы, ПВХ) —
|
||||||
|
# перестроить дескриптор типа через Build-ValueTypeXml (не расплющивать в скаляр)
|
||||||
|
if ($propName -ceq "Type") {
|
||||||
|
$typeIndent = Get-ChildIndent $script:propertiesEl
|
||||||
|
$newTypeXml = Build-ValueTypeXml $typeIndent $valueStr
|
||||||
|
$newTypeNodes = Import-Fragment $newTypeXml
|
||||||
|
if ($newTypeNodes.Count -gt 0) {
|
||||||
|
# ReplaceChild сохраняет whitespace до/после узла на месте (без склейки отступов)
|
||||||
|
$script:propertiesEl.ReplaceChild($newTypeNodes[0], $propEl) | Out-Null
|
||||||
|
Info "Modified property: Type = $valueStr"
|
||||||
|
$script:modifyCount++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Guard: не расплющивать структурное свойство (с дочерними узлами) в скалярный текст —
|
||||||
|
# это молча повредит XML. Завершаем ошибкой ДО записи файла.
|
||||||
|
$hasChildElements = $false
|
||||||
|
foreach ($ch in $propEl.ChildNodes) {
|
||||||
|
if ($ch.NodeType -eq 'Element') { $hasChildElements = $true; break }
|
||||||
|
}
|
||||||
|
if ($hasChildElements) {
|
||||||
|
Write-Error "modify-property: свойство '$propName' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
$propEl.InnerText = $valueStr
|
$propEl.InnerText = $valueStr
|
||||||
Info "Modified property: $propName = $valueStr"
|
Info "Modified property: $propName = $valueStr"
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
@@ -2216,6 +2300,56 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
|
|||||||
Info "Changed synonym of $xmlTag '$elemName': $changeValue"
|
Info "Changed synonym of $xmlTag '$elemName': $changeValue"
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
}
|
}
|
||||||
|
"Format" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "Format" (Build-MLTextXml (Get-ChildIndent $propsEl) "Format" "$changeValue")) {
|
||||||
|
Info "Set $xmlTag '$elemName'.Format"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"EditFormat" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "EditFormat" (Build-MLTextXml (Get-ChildIndent $propsEl) "EditFormat" "$changeValue")) {
|
||||||
|
Info "Set $xmlTag '$elemName'.EditFormat"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ToolTip" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "ToolTip" (Build-MLTextXml (Get-ChildIndent $propsEl) "ToolTip" "$changeValue")) {
|
||||||
|
Info "Set $xmlTag '$elemName'.ToolTip"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ChoiceForm" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "ChoiceForm" "<ChoiceForm>$(Esc-Xml "$changeValue")</ChoiceForm>") {
|
||||||
|
Info "Set $xmlTag '$elemName'.ChoiceForm"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"MinValue" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "MinValue" (Build-MinMaxValueXml "MinValue" $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.MinValue"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"MaxValue" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "MaxValue" (Build-MinMaxValueXml "MaxValue" $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.MaxValue"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"LinkByType" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "LinkByType" (Build-LinkByTypeXml (Get-ChildIndent $propsEl) $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.LinkByType"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ChoiceParameterLinks" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "ChoiceParameterLinks" (Build-ChoiceParameterLinksXml (Get-ChildIndent $propsEl) $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.ChoiceParameterLinks"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ChoiceParameters" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "ChoiceParameters" (Build-ChoiceParametersXml (Get-ChildIndent $propsEl) $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.ChoiceParameters"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"FillValue" {
|
||||||
|
if (Set-AttrPropertyElement $propsEl "FillValue" (Build-FillValueExplicitXml (Get-AttrTypeStrFromXml $propsEl) $changeValue)) {
|
||||||
|
Info "Set $xmlTag '$elemName'.FillValue"; $script:modifyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
default {
|
default {
|
||||||
# Scalar property change (Indexing, FillChecking, Use, etc.)
|
# Scalar property change (Indexing, FillChecking, Use, etc.)
|
||||||
$scalarEl = $null
|
$scalarEl = $null
|
||||||
@@ -2235,7 +2369,23 @@ function Modify-ChildElements($modifyDef, [string]$childType) {
|
|||||||
Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr"
|
Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr"
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
} else {
|
} else {
|
||||||
Warn "$xmlTag '$elemName': property '$changeProp' not found"
|
# create-if-missing: известное свойство создаём в позиции; неизвестное → ошибка (опечатка)
|
||||||
|
if ($script:knownChildProps -notcontains $changeProp) {
|
||||||
|
Write-Error "modify: неизвестное свойство '$changeProp' у $xmlTag '$elemName' (опечатка?)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$valueStr = "$changeValue"
|
||||||
|
if ($changeValue -is [bool]) {
|
||||||
|
$valueStr = if ($changeValue) { "true" } else { "false" }
|
||||||
|
} else {
|
||||||
|
$valueStr = Normalize-EnumValue $changeProp $valueStr
|
||||||
|
}
|
||||||
|
$newNodes = Import-Fragment "<$changeProp>$(Esc-Xml $valueStr)</$changeProp>"
|
||||||
|
if ($newNodes.Count -gt 0) {
|
||||||
|
Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $changeProp
|
||||||
|
Info "Created $xmlTag '$elemName'.$changeProp = $valueStr"
|
||||||
|
$script:modifyCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2271,6 +2421,433 @@ $script:complexPropertyMap = @{
|
|||||||
"RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
"RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
||||||
"BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
"BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
||||||
"InputByString" = @{ tag = "xr:Field"; attr = $null }
|
"InputByString" = @{ tag = "xr:Field"; attr = $null }
|
||||||
|
"DataLockFields" = @{ tag = "xr:Field"; attr = $null; expand = $true }
|
||||||
|
"RegisteredDocuments" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Известные свойства объекта (union по корпусу acc+erp 8.3.24) — allowlist для modify-property.
|
||||||
|
# Известное отсутствующее свойство create-if-missing создаётся; неизвестное (опечатка) → ошибка.
|
||||||
|
$script:knownObjectProps = @(
|
||||||
|
'ActionPeriod','ActionPeriodUse','Addressing','AutoOrderByCode','Autonumbering','AuxiliaryChoiceForm',
|
||||||
|
'AuxiliaryFolderChoiceForm','AuxiliaryFolderForm','AuxiliaryForm','AuxiliaryListForm','AuxiliaryObjectForm',
|
||||||
|
'AuxiliaryRecordForm','AuxiliarySettingsForm','BaseCalculationTypes','BasePeriod','BasedOn',
|
||||||
|
'CharacteristicExtValues','Characteristics','ChartOfAccounts','ChartOfCalculationTypes','CheckUnique',
|
||||||
|
'ChoiceDataGetModeOnInputByString','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput','ChoiceMode',
|
||||||
|
'ChoiceParameterLinks','ChoiceParameters','CodeAllowedLength','CodeLength','CodeMask','CodeSeries','CodeType',
|
||||||
|
'Comment','Correspondence','CreateOnInput','CreateTaskInPrivilegedMode','CurrentPerformer','DataHistory',
|
||||||
|
'DataLockControlMode','DataLockFields','DefaultChoiceForm','DefaultFolderChoiceForm','DefaultFolderForm',
|
||||||
|
'DefaultForm','DefaultListForm','DefaultObjectForm','DefaultPresentation','DefaultRecordForm','DefaultSettingsForm',
|
||||||
|
'DefaultVariantForm','DependenceOnCalculationTypes','DescriptionLength','DistributedInfoBase','EditFormat',
|
||||||
|
'EditType','EnableTotalsSliceFirst','EnableTotalsSliceLast','EnableTotalsSplitting',
|
||||||
|
'ExecuteAfterWriteDataHistoryVersionProcessing','Explanation','ExtDimensionTypes','ExtendedEdit',
|
||||||
|
'ExtendedListPresentation','ExtendedObjectPresentation','ExtendedPresentation','ExtendedRecordPresentation',
|
||||||
|
'FillChecking','FoldersOnTop','Format','FullTextSearch','FullTextSearchOnInputByString','Hierarchical',
|
||||||
|
'HierarchyType','IncludeConfigurationExtensions','IncludeHelpInContents','InformationRegisterPeriodicity',
|
||||||
|
'InputByString','LevelCount','LimitLevelCount','LinkByType','ListPresentation','MainAddressingAttribute',
|
||||||
|
'MainDataCompositionSchema','MainFilterOnPeriod','MarkNegatives','Mask','MaxExtDimensionCount','MaxValue',
|
||||||
|
'MinValue','MultiLine','Name','NumberAllowedLength','NumberLength','NumberPeriodicity','NumberType','Numerator',
|
||||||
|
'ObjectPresentation','OrderLength','Owners','PasswordMode','PeriodAdjustmentLength','Periodicity',
|
||||||
|
'PostInPrivilegedMode','Posting','PredefinedDataUpdate','QuickChoice','RealTimePosting','RecordPresentation',
|
||||||
|
'RegisterRecords','RegisterRecordsDeletion','RegisterRecordsWritingOnPost','RegisterType','RegisteredDocuments',
|
||||||
|
'Schedule','ScheduleDate','ScheduleValue','SearchStringModeOnInputByString','SequenceFilling','SettingsStorage',
|
||||||
|
'StandardAttributes','StandardTabularSections','SubordinationUse','Synonym','Task','TaskNumberAutoPrefix',
|
||||||
|
'ToolTip','Type','UnpostInPrivilegedMode','UpdateDataHistoryImmediatelyAfterWrite','UseStandardCommands',
|
||||||
|
'VariantsStorage','WriteMode'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Известные свойства дочерних элементов (union Attribute/Dimension/Resource по корпусу) — allowlist default-ветки
|
||||||
|
# modify-attribute/-dimension/-resource.
|
||||||
|
$script:knownChildProps = @(
|
||||||
|
'AccountingFlag','Balance','BaseDimension','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput',
|
||||||
|
'ChoiceParameterLinks','ChoiceParameters','Comment','CreateOnInput','DataHistory','DenyIncompleteValues',
|
||||||
|
'DocumentMap','EditFormat','ExtDimensionAccountingFlag','ExtendedEdit','FillChecking','FillFromFillingValue',
|
||||||
|
'FillValue','Format','FullTextSearch','Indexing','LinkByType','MainFilter','MarkNegatives','Mask','Master',
|
||||||
|
'MaxValue','MinValue','MultiLine','Name','PasswordMode','QuickChoice','RegisterRecordsMap','ScheduleLink',
|
||||||
|
'Synonym','ToolTip','Type','Use','UseInTotals'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Канонический порядок свойств реквизита (последовательность Build-AttributeFragment) — для вставки в позицию.
|
||||||
|
# Порядок 1С терпит (cert), но держим канонический для консистентности с meta-compile.
|
||||||
|
$script:attrPropOrder = @(
|
||||||
|
'Name','Synonym','Comment','Type','PasswordMode','Format','EditFormat','ToolTip','MarkNegatives','Mask',
|
||||||
|
'MultiLine','ExtendedEdit','MinValue','MaxValue','FillFromFillingValue','FillValue','FillChecking',
|
||||||
|
'ChoiceFoldersAndItems','ChoiceParameterLinks','ChoiceParameters','QuickChoice','CreateOnInput','ChoiceForm',
|
||||||
|
'LinkByType','ChoiceHistoryOnInput','Use','Indexing','FullTextSearch','DataHistory'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Вставить новый элемент свойства в Properties в канонической позиции (по orderArray); если свойства нет в
|
||||||
|
# orderArray (или orderArray пуст) — append. Порядок 1С терпит, канонический — для консистентности/снапшотов.
|
||||||
|
function Insert-PropertyInOrder($propsEl, $newNode, $orderArray, $propName) {
|
||||||
|
$childIndent = "$(Get-ChildIndent $propsEl)"
|
||||||
|
$refNode = $null
|
||||||
|
$idx = if ($orderArray) { [array]::IndexOf($orderArray, $propName) } else { -1 }
|
||||||
|
if ($idx -ge 0) {
|
||||||
|
foreach ($ch in $propsEl.ChildNodes) {
|
||||||
|
if ($ch.NodeType -eq 'Element') {
|
||||||
|
$ci = [array]::IndexOf($orderArray, $ch.LocalName)
|
||||||
|
if ($ci -gt $idx) { $refNode = $ch; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Insert-BeforeElement $propsEl $newNode $refNode $childIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Заменить существующий элемент свойства реквизита новым фрагментом (по образцу ветки type),
|
||||||
|
# либо создать в канонической позиции, если его нет. Возвращает $true при успехе.
|
||||||
|
function Set-AttrPropertyElement($propsEl, $propName, $fragmentXml) {
|
||||||
|
$newNodes = Import-Fragment $fragmentXml
|
||||||
|
if ($newNodes.Count -eq 0) { return $false }
|
||||||
|
$existing = $null
|
||||||
|
foreach ($ch in $propsEl.ChildNodes) {
|
||||||
|
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $propName) { $existing = $ch; break }
|
||||||
|
}
|
||||||
|
if ($existing) {
|
||||||
|
# InsertBefore+RemoveChild сохраняет ведущий/хвостовой whitespace позиции existing
|
||||||
|
# (InsertAfter+Remove-NodeWithWhitespace склеил бы: удаляет ведущий ws как отдельный узел).
|
||||||
|
$propsEl.InsertBefore($newNodes[0], $existing) | Out-Null
|
||||||
|
$propsEl.RemoveChild($existing) | Out-Null
|
||||||
|
} else {
|
||||||
|
Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $propName
|
||||||
|
}
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# MinValue/MaxValue — типизированное значение (порт Emit-MinMaxValue): nil / xs:string / xs:decimal.
|
||||||
|
function Build-MinMaxValueXml([string]$tag, $val) {
|
||||||
|
if ($null -eq $val -or "$val" -eq '') { return "<$tag xsi:nil=`"true`"/>" }
|
||||||
|
$t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' }
|
||||||
|
return "<$tag xsi:type=`"$t`">$(Esc-Xml "$val")</$tag>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Порт из meta-compile: развёртка путей данных + связи выбора / тип по ссылке (structural modify) ---
|
||||||
|
|
||||||
|
# Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null).
|
||||||
|
function Get-ChElProp($obj, [string[]]$names) {
|
||||||
|
if ($null -eq $obj) { return $null }
|
||||||
|
foreach ($n in $names) {
|
||||||
|
if ($obj -is [System.Collections.IDictionary]) { if ($obj.Contains($n)) { return $obj[$n] } }
|
||||||
|
elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { return $obj.PSObject.Properties[$n].Value }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартный реквизит рус/англ → английский (для Catalog/Document); использует существующие reserved-карты.
|
||||||
|
function Resolve-StdAttrEn([string]$name) {
|
||||||
|
$ctx = switch ("$script:objType") { 'Catalog' { 'catalog' } 'Document' { 'document' } default { $null } }
|
||||||
|
if (-not $ctx) { return $null }
|
||||||
|
$stdSet = $script:reservedByContext[$ctx]
|
||||||
|
foreach ($en in $stdSet) {
|
||||||
|
$ru = $script:reservedAttrNames[$en]
|
||||||
|
if (($name -ieq $en) -or ($ru -and $name -ieq $ru)) { return $en }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Прощающий ввод пути данных: короткое имя реквизита → полный путь объекта (порт Expand-DataPath).
|
||||||
|
function Expand-DataPath([string]$dp) {
|
||||||
|
if (-not $dp) { return $dp }
|
||||||
|
$s = "$dp"
|
||||||
|
if ($s -match '[:/]') { return $s }
|
||||||
|
if ($s -match '^-?\d+$') { return $s }
|
||||||
|
if ($s -match '^(StandardAttribute|Attribute)\.') { return "$($script:objType).$($script:objName).$s" }
|
||||||
|
if (-not $s.Contains('.')) {
|
||||||
|
$en = Resolve-StdAttrEn $s
|
||||||
|
if ($en) { return "$($script:objType).$($script:objName).StandardAttribute.$en" }
|
||||||
|
return "$($script:objType).$($script:objName).Attribute.$s"
|
||||||
|
}
|
||||||
|
return $s
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shorthand "name=path" | "name=path:Clear|DontChange" → {name, dataPath, valueChange?}.
|
||||||
|
function ConvertFrom-ChLinkShorthand([string]$s) {
|
||||||
|
$eq = $s.IndexOf('=')
|
||||||
|
if ($eq -lt 0) { return @{ name = $s.Trim() } }
|
||||||
|
$o = @{ name = $s.Substring(0, $eq).Trim() }; $rest = $s.Substring($eq + 1).Trim()
|
||||||
|
if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] }
|
||||||
|
else { $o['dataPath'] = $rest }
|
||||||
|
return $o
|
||||||
|
}
|
||||||
|
|
||||||
|
# LinkByType — {dataPath, linkItem?} (порт Emit-LinkByType). Строка → dataPath, linkItem=0.
|
||||||
|
function Build-LinkByTypeXml([string]$indent, $spec) {
|
||||||
|
if (-not $spec) { return "$indent<LinkByType/>" }
|
||||||
|
if ($spec -is [string]) { $dp = "$spec"; $li = 0 }
|
||||||
|
else {
|
||||||
|
$dp = "$(Get-ChElProp $spec @('dataPath','path','путь'))"
|
||||||
|
$liRaw = Get-ChElProp $spec @('linkItem','элементСвязи')
|
||||||
|
$li = if ($null -ne $liRaw) { $liRaw } else { 0 }
|
||||||
|
}
|
||||||
|
if (-not $dp) { return "$indent<LinkByType/>" }
|
||||||
|
$dp = Expand-DataPath $dp
|
||||||
|
$lines = @(
|
||||||
|
"$indent<LinkByType>"
|
||||||
|
"$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>"
|
||||||
|
"$indent`t<xr:LinkItem>$li</xr:LinkItem>"
|
||||||
|
"$indent</LinkByType>"
|
||||||
|
)
|
||||||
|
return $lines -join "`r`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ChoiceParameterLinks — [{name, dataPath, valueChange?}] (порт Emit-ChoiceParameterLinks). valueChange дефолт Clear.
|
||||||
|
function Build-ChoiceParameterLinksXml([string]$indent, $cpl) {
|
||||||
|
if (-not $cpl -or @($cpl).Count -eq 0) { return "$indent<ChoiceParameterLinks/>" }
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.Append("$indent<ChoiceParameterLinks>") | Out-Null
|
||||||
|
foreach ($lk in @($cpl)) {
|
||||||
|
if ($lk -is [string]) { $lk = ConvertFrom-ChLinkShorthand $lk }
|
||||||
|
$name = Get-ChElProp $lk @('name','имя')
|
||||||
|
$dp = Expand-DataPath (Get-ChElProp $lk @('dataPath','path','путь'))
|
||||||
|
$vcRaw = Get-ChElProp $lk @('valueChange','режимИзменения')
|
||||||
|
$vc = 'Clear'
|
||||||
|
if ($vcRaw) {
|
||||||
|
$vc = switch -Regex ("$vcRaw".ToLower()) {
|
||||||
|
'^(clear|очистить|очистка)$' { 'Clear'; break }
|
||||||
|
'^(dontchange|неизменять|неменять|нет)$' { 'DontChange'; break }
|
||||||
|
default { "$vcRaw" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sb.Append("`r`n$indent`t<xr:Link>") | Out-Null
|
||||||
|
$sb.Append("`r`n$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>") | Out-Null
|
||||||
|
$sb.Append("`r`n$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>") | Out-Null
|
||||||
|
$sb.Append("`r`n$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>") | Out-Null
|
||||||
|
$sb.Append("`r`n$indent`t</xr:Link>") | Out-Null
|
||||||
|
}
|
||||||
|
$sb.Append("`r`n$indent</ChoiceParameterLinks>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Порт из meta-compile: значения параметров выбора (ChoiceParameters) ---
|
||||||
|
|
||||||
|
$script:fillRefRoots = @{
|
||||||
|
'перечисление'='Enum'; 'справочник'='Catalog'; 'документ'='Document';
|
||||||
|
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
|
||||||
|
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
|
||||||
|
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
|
||||||
|
'enum'='Enum'; 'catalog'='Catalog'; 'document'='Document'; 'chartofaccounts'='ChartOfAccounts';
|
||||||
|
'chartofcharacteristictypes'='ChartOfCharacteristicTypes'; 'chartofcalculationtypes'='ChartOfCalculationTypes';
|
||||||
|
'exchangeplan'='ExchangePlan'; 'businessprocess'='BusinessProcess'; 'task'='Task'
|
||||||
|
}
|
||||||
|
$script:fillEmptyRefWords = @('emptyref','пустаяссылка')
|
||||||
|
$script:fillEnumValWords = @('enumvalue','значениеперечисления')
|
||||||
|
$script:accountTypeValues = @('Active','Passive','ActivePassive')
|
||||||
|
$script:fillRefKindRoot = @{
|
||||||
|
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
|
||||||
|
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
|
||||||
|
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
|
||||||
|
'businessprocessref'='BusinessProcess'; 'taskref'='Task'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ChScalar([string]$s) {
|
||||||
|
$t = "$s".Trim()
|
||||||
|
if ($t -match '^(?i:true|истина)$') { return $true }
|
||||||
|
if ($t -match '^(?i:false|ложь)$') { return $false }
|
||||||
|
if ($t -match '^-?\d+$') { return [int]$t }
|
||||||
|
if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
|
||||||
|
return $t
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-FillNum($n) {
|
||||||
|
if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) }
|
||||||
|
return "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Normalize-FillRef([string]$s) {
|
||||||
|
if ([string]::IsNullOrEmpty($s)) { return $null }
|
||||||
|
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$') { return $s }
|
||||||
|
$parts = $s -split '\.'
|
||||||
|
if ($parts.Count -lt 2) { return $null }
|
||||||
|
$root = $script:fillRefRoots[$parts[0].ToLower()]
|
||||||
|
if (-not $root) { return $null }
|
||||||
|
$typeName = $parts[1]
|
||||||
|
if ($root -eq 'Enum') {
|
||||||
|
if ($parts.Count -eq 2) { return $null }
|
||||||
|
if ($parts.Count -eq 3) {
|
||||||
|
if ($script:fillEmptyRefWords -contains $parts[2].ToLower()) { return "Enum.$typeName.EmptyRef" }
|
||||||
|
return "Enum.$typeName.EnumValue.$($parts[2])"
|
||||||
|
}
|
||||||
|
$member = $parts[2]
|
||||||
|
if ($script:fillEnumValWords -contains $member.ToLower()) { $rest = $parts[3..($parts.Count-1)] -join '.' }
|
||||||
|
else { $rest = $parts[2..($parts.Count-1)] -join '.' }
|
||||||
|
return "Enum.$typeName.EnumValue.$rest"
|
||||||
|
}
|
||||||
|
$tail = @($parts[1..($parts.Count-1)])
|
||||||
|
for ($i = 0; $i -lt $tail.Count; $i++) {
|
||||||
|
if ($script:fillEmptyRefWords -contains $tail[$i].ToLower()) { $tail[$i] = 'EmptyRef' }
|
||||||
|
}
|
||||||
|
return "$root." + ($tail -join '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Expand-ChoiceRefValue([string]$value, [string]$typeStr) {
|
||||||
|
if (-not $typeStr) { return $null }
|
||||||
|
$t = Resolve-TypeStr $typeStr
|
||||||
|
$root = $null; $tn = $null
|
||||||
|
if ($t -match '^(\w+Ref)\.(.+)$') { $root = $script:fillRefKindRoot[$Matches[1].ToLower()]; $tn = $Matches[2] }
|
||||||
|
elseif ($t -match '^([^.]+)\.(.+)$') { $root = $script:fillRefRoots[$Matches[1].ToLower()]; $tn = $Matches[2] }
|
||||||
|
if (-not $root) { return $null }
|
||||||
|
if ($script:fillEmptyRefWords -contains "$value".ToLower()) { return "$root.$tn.EmptyRef" }
|
||||||
|
if ($root -eq 'Enum') { return "Enum.$tn.EnumValue.$value" }
|
||||||
|
return "$root.$tn.$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Normalize-ChoiceValue($value) {
|
||||||
|
if ($value -is [bool]) { return @{ XsiType='xs:boolean'; Text=$(if ($value) { 'true' } else { 'false' }) } }
|
||||||
|
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
|
||||||
|
return @{ XsiType='xs:decimal'; Text=(Format-FillNum $value) }
|
||||||
|
}
|
||||||
|
$s = "$value"
|
||||||
|
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
|
||||||
|
if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { return @{ XsiType='xs:dateTime'; Text=$s } }
|
||||||
|
$ref = Normalize-FillRef $s
|
||||||
|
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
|
||||||
|
if ($script:accountTypeValues -contains $s) { return @{ XsiType='ent:AccountType'; Text=$s } }
|
||||||
|
return @{ XsiType='xs:string'; Text=$s }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Normalize-ChoiceValueT($value, [string]$typeStr) {
|
||||||
|
if ($typeStr -and ($value -is [string]) -and (-not "$value".Contains('.'))) {
|
||||||
|
$ex = Expand-ChoiceRefValue "$value" $typeStr
|
||||||
|
if ($ex) { return @{ XsiType='xr:DesignTimeRef'; Text=$ex } }
|
||||||
|
}
|
||||||
|
return Normalize-ChoiceValue $value
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-ChParamShorthand([string]$s) {
|
||||||
|
$eq = $s.IndexOf('=')
|
||||||
|
if ($eq -lt 0) { return @{ name = $s.Trim() } }
|
||||||
|
$name = $s.Substring(0, $eq).Trim(); $rest = $s.Substring($eq + 1)
|
||||||
|
if ($rest -match ',') {
|
||||||
|
$vals = @(); foreach ($p in ($rest -split ',')) { $vals += ,(ConvertTo-ChScalar $p) }
|
||||||
|
return @{ name = $name; value = $vals }
|
||||||
|
}
|
||||||
|
return @{ name = $name; value = (ConvertTo-ChScalar $rest) }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ChoiceParameters — [{name, type?, value?}] (порт Emit-ChoiceParameters). Значение на app:value (xsi:type=тип);
|
||||||
|
# массив → v8:FixedArray с v8:Value; без value → nil. Требует xmlns:app в Import-Fragment.
|
||||||
|
function Build-ChoiceParametersXml([string]$indent, $cp) {
|
||||||
|
if (-not $cp -or @($cp).Count -eq 0) { return "$indent<ChoiceParameters/>" }
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.Append("$indent<ChoiceParameters>") | Out-Null
|
||||||
|
foreach ($item in @($cp)) {
|
||||||
|
if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
|
||||||
|
$name = Get-ChElProp $item @('name','имя')
|
||||||
|
$ptype = Get-ChElProp $item @('type','тип')
|
||||||
|
$hasVal = $false; $val = $null
|
||||||
|
if ($item -is [System.Collections.IDictionary]) {
|
||||||
|
if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
|
||||||
|
elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
|
||||||
|
} elseif ($item.PSObject) {
|
||||||
|
if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
|
||||||
|
elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
|
||||||
|
}
|
||||||
|
$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
|
||||||
|
$sb.Append("`r`n$indent`t<app:item name=`"$(Esc-Xml "$name")`">") | Out-Null
|
||||||
|
if (-not $hasVal) {
|
||||||
|
$sb.Append("`r`n$indent`t`t<app:value xsi:nil=`"true`"/>") | Out-Null
|
||||||
|
} elseif ($valIsArray) {
|
||||||
|
$sb.Append("`r`n$indent`t`t<app:value xsi:type=`"v8:FixedArray`">") | Out-Null
|
||||||
|
foreach ($v in $val) {
|
||||||
|
$norm = Normalize-ChoiceValueT $v $ptype
|
||||||
|
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
|
||||||
|
else { $sb.Append("`r`n$indent`t`t`t<v8:Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</v8:Value>") | Out-Null }
|
||||||
|
}
|
||||||
|
$sb.Append("`r`n$indent`t`t</app:value>") | Out-Null
|
||||||
|
} else {
|
||||||
|
$norm = Normalize-ChoiceValueT $val $ptype
|
||||||
|
if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`"/>") | Out-Null }
|
||||||
|
else { $sb.Append("`r`n$indent`t`t<app:value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</app:value>") | Out-Null }
|
||||||
|
}
|
||||||
|
$sb.Append("`r`n$indent`t</app:item>") | Out-Null
|
||||||
|
}
|
||||||
|
$sb.Append("`r`n$indent</ChoiceParameters>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Порт из meta-compile: явное значение заполнения (FillValue) ---
|
||||||
|
|
||||||
|
$script:fillBoolTrue = @('true','истина','да')
|
||||||
|
$script:fillBoolFalse = @('false','ложь','нет')
|
||||||
|
|
||||||
|
function Esc-XmlText([string]$s) { return $s.Replace('&','&').Replace('<','<').Replace('>','>') }
|
||||||
|
|
||||||
|
function Get-FillTypeCategory([string]$typeStr) {
|
||||||
|
if (-not $typeStr) { return 'String' }
|
||||||
|
if ($typeStr -match '\+') { return 'Other' }
|
||||||
|
$t = Resolve-TypeStr $typeStr
|
||||||
|
if ($t -match '^Boolean$') { return 'Boolean' }
|
||||||
|
if ($t -match '^String(\(|$)') { return 'String' }
|
||||||
|
if ($t -match '^Number(\(|$)') { return 'Number' }
|
||||||
|
if ($t -match '^(Date|DateTime)$') { return 'Date' }
|
||||||
|
return 'Other'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Expand-FillShortRef([string]$s, [string]$typeStr) {
|
||||||
|
if (-not $typeStr) { return $null }
|
||||||
|
if ($typeStr -match '\+') { return $null }
|
||||||
|
$t = Resolve-TypeStr $typeStr
|
||||||
|
if ($t -notmatch '^(\w+Ref)\.(.+)$') { return $null }
|
||||||
|
$root = $script:fillRefKindRoot[$Matches[1].ToLower()]
|
||||||
|
if (-not $root) { return $null }
|
||||||
|
$typeName = $Matches[2]
|
||||||
|
if ($script:fillEmptyRefWords -contains $s.ToLower()) { return "$root.$typeName.EmptyRef" }
|
||||||
|
if ($root -eq 'Enum') { return "Enum.$typeName.EnumValue.$s" }
|
||||||
|
return "$root.$typeName.$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-FillValueSpec([string]$s, [string]$typeStr) {
|
||||||
|
$cat = Get-FillTypeCategory $typeStr
|
||||||
|
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
|
||||||
|
if ($cat -eq 'String') { return @{ XsiType='xs:string'; Text=$s } }
|
||||||
|
if ($cat -eq 'Boolean' -or ($script:fillBoolTrue -contains $s.ToLower()) -or ($script:fillBoolFalse -contains $s.ToLower())) {
|
||||||
|
if ($script:fillBoolTrue -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='true' } }
|
||||||
|
if ($script:fillBoolFalse -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='false' } }
|
||||||
|
}
|
||||||
|
if ($cat -eq 'Number') { return @{ XsiType='xs:decimal'; Text=$s } }
|
||||||
|
if ($cat -eq 'Date' -or $s -match '^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$') {
|
||||||
|
if ($s -match '^\d{4}-\d{2}-\d{2}$') { $s = "${s}T00:00:00" }
|
||||||
|
return @{ XsiType='xs:dateTime'; Text=$s }
|
||||||
|
}
|
||||||
|
$ref = Normalize-FillRef $s
|
||||||
|
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
|
||||||
|
$short = Expand-FillShortRef $s $typeStr
|
||||||
|
if ($short) { return @{ XsiType='xr:DesignTimeRef'; Text=$short } }
|
||||||
|
return @{ XsiType='xs:string'; Text=$s }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Извлечь тип реквизита из XML (<Type>/<v8:Type>) → DSL-typeStr для категоризации FillValue.
|
||||||
|
function Get-AttrTypeStrFromXml($propsEl) {
|
||||||
|
$typeEl = $null
|
||||||
|
foreach ($ch in $propsEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') { $typeEl = $ch; break } }
|
||||||
|
if (-not $typeEl) { return "" }
|
||||||
|
$mapped = @()
|
||||||
|
foreach ($ch in $typeEl.ChildNodes) {
|
||||||
|
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') {
|
||||||
|
$t = $ch.InnerText.Trim()
|
||||||
|
$colon = $t.IndexOf(':'); if ($colon -ge 0) { $t = $t.Substring($colon + 1) }
|
||||||
|
switch -Regex ($t) {
|
||||||
|
'^string$' { $mapped += 'String'; break }
|
||||||
|
'^decimal$' { $mapped += 'Number'; break }
|
||||||
|
'^boolean$' { $mapped += 'Boolean'; break }
|
||||||
|
'^dateTime$' { $mapped += 'Date'; break }
|
||||||
|
default { $mapped += $t }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($mapped.Count -eq 0) { return "" }
|
||||||
|
if ($mapped.Count -gt 1) { return ($mapped -join ' + ') }
|
||||||
|
return $mapped[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
# FillValue — явное значение (порт Emit-FillValue, ветка hasSpec). Маркеры {nil}/{emptyRef}; иначе по типу.
|
||||||
|
function Build-FillValueExplicitXml([string]$typeStr, $spec) {
|
||||||
|
if ($null -eq $spec) { return "<FillValue xsi:nil=`"true`"/>" }
|
||||||
|
if ($spec -is [bool]) { return "<FillValue xsi:type=`"xs:boolean`">$(if ($spec) { 'true' } else { 'false' })</FillValue>" }
|
||||||
|
if ($spec -is [int] -or $spec -is [long] -or $spec -is [double] -or $spec -is [decimal]) { return "<FillValue xsi:type=`"xs:decimal`">$(Format-FillNum $spec)</FillValue>" }
|
||||||
|
if ((Get-ChElProp $spec @('nil')) -eq $true) { return "<FillValue xsi:nil=`"true`"/>" }
|
||||||
|
if ((Get-ChElProp $spec @('emptyRef','пустаяссылка')) -eq $true) { return "<FillValue xsi:type=`"xr:DesignTimeRef`"/>" }
|
||||||
|
$r = Resolve-FillValueSpec "$spec" $typeStr
|
||||||
|
if ($r.Text -eq '' -and $r.XsiType -eq 'xs:string') { return "<FillValue xsi:type=`"xs:string`"/>" }
|
||||||
|
return "<FillValue xsi:type=`"$($r.XsiType)`">$(Esc-XmlText $r.Text)</FillValue>"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Find-PropertyElement([string]$propName) {
|
function Find-PropertyElement([string]$propName) {
|
||||||
@@ -2295,6 +2872,7 @@ function Get-ComplexPropertyValues([System.Xml.XmlElement]$propEl) {
|
|||||||
function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
||||||
$mapEntry = $script:complexPropertyMap[$propertyName]
|
$mapEntry = $script:complexPropertyMap[$propertyName]
|
||||||
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
||||||
|
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
|
||||||
|
|
||||||
$propEl = Find-PropertyElement $propertyName
|
$propEl = Find-PropertyElement $propertyName
|
||||||
if (-not $propEl) {
|
if (-not $propEl) {
|
||||||
@@ -2342,6 +2920,8 @@ function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
||||||
|
$mapEntry = $script:complexPropertyMap[$propertyName]
|
||||||
|
if ($mapEntry -and $mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
|
||||||
$propEl = Find-PropertyElement $propertyName
|
$propEl = Find-PropertyElement $propertyName
|
||||||
if (-not $propEl) {
|
if (-not $propEl) {
|
||||||
Warn "Property element '$propertyName' not found in Properties"
|
Warn "Property element '$propertyName' not found in Properties"
|
||||||
@@ -2379,6 +2959,7 @@ function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
|||||||
function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
|
function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
|
||||||
$mapEntry = $script:complexPropertyMap[$propertyName]
|
$mapEntry = $script:complexPropertyMap[$propertyName]
|
||||||
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
||||||
|
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
|
||||||
|
|
||||||
$propEl = Find-PropertyElement $propertyName
|
$propEl = Find-PropertyElement $propertyName
|
||||||
if (-not $propEl) {
|
if (-not $propEl) {
|
||||||
@@ -2428,6 +3009,95 @@ function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
|
|||||||
# Section 13: Main processing
|
# Section 13: Main processing
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Predefined data (Ext/Predefined.xml) — add предопределённых (Catalog/ChartOfCharacteristicTypes).
|
||||||
|
# Существующие <Item id=GUID> сохраняются побайтово (текстовый append), новые получают свежий GUID —
|
||||||
|
# инвариант «не менять id существующей сущности».
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
$script:predefXsiTypeByObj = @{
|
||||||
|
'Catalog' = 'CatalogPredefinedItems'
|
||||||
|
'ChartOfCharacteristicTypes' = 'PlanOfCharacteristicKindPredefinedItems'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PredefinedPath {
|
||||||
|
$objDir = Join-Path (Split-Path $resolvedPath) $script:objName
|
||||||
|
return (Join-Path (Join-Path $objDir "Ext") "Predefined.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ObjectCodeType {
|
||||||
|
foreach ($ch in $script:propertiesEl.ChildNodes) {
|
||||||
|
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'CodeType') { return $ch.InnerText.Trim() }
|
||||||
|
}
|
||||||
|
return 'String'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект {name,code,description,isFolder,childItems}.
|
||||||
|
function Resolve-PredefItem($val) {
|
||||||
|
if ($val -is [string]) {
|
||||||
|
$s = "$val"; $descRaw = $null; $hasDesc = $false
|
||||||
|
if ($s -match '\[(.*)\]') { $descRaw = $Matches[1]; $hasDesc = $true; $s = $s -replace '\s*\[.*\]', '' }
|
||||||
|
$m = [regex]::Match($s.Trim(), '^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$')
|
||||||
|
$name = $m.Groups[2].Value
|
||||||
|
$code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' }
|
||||||
|
$desc = if ($hasDesc) { $descRaw } else { Split-CamelCase $name }
|
||||||
|
return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @() }
|
||||||
|
}
|
||||||
|
$gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null }
|
||||||
|
$name = "$(& $gv $val @('name','имя'))"
|
||||||
|
$codeV = & $gv $val @('code','код'); $code = if ($null -ne $codeV) { "$codeV" } else { '' }
|
||||||
|
$hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование']
|
||||||
|
$descV = & $gv $val @('description','наименование')
|
||||||
|
$desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name }
|
||||||
|
$isFolder = ((& $gv $val @('isFolder','группа')) -eq $true)
|
||||||
|
$subs = & $gv $val @('childItems','подчиненные')
|
||||||
|
return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { @($subs) } else { @() }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-PredefItemXml([string]$indent, $val, [string]$codeType) {
|
||||||
|
$r = Resolve-PredefItem $val
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
[void]$sb.Append("$indent<Item id=`"$(New-Guid-String)`">`r`n")
|
||||||
|
[void]$sb.Append("$indent`t<Name>$(Esc-XmlText $r.name)</Name>`r`n")
|
||||||
|
if (-not $r.code) { [void]$sb.Append("$indent`t<Code/>`r`n") }
|
||||||
|
elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t<Code xsi:type=`"xs:decimal`">$(Esc-XmlText $r.code)</Code>`r`n") }
|
||||||
|
else { [void]$sb.Append("$indent`t<Code>$(Esc-XmlText $r.code)</Code>`r`n") }
|
||||||
|
if ($r.desc -eq '') { [void]$sb.Append("$indent`t<Description/>`r`n") }
|
||||||
|
else { [void]$sb.Append("$indent`t<Description>$(Esc-XmlText $r.desc)</Description>`r`n") }
|
||||||
|
[void]$sb.Append("$indent`t<IsFolder>$(if ($r.isFolder) { 'true' } else { 'false' })</IsFolder>`r`n")
|
||||||
|
if ($r.children.Count -gt 0) {
|
||||||
|
[void]$sb.Append("$indent`t<ChildItems>`r`n")
|
||||||
|
foreach ($c in $r.children) { [void]$sb.Append((Build-PredefItemXml "$indent`t`t" $c $codeType)) }
|
||||||
|
[void]$sb.Append("$indent`t</ChildItems>`r`n")
|
||||||
|
}
|
||||||
|
[void]$sb.Append("$indent</Item>`r`n")
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-PredefinedItems($items) {
|
||||||
|
$xsiType = $script:predefXsiTypeByObj[$script:objType]
|
||||||
|
if (-not $xsiType) { Write-Error "add-predefined: тип объекта '$($script:objType)' не поддержан (только Catalog, ChartOfCharacteristicTypes)"; exit 1 }
|
||||||
|
$codeType = Get-ObjectCodeType
|
||||||
|
$version = $script:xmlDoc.DocumentElement.GetAttribute("version")
|
||||||
|
$path = Get-PredefinedPath
|
||||||
|
$itemsXml = ""
|
||||||
|
foreach ($it in @($items)) { $itemsXml += (Build-PredefItemXml "`t" $it $codeType) }
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
if (Test-Path $path) {
|
||||||
|
$text = [System.IO.File]::ReadAllText($path, $utf8Bom)
|
||||||
|
$text = $text.Replace("</PredefinedData>", "$itemsXml</PredefinedData>")
|
||||||
|
} else {
|
||||||
|
$extDir = Split-Path $path
|
||||||
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
|
$hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" 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`" xsi:type=`"$xsiType`" version=`"$version`">`r`n"
|
||||||
|
$text = "$hdr$itemsXml</PredefinedData>`r`n"
|
||||||
|
}
|
||||||
|
[System.IO.File]::WriteAllText($path, $text, $utf8Bom)
|
||||||
|
$n = @($items).Count
|
||||||
|
Info "Added $n predefined item(s) → $path"
|
||||||
|
$script:addCount += $n
|
||||||
|
}
|
||||||
|
|
||||||
# --- Inline mode conversion ---
|
# --- Inline mode conversion ---
|
||||||
if ($Operation) {
|
if ($Operation) {
|
||||||
$def = Convert-InlineToDefinition $Operation $Value
|
$def = Convert-InlineToDefinition $Operation $Value
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user