mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-08 04:30:19 +03:00
Compare commits
89
Commits
@@ -1,4 +1,4 @@
|
||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -154,6 +167,11 @@ $script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
$script:xmlDoc.Load($resolvedPath)
|
||||
|
||||
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
|
||||
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
|
||||
|
||||
$script:addCount = 0
|
||||
$script:removeCount = 0
|
||||
$script:modifyCount = 0
|
||||
@@ -851,7 +869,7 @@ function Do-SetHomePage($valArg) {
|
||||
|
||||
$hpXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
||||
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
||||
$leftXml
|
||||
$rightXml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -307,13 +324,49 @@ def parse_batch_value(val):
|
||||
return items
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
@@ -357,6 +410,10 @@ def main():
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||
format_version = xml_root.get('version') or '2.17'
|
||||
|
||||
add_count = 0
|
||||
remove_count = 0
|
||||
modify_count = 0
|
||||
@@ -906,7 +963,7 @@ def main():
|
||||
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
|
||||
f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">\r\n'
|
||||
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
||||
f'{left_xml}\r\n'
|
||||
f'{right_xml}\r\n'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -7,7 +7,12 @@ param(
|
||||
[string]$OutputDir = "src",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24"
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||
# совместимости она не зависит: 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный —
|
||||
# 2.17 читается всеми поддерживаемыми платформами.
|
||||
[ValidateSet("2.17", "2.20", "2.21")]
|
||||
[string]$FormatVersion = "2.17"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -73,7 +78,7 @@ $versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
@@ -175,7 +180,7 @@ $cfgXml = @"
|
||||
# --- Languages/Русский.xml ---
|
||||
$langXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<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">
|
||||
<Properties>
|
||||
<Name>Русский</Name>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration."""
|
||||
import sys, os, argparse, uuid
|
||||
@@ -24,6 +24,9 @@ def main():
|
||||
parser.add_argument('-Version', dest='Version', default='')
|
||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
||||
# 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми платформами.
|
||||
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', choices=['2.17', '2.20', '2.21'])
|
||||
args = parser.parse_args()
|
||||
|
||||
name = args.Name
|
||||
@@ -96,7 +99,7 @@ def main():
|
||||
\t\t\t</xr:ContainedObject>\n"""
|
||||
|
||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||
\t<Configuration uuid="{uuid_cfg}">
|
||||
\t\t<InternalInfo>
|
||||
{contained_objects}\t\t</InternalInfo>
|
||||
@@ -168,7 +171,7 @@ def main():
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<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="{args.FormatVersion}">
|
||||
\t<Language uuid="{uuid_lang}">
|
||||
\t\t<Properties>
|
||||
\t\t\t<Name>Русский</Name>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cfe-borrow v1.8 — 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
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-borrow v1.8 — 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
|
||||
|
||||
import argparse
|
||||
@@ -349,13 +349,49 @@ def expand_self_closing(container, parent_indent):
|
||||
container.text = "\r\n" + parent_indent
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: cfe-patch-method
|
||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -10,22 +10,31 @@ allowed-tools:
|
||||
|
||||
# /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` | Путь к расширению (обязат.) | — |
|
||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
||||
| `Context` | Директива контекста | `НаСервере` |
|
||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|
||||
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
|
||||
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
|
||||
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
|
||||
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
|
||||
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
|
||||
|
||||
## Формат ModulePath
|
||||
|
||||
@@ -40,39 +49,97 @@ allowed-tools:
|
||||
|
||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||
|
||||
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
|
||||
|
||||
## Типы перехвата
|
||||
|
||||
| InterceptorType | Декоратор | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
||||
| `After` | `&После` | Код после вызова оригинального метода |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
||||
| InterceptorType | Декоратор | Назначение | Применим к |
|
||||
|-----------------|-----------|------------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
|
||||
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
|
||||
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
|
||||
|
||||
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
|
||||
|
||||
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
|
||||
|
||||
- **Добавляешь код** → оберни его `#Вставка` … `#КонецВставки`.
|
||||
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление` … `#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
|
||||
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
|
||||
|
||||
Пример:
|
||||
```bsl
|
||||
&ИзменениеИКонтроль("ПриЗаписи")
|
||||
Процедура Расш_ПриЗаписи(Отказ)
|
||||
СуммаДокумента = РассчитатьСумму();
|
||||
#Вставка
|
||||
// доработка: округляем
|
||||
СуммаДокумента = Окр(СуммаДокумента, 2);
|
||||
#КонецВставки
|
||||
#Удаление
|
||||
Записать();
|
||||
#КонецУдаления
|
||||
#Вставка
|
||||
ЗаписатьСПроверкой(Отказ);
|
||||
#КонецВставки
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Правила:
|
||||
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||
|
||||
## Актуализация
|
||||
|
||||
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
|
||||
|
||||
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
|
||||
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
|
||||
|
||||
Статусы в выводе:
|
||||
|
||||
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
|
||||
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
|
||||
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
|
||||
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
|
||||
|
||||
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
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
|
||||
# Перехват &Перед на сервере
|
||||
... -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
|
||||
&НаСервере
|
||||
&Перед("ПриЗаписи")
|
||||
Процедура Расш1_ПриЗаписи()
|
||||
// TODO: код перед вызовом оригинального метода
|
||||
КонецПроцедуры
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
# --- 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 ---
|
||||
& $finalize
|
||||
|
||||
|
||||
@@ -885,6 +885,21 @@ def main():
|
||||
elif check13_ok:
|
||||
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 ---
|
||||
r.finalize(out_file)
|
||||
sys.exit(1 if r.errors > 0 else 0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-create v1.6 — Create 1C information base
|
||||
# db-create v1.7 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -138,6 +138,14 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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 ---
|
||||
@@ -177,8 +185,12 @@ try {
|
||||
$__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
|
||||
}
|
||||
@@ -221,12 +233,18 @@ try {
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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 ($InfoBaseServer -and $InfoBaseRef) {
|
||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-create v1.6 — Create 1C information base
|
||||
# db-create v1.7 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -78,6 +78,13 @@ def resolve_v8path(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 "
|
||||
@@ -145,15 +152,25 @@ def main():
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||
@@ -196,11 +213,23 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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 args.InfoBaseServer and args.InfoBaseRef:
|
||||
if is_server:
|
||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-cf v1.6 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -76,6 +76,13 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -147,6 +154,13 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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 ---
|
||||
@@ -183,12 +197,16 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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
|
||||
}
|
||||
@@ -224,13 +242,18 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-cf v1.6 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,21 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -150,17 +165,23 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||
@@ -194,7 +215,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -203,8 +224,14 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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:
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-dt v1.5 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -60,6 +60,13 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -131,6 +138,13 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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 ---
|
||||
@@ -163,12 +177,16 @@ try {
|
||||
$arguments += "$OutputFile"
|
||||
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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
|
||||
}
|
||||
@@ -197,13 +215,18 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-dt v1.5 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,21 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -143,17 +158,23 @@ def main():
|
||||
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)}")
|
||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||
@@ -181,7 +202,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -190,8 +211,14 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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:
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-xml v1.8 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -99,6 +99,13 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -170,6 +177,13 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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 ---
|
||||
@@ -224,12 +238,16 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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
|
||||
}
|
||||
@@ -293,14 +311,19 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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) {
|
||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
||||
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 {
|
||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-xml v1.8 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,21 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -181,17 +196,23 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||
@@ -248,7 +269,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -257,9 +278,15 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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:
|
||||
print("Dump completed successfully")
|
||||
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:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-cf v1.6 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -76,6 +76,30 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -183,14 +207,14 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
if ($output) { Write-Host ($output | Out-String) }
|
||||
exit $exitCode
|
||||
@@ -224,7 +248,7 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
@@ -232,7 +256,7 @@ try {
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||
} 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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-cf v1.6 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,39 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -150,12 +183,12 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
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})", file=sys.stderr)
|
||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
@@ -194,7 +227,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -206,7 +239,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||
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):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-dt v1.5 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -73,6 +73,30 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -177,14 +201,14 @@ try {
|
||||
$arguments += "$InputFile"
|
||||
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
if ($output) { Write-Host ($output | Out-String) }
|
||||
exit $exitCode
|
||||
@@ -213,7 +237,7 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
@@ -221,7 +245,7 @@ try {
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-dt v1.5 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,39 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -147,12 +180,12 @@ def main():
|
||||
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)}")
|
||||
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})", file=sys.stderr)
|
||||
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:
|
||||
@@ -189,7 +222,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -201,7 +234,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Information base restored successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-git v1.11 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -108,6 +108,30 @@ param(
|
||||
$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 ---
|
||||
function Get-ObjectXmlFromSubFile {
|
||||
param([string]$RelativePath)
|
||||
@@ -372,12 +396,12 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
if ($output) { Write-Host ($output | Out-String) }
|
||||
exit $exitCode
|
||||
}
|
||||
@@ -388,14 +412,14 @@ try {
|
||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||
if ($Password) { $applyArgs += "--password=$Password" }
|
||||
$applyArgs += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||
}
|
||||
@@ -446,7 +470,7 @@ try {
|
||||
# --- Execute ---
|
||||
Write-Host ""
|
||||
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
|
||||
$exitCode = $process.ExitCode
|
||||
@@ -456,7 +480,7 @@ try {
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} 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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-git v1.11 — 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
|
||||
|
||||
import argparse
|
||||
@@ -121,6 +121,39 @@ def run_git(config_dir, git_args):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -307,10 +340,10 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
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})", file=sys.stderr)
|
||||
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
@@ -327,13 +360,13 @@ def main():
|
||||
if args.Password:
|
||||
apply_args.append(f"--password={args.Password}")
|
||||
apply_args.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
||||
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})", file=sys.stderr)
|
||||
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:
|
||||
@@ -382,7 +415,7 @@ def main():
|
||||
# --- Execute ---
|
||||
print("")
|
||||
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(
|
||||
[v8path] + arguments,
|
||||
@@ -396,7 +429,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
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):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-xml v1.12 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -108,6 +108,30 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -244,12 +268,12 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
if ($output) { Write-Host ($output | Out-String) }
|
||||
exit $exitCode
|
||||
}
|
||||
@@ -261,14 +285,14 @@ try {
|
||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||
if ($Password) { $applyArgs += "--password=$Password" }
|
||||
$applyArgs += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||
}
|
||||
@@ -351,7 +375,7 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
@@ -392,7 +416,7 @@ try {
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if ($logContent) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-xml v1.12 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,39 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -199,10 +232,10 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
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})", file=sys.stderr)
|
||||
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:
|
||||
@@ -219,13 +252,13 @@ def main():
|
||||
if args.Password:
|
||||
apply_args.append(f"--password={args.Password}")
|
||||
apply_args.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
||||
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})", file=sys.stderr)
|
||||
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:
|
||||
@@ -308,7 +341,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -352,7 +385,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
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:
|
||||
print("--- Log ---")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-run v1.2 — Launch 1C:Enterprise
|
||||
# db-run v1.4 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -79,6 +79,13 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -165,7 +172,23 @@ if ($URL) {
|
||||
|
||||
$argString += " /DisableStartupDialogs"
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
Write-Host "Running: 1cv8.exe $argString"
|
||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
||||
# --- Execute (background) ---
|
||||
# Redact the password/user before printing the command line — never leak secrets.
|
||||
$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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-run v1.2 — Launch 1C:Enterprise
|
||||
# db-run v1.4 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
@@ -74,6 +75,15 @@ def resolve_v8path(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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -131,9 +141,23 @@ def main():
|
||||
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
subprocess.Popen([v8path] + arguments)
|
||||
# --- Execute (background) ---
|
||||
# Redact the password/user before printing the command line — never leak secrets.
|
||||
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")
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-update v1.6 — Update 1C database configuration
|
||||
# db-update v1.10 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -89,6 +89,30 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -191,14 +215,14 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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)" -ForegroundColor Red
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
if ($output) { Write-Host ($output | Out-String) }
|
||||
exit $exitCode
|
||||
@@ -243,7 +267,7 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
@@ -251,7 +275,7 @@ try {
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||
} 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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.6 — Update 1C database configuration
|
||||
# db-update v1.10 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,39 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -151,12 +184,12 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
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})", file=sys.stderr)
|
||||
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:
|
||||
@@ -203,7 +236,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -215,7 +248,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Database configuration updated successfully")
|
||||
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):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-build v1.6 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -70,6 +70,13 @@ param(
|
||||
$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 ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
@@ -141,6 +148,13 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -188,12 +202,16 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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
|
||||
}
|
||||
@@ -222,13 +240,18 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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) {
|
||||
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 {
|
||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-build v1.6 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,21 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -166,17 +181,23 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
@@ -199,7 +220,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -208,8 +229,14 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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:
|
||||
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:
|
||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-dump v1.6 — 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
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -155,6 +155,20 @@ function Invoke-IbcmdProcess {
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
@@ -189,12 +203,16 @@ try {
|
||||
if ($UserName) { $arguments += "--user=$UserName" }
|
||||
if ($Password) { $arguments += "--password=$Password" }
|
||||
$arguments += "--data=$tempDir"
|
||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||
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
|
||||
}
|
||||
@@ -224,13 +242,18 @@ try {
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- 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
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- 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) {
|
||||
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 {
|
||||
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-dump v1.6 — 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
|
||||
|
||||
import argparse
|
||||
@@ -99,6 +99,21 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
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():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -163,17 +178,23 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f"--password={args.Password}")
|
||||
arguments.append(f"--data={ib_data}")
|
||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||
if result.returncode == 0:
|
||||
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: {result.returncode})", file=sys.stderr)
|
||||
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(result.returncode)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
@@ -197,7 +218,7 @@ def main():
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
@@ -206,8 +227,14 @@ def main():
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- 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:
|
||||
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:
|
||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-add v1.8 — 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
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -471,7 +484,10 @@ if (-not $childObjects) {
|
||||
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.InnerText = $FormName
|
||||
|
||||
@@ -525,6 +541,7 @@ if ($insertBefore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- 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 " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
||||
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) {
|
||||
Write-Host "${defaultPropName}: $defaultValue"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.8 — 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
|
||||
|
||||
import argparse
|
||||
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -193,14 +210,50 @@ def detect_format_version(d):
|
||||
return "2.17"
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
@@ -539,47 +592,50 @@ def main():
|
||||
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Add <Form>$FormName</Form>
|
||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||
form_elem.text = form_name
|
||||
# Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
|
||||
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
|
||||
|
||||
# Find first <Template> to insert before it
|
||||
first_template = child_objects.find("md:Template", NSMAP)
|
||||
# Find first <TabularSection> to insert before it (if no Template)
|
||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
||||
if not already_registered:
|
||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||
form_elem.text = form_name
|
||||
|
||||
# Determine insertion point: before Template, before TabularSection, or at end
|
||||
insert_before = None
|
||||
if first_template is not None:
|
||||
insert_before = first_template
|
||||
elif first_tabular is not None:
|
||||
insert_before = first_tabular
|
||||
# Find first <Template> to insert before it
|
||||
first_template = child_objects.find("md:Template", NSMAP)
|
||||
# Find first <TabularSection> to insert before it (if no Template)
|
||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
||||
|
||||
if insert_before is not None:
|
||||
# Insert before the found element
|
||||
idx = list(child_objects).index(insert_before)
|
||||
child_objects.insert(idx, form_elem)
|
||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
||||
form_elem.tail = "\n\t\t\t"
|
||||
else:
|
||||
# Add to end of ChildObjects
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = "\n\t\t"
|
||||
# Determine insertion point: before Template, before TabularSection, or at end
|
||||
insert_before = None
|
||||
if first_template is not None:
|
||||
insert_before = first_template
|
||||
elif first_tabular is not None:
|
||||
insert_before = first_tabular
|
||||
|
||||
if insert_before is not None:
|
||||
# Insert before the found element
|
||||
idx = list(child_objects).index(insert_before)
|
||||
child_objects.insert(idx, form_elem)
|
||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
||||
form_elem.tail = "\n\t\t\t"
|
||||
else:
|
||||
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"
|
||||
# Add to end of ChildObjects
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = "\n\t\t"
|
||||
else:
|
||||
if len(children) > 0:
|
||||
last_child = children[-1]
|
||||
old_tail = last_child.tail
|
||||
last_child.tail = "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||
else:
|
||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = "\n\t\t"
|
||||
|
||||
# --- SetDefault ---
|
||||
|
||||
@@ -624,7 +680,10 @@ def main():
|
||||
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
||||
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
||||
print()
|
||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
||||
if 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:
|
||||
print(f"{default_prop_name}: {default_value}")
|
||||
print()
|
||||
|
||||
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
| `showTitle: true` | Показывать заголовок группы |
|
||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
|
||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||
| `children: [...]` | Вложенные элементы |
|
||||
|
||||
@@ -549,8 +550,8 @@ PictureField, привязанный к булеву/числу, рисует и
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
|
||||
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
|
||||
1. **Каркас**: `/form-add` создаёт метаданные формы (`ФормаСписка.xml`), `Module.bsl` и регистрирует форму у объекта.
|
||||
2. **Компиляция**: `/form-compile` наполняет `Form.xml` элементами.
|
||||
3. **Проверка**: `/form-validate`, `/form-info`.
|
||||
|
||||
## Верификация
|
||||
|
||||
@@ -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
|
||||
param(
|
||||
[string]$JsonPath,
|
||||
@@ -1362,6 +1362,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -1398,10 +1408,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
import argparse
|
||||
import copy
|
||||
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
|
||||
@@ -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
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -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
|
||||
import argparse
|
||||
import json
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -1458,14 +1475,40 @@ if elem_events_list:
|
||||
|
||||
# ── 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")
|
||||
# Fix XML declaration quotes
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
# Восстановить регистр encoding как в оригинале.
|
||||
xml_bytes = xml_bytes.replace(
|
||||
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"
|
||||
# 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:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
if _fe_bom:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
f.write(xml_bytes)
|
||||
|
||||
# ── 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
|
||||
param(
|
||||
[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
|
||||
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
||||
# 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) {
|
||||
try {
|
||||
$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).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
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 $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
|
||||
if ($objectContext) { $header += " ($objectContext)" }
|
||||
$header += " ==="
|
||||
$lines += $header
|
||||
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
|
||||
$support = Get-SupportStatusForPath $FormPath
|
||||
if ($null -ne $support) { $lines += "Поддержка: $support" }
|
||||
|
||||
# --- Form properties (Title excluded — shown in header) ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
if is_external_root(rp):
|
||||
return None
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if is_external_root(d + ".xml"):
|
||||
return None
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
@@ -513,7 +528,9 @@ def main():
|
||||
header += f" ({object_context})"
|
||||
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) ---
|
||||
prop_names = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-remove v1.3 — Remove form from 1C object
|
||||
# form-remove v1.4 — Remove form from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# remove-form v1.3 — Remove form from 1C object
|
||||
# remove-form v1.4 — Remove form from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -13,14 +13,50 @@ from lxml import etree
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -188,14 +205,50 @@ def detect_format_version(d):
|
||||
return "2.17"
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
import argparse
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -270,13 +287,49 @@ def parse_value_list(val):
|
||||
return [val]
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ allowed-tools:
|
||||
Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
|
||||
регистрирует объект в `Configuration.xml`.
|
||||
|
||||
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
|
||||
платформа (для инкрементальной выгрузки).
|
||||
|
||||
## Порядок работы
|
||||
|
||||
1. Составь JSON по синтаксису ниже → запиши во временный файл.
|
||||
|
||||
@@ -125,6 +125,7 @@ shorthand — вместо строки задаётся объект:
|
||||
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
|
||||
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
|
||||
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
|
||||
| `lineNumberLength` | по режиму совместимости | `5`…`9` — разрядность номера строки: `5` → до 99 999 строк, `9` → до 999 999 999. Требует формата 2.20 (платформа 8.3.27) |
|
||||
|
||||
### `lineNumber` — стандартный реквизит НомерСтроки
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
|
||||
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
|
||||
| `foldersOnTop` | `true` | bool (группы сверху) |
|
||||
| `owners` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` |
|
||||
| `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
|
||||
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
|
||||
| `codeLength` | `9` | длина кода (0 — без кода) |
|
||||
| `codeType` | `String` | `String` / `Number` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-compile v1.65 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.68 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -36,6 +36,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -72,10 +82,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -773,11 +786,28 @@ $script:mdRefRoots = @{
|
||||
'журналдокументов'='DocumentJournal'; 'отчет'='Report'; 'отчёт'='Report'; 'обработка'='DataProcessor';
|
||||
'табличнаячасть'='TabularSection'; 'реквизит'='Attribute'; 'измерение'='Dimension'; 'ресурс'='Resource';
|
||||
'стандартныйреквизит'='StandardAttribute'; 'значениеперечисления'='EnumValue'; 'команда'='Command';
|
||||
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag'
|
||||
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag';
|
||||
# Ссылочные формы (тип ссылки вместо объекта метаданных): в MDObjectRef-пути нужен ОБЪЕКТ, т.е.
|
||||
# "CatalogRef.Валюты" → "Catalog.Валюты". Вид метаданных, оканчивающийся на Ref, не существует,
|
||||
# поэтому схлопывание однозначно. В ТИПАХ реквизитов запись CatalogRef.X верна — там эта мапа не применяется.
|
||||
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
|
||||
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
|
||||
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
|
||||
'businessprocessref'='BusinessProcess'; 'taskref'='Task';
|
||||
'справочникссылка'='Catalog'; 'документссылка'='Document'; 'перечислениессылка'='Enum';
|
||||
'плансчетовссылка'='ChartOfAccounts'; 'планвидовхарактеристикссылка'='ChartOfCharacteristicTypes';
|
||||
'планвидоврасчетассылка'='ChartOfCalculationTypes'; 'планвидоврасчётассылка'='ChartOfCalculationTypes';
|
||||
'планобменассылка'='ExchangePlan'; 'бизнеспроцессссылка'='BusinessProcess'; 'задачассылка'='Task'
|
||||
}
|
||||
# $defaultRoot — корень для ГОЛОГО имени без точки (напр. owners: "Валюты" → "Catalog.Валюты").
|
||||
# Без него голое имя возвращается как есть (прежнее поведение вызывающих без подстановки).
|
||||
function Normalize-MDObjectRef {
|
||||
param([string]$ref)
|
||||
if (-not $ref -or -not $ref.Contains('.')) { return $ref }
|
||||
param([string]$ref, [string]$defaultRoot)
|
||||
if (-not $ref) { return $ref }
|
||||
if (-not $ref.Contains('.')) {
|
||||
if ($defaultRoot) { return "$defaultRoot.$ref" }
|
||||
return $ref
|
||||
}
|
||||
$parts = $ref -split '\.'
|
||||
for ($k = 0; $k -lt $parts.Count; $k += 2) {
|
||||
$t = $script:mdRefRoots[$parts[$k].ToLower()]
|
||||
@@ -1291,6 +1321,12 @@ function Emit-StandardAttribute {
|
||||
X "$indent`t<xr:MultiLine>false</xr:MultiLine>"
|
||||
X "$indent`t<xr:FillFromFillingValue>$ffv</xr:FillFromFillingValue>"
|
||||
X "$indent`t<xr:CreateOnInput>Auto</xr:CreateOnInput>"
|
||||
# Формат 2.20 (8.3.27): режим приведения типов. Платформа пишет его КАЖДОМУ стандартному
|
||||
# реквизиту; значение всегда TransformValues, кроме владельца (Owner) — там Deny.
|
||||
if ($script:isFormat220) {
|
||||
$trm = OvOr 'TypeReductionMode' $(if ($attrName -ceq 'Owner') { 'Deny' } else { 'TransformValues' })
|
||||
X "$indent`t<xr:TypeReductionMode>$trm</xr:TypeReductionMode>"
|
||||
}
|
||||
X "$indent`t<xr:MaxValue xsi:nil=`"true`"/>"
|
||||
Emit-MLText "$indent`t" "xr:ToolTip" $tt
|
||||
X "$indent`t<xr:ExtendedEdit>false</xr:ExtendedEdit>"
|
||||
@@ -1484,7 +1520,7 @@ function Emit-BasedOn {
|
||||
$arr = @($items | Where-Object { $_ })
|
||||
if ($arr.Count -eq 0) { X "$indent<BasedOn/>"; return }
|
||||
X "$indent<BasedOn>"
|
||||
foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$it")</xr:Item>" }
|
||||
foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$it"))</xr:Item>" }
|
||||
X "$indent</BasedOn>"
|
||||
}
|
||||
|
||||
@@ -1919,6 +1955,12 @@ function Emit-Attribute {
|
||||
X "$indent`t`t<DataHistory>$dh</DataHistory>"
|
||||
}
|
||||
}
|
||||
# Формат 2.20 (8.3.27): режим приведения типов — последним в Properties и ТОЛЬКО у измерений
|
||||
# регистра сведений (у реквизитов/ресурсов и у прочих семейств регистров платформа его не пишет).
|
||||
if ($script:isFormat220 -and $elemTag -eq "Dimension" -and $context -eq "register-info") {
|
||||
$trm = if ($parsed.typeReductionMode) { "$($parsed.typeReductionMode)" } else { "TransformValues" }
|
||||
X "$indent`t`t<TypeReductionMode>$trm</TypeReductionMode>"
|
||||
}
|
||||
|
||||
X "$indent`t</Properties>"
|
||||
X "$indent</$elemTag>"
|
||||
@@ -1990,7 +2032,7 @@ function Emit-Command {
|
||||
# --- 9. TabularSection emitter ---
|
||||
|
||||
function Emit-TabularSection {
|
||||
param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName, $tsSynonymArg = $null, $tsTooltip = $null, $tsComment = $null, $tsLineNumber = $null, $tsFillChecking = $null, $tsUse = $null)
|
||||
param([string]$indent, [string]$tsName, $columns, [string]$objectType, [string]$objectName, $tsSynonymArg = $null, $tsTooltip = $null, $tsComment = $null, $tsLineNumber = $null, $tsFillChecking = $null, $tsUse = $null, $tsLineNumberLength = $null)
|
||||
$uuid = New-Guid-String
|
||||
X "$indent<TabularSection uuid=`"$uuid`">"
|
||||
|
||||
@@ -2029,6 +2071,12 @@ function Emit-TabularSection {
|
||||
$use = if ($tsUse) { "$tsUse" } else { "ForItem" }
|
||||
X "$indent`t`t<Use>$use</Use>"
|
||||
}
|
||||
# Формат 2.20 (8.3.27): длина номера строки ТЧ (5..9 → до 999 999 999 строк вместо 99 999).
|
||||
# Последним в Properties. Дефолт платформа берёт из режима совместимости на момент создания ТЧ.
|
||||
if ($script:isFormat220) {
|
||||
$lnl = if ($null -ne $tsLineNumberLength -and "$tsLineNumberLength" -ne '') { [int]$tsLineNumberLength } else { $script:lineNumberLengthDefault }
|
||||
X "$indent`t`t<LineNumberLength>$lnl</LineNumberLength>"
|
||||
}
|
||||
X "$indent`t</Properties>"
|
||||
|
||||
$tsContext = if ($objectType -in @("DataProcessor","Report")) { "processor-tabular" } else { "tabular" }
|
||||
@@ -2245,8 +2293,7 @@ function Emit-CatalogProperties {
|
||||
if ($def.owners -and $def.owners.Count -gt 0) {
|
||||
X "$i<Owners>"
|
||||
foreach ($ownerRef in $def.owners) {
|
||||
$fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" }
|
||||
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$fullRef</xr:Item>"
|
||||
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$ownerRef" 'Catalog'))</xr:Item>"
|
||||
}
|
||||
X "$i</Owners>"
|
||||
} else {
|
||||
@@ -2397,7 +2444,7 @@ function Emit-DocumentProperties {
|
||||
}
|
||||
if ($regRecords.Count -gt 0) {
|
||||
X "$i<RegisterRecords>"
|
||||
foreach ($rr in $regRecords) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$rr</xr:Item>" }
|
||||
foreach ($rr in $regRecords) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$rr"))</xr:Item>" }
|
||||
X "$i</RegisterRecords>"
|
||||
} else {
|
||||
X "$i<RegisterRecords/>"
|
||||
@@ -3511,7 +3558,7 @@ function Emit-ChartOfCalculationTypesProperties {
|
||||
$baseTypes = @(); if ($def.baseCalculationTypes) { $baseTypes = @($def.baseCalculationTypes | ForEach-Object { Resolve-TypePrefixSyn "$_" }) }
|
||||
if ($baseTypes.Count -gt 0) {
|
||||
X "$i<BaseCalculationTypes>"
|
||||
foreach ($bt in $baseTypes) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml $bt)</xr:Item>" }
|
||||
foreach ($bt in $baseTypes) { X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$bt"))</xr:Item>" }
|
||||
X "$i</BaseCalculationTypes>"
|
||||
} else { X "$i<BaseCalculationTypes/>" }
|
||||
$actionPeriodUse = if ($def.actionPeriodUse -eq $true) { "true" } else { "false" }
|
||||
@@ -3955,7 +4002,51 @@ function Detect-FormatVersion([string]$dir) {
|
||||
return "2.17"
|
||||
}
|
||||
|
||||
# Режим совместимости конфигурации — из него выводится дефолт <LineNumberLength> табличной части
|
||||
# (≤Version8_3_26 → 5, ≥Version8_3_27 → 9; платформа фиксирует значение при СОЗДАНии ТЧ).
|
||||
# NB: версия ФОРМАТА от режима совместимости не зависит (её задаёт платформа выгрузки) — это
|
||||
# независимые вещи, читаются из одного файла разными функциями.
|
||||
# Читаем префикс побольше: <CompatibilityMode> лежит ~11-12 КБ от начала (в отличие от version=
|
||||
# в первой строке), 2000 байт Detect-FormatVersion сюда не хватает.
|
||||
function Detect-CompatibilityMode([string]$dir) {
|
||||
$d = $dir
|
||||
while ($d) {
|
||||
$cfgPath = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfgPath) {
|
||||
# NB: длина файла — в БАЙТАХ, а Substring режет по СИМВОЛАМ (кириллица = 2 байта),
|
||||
# поэтому ограничиваем по длине уже декодированной строки.
|
||||
$text = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||
$head = $text.Substring(0, [Math]::Min(65536, $text.Length))
|
||||
if ($head -match '<CompatibilityMode>([^<]+)</CompatibilityMode>') { return $Matches[1].Trim() }
|
||||
}
|
||||
$parent = Split-Path $d -Parent
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return "Version8_3_24"
|
||||
}
|
||||
|
||||
# Номер версии режима совместимости для сравнений: "Version8_3_27" → 80327, "Version8_5_1" → 80501.
|
||||
function Get-CompatModeRank([string]$mode) {
|
||||
if ($mode -match '^Version(\d+)_(\d+)_(\d+)$') {
|
||||
return [int]$Matches[1] * 10000 + [int]$Matches[2] * 100 + [int]$Matches[3]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||
function Get-FormatRank([string]$ver) {
|
||||
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||
return 0
|
||||
}
|
||||
|
||||
$script:formatVersion = Detect-FormatVersion $OutputDir
|
||||
$script:compatMode = Detect-CompatibilityMode $OutputDir
|
||||
# Формат 2.20+ (платформа 8.3.27) — только тогда эмитим новые свойства.
|
||||
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
|
||||
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
||||
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }
|
||||
|
||||
# --- 15. Main assembler ---
|
||||
|
||||
@@ -4035,10 +4126,10 @@ if ($objType -in $typesWithAttrTS) {
|
||||
# Нормализуем в $tsSections[name] = @{ columns; synonym; tooltip; comment }.
|
||||
function New-TsEntry { param($val)
|
||||
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
|
||||
return @{ columns = @($val); synonym = $null; tooltip = $null; comment = $null; lineNumber = $null; fillChecking = $null; use = $null }
|
||||
return @{ columns = @($val); synonym = $null; tooltip = $null; comment = $null; lineNumber = $null; fillChecking = $null; use = $null; lineNumberLength = $null }
|
||||
}
|
||||
$cols = if ($val.attributes) { @($val.attributes) } elseif ($val.columns) { @($val.columns) } else { @() }
|
||||
return @{ columns = $cols; synonym = $val.synonym; tooltip = $val.tooltip; comment = if ($val.comment) { "$($val.comment)" } else { $null }; lineNumber = $val.lineNumber; fillChecking = $val.fillChecking; use = $val.use }
|
||||
return @{ columns = $cols; synonym = $val.synonym; tooltip = $val.tooltip; comment = if ($val.comment) { "$($val.comment)" } else { $null }; lineNumber = $val.lineNumber; fillChecking = $val.fillChecking; use = $val.use; lineNumberLength = $val.lineNumberLength }
|
||||
}
|
||||
if ($def.tabularSections -is [array] -or $def.tabularSections.GetType().Name -eq "Object[]") {
|
||||
foreach ($ts in $def.tabularSections) { $tsSections[$ts.name] = New-TsEntry $ts }
|
||||
@@ -4088,7 +4179,7 @@ if ($objType -in $typesWithAttrTS) {
|
||||
}
|
||||
foreach ($tsName in $tsSections.Keys) {
|
||||
$tsE = $tsSections[$tsName]
|
||||
Emit-TabularSection "`t`t`t" $tsName $tsE.columns $objType $objName $tsE.synonym $tsE.tooltip $tsE.comment $tsE.lineNumber $tsE.fillChecking $tsE.use
|
||||
Emit-TabularSection "`t`t`t" $tsName $tsE.columns $objType $objName $tsE.synonym $tsE.tooltip $tsE.comment $tsE.lineNumber $tsE.fillChecking $tsE.use $tsE.lineNumberLength
|
||||
}
|
||||
foreach ($af in $acctFlags) {
|
||||
Emit-Attribute "`t`t`t" $af "account-flag" "AccountingFlag"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-compile v1.65 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.68 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -36,6 +36,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -75,6 +87,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -82,6 +97,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -805,11 +822,26 @@ md_ref_roots = {
|
||||
'табличнаячасть': 'TabularSection', 'реквизит': 'Attribute', 'измерение': 'Dimension', 'ресурс': 'Resource',
|
||||
'стандартныйреквизит': 'StandardAttribute', 'значениеперечисления': 'EnumValue', 'команда': 'Command',
|
||||
'признакучета': 'AccountingFlag', 'признакучёта': 'AccountingFlag',
|
||||
# Ссылочные формы (тип ссылки вместо объекта метаданных): в MDObjectRef-пути нужен ОБЪЕКТ, т.е.
|
||||
# "CatalogRef.Валюты" → "Catalog.Валюты". Вид метаданных, оканчивающийся на Ref, не существует,
|
||||
# поэтому схлопывание однозначно. В ТИПАХ реквизитов запись CatalogRef.X верна — там мапа не применяется.
|
||||
'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum',
|
||||
'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes',
|
||||
'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan',
|
||||
'businessprocessref': 'BusinessProcess', 'taskref': 'Task',
|
||||
'справочникссылка': 'Catalog', 'документссылка': 'Document', 'перечислениессылка': 'Enum',
|
||||
'плансчетовссылка': 'ChartOfAccounts', 'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypes',
|
||||
'планвидоврасчетассылка': 'ChartOfCalculationTypes', 'планвидоврасчётассылка': 'ChartOfCalculationTypes',
|
||||
'планобменассылка': 'ExchangePlan', 'бизнеспроцессссылка': 'BusinessProcess', 'задачассылка': 'Task',
|
||||
}
|
||||
|
||||
def normalize_md_object_ref(ref):
|
||||
if not ref or '.' not in ref:
|
||||
def normalize_md_object_ref(ref, default_root=None):
|
||||
"""default_root — корень для ГОЛОГО имени без точки (owners: "Валюты" → "Catalog.Валюты").
|
||||
Без него голое имя возвращается как есть (прежнее поведение вызывающих без подстановки)."""
|
||||
if not ref:
|
||||
return ref
|
||||
if '.' not in ref:
|
||||
return f'{default_root}.{ref}' if default_root else ref
|
||||
parts = ref.split('.')
|
||||
for k in range(0, len(parts), 2):
|
||||
t = md_ref_roots.get(parts[k].lower())
|
||||
@@ -1308,6 +1340,11 @@ def emit_standard_attribute(indent, attr_name, ov=None):
|
||||
X(f'{indent}\t<xr:MultiLine>false</xr:MultiLine>')
|
||||
X(f'{indent}\t<xr:FillFromFillingValue>{ffv}</xr:FillFromFillingValue>')
|
||||
X(f'{indent}\t<xr:CreateOnInput>Auto</xr:CreateOnInput>')
|
||||
# Формат 2.20 (8.3.27): режим приведения типов. Платформа пишет его КАЖДОМУ стандартному
|
||||
# реквизиту; значение всегда TransformValues, кроме владельца (Owner) — там Deny.
|
||||
if is_format_220:
|
||||
trm = ov.get('TypeReductionMode', 'Deny' if attr_name == 'Owner' else 'TransformValues')
|
||||
X(f'{indent}\t<xr:TypeReductionMode>{trm}</xr:TypeReductionMode>')
|
||||
X(f'{indent}\t<xr:MaxValue xsi:nil="true"/>')
|
||||
emit_mltext(f'{indent}\t', 'xr:ToolTip', tt)
|
||||
X(f'{indent}\t<xr:ExtendedEdit>false</xr:ExtendedEdit>')
|
||||
@@ -1570,7 +1607,7 @@ def emit_based_on(indent, items):
|
||||
return
|
||||
X(f'{indent}<BasedOn>')
|
||||
for it in arr:
|
||||
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(str(it))}</xr:Item>')
|
||||
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(it)))}</xr:Item>')
|
||||
X(f'{indent}</BasedOn>')
|
||||
|
||||
# --- Параметры/связи выбора (порт из form-compile) ---
|
||||
@@ -1980,6 +2017,10 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
||||
# DataHistory — not for Chart* types and non-InformationRegister register family
|
||||
if context not in ('chart', 'register-other', 'register-accum', 'register-calc', 'register-account'):
|
||||
X(f'{indent}\t\t<DataHistory>{parsed.get("dataHistory") or "Use"}</DataHistory>')
|
||||
# Формат 2.20 (8.3.27): режим приведения типов — последним в Properties и ТОЛЬКО у измерений
|
||||
# регистра сведений (у реквизитов/ресурсов и у прочих семейств регистров платформа его не пишет).
|
||||
if is_format_220 and elem_tag == 'Dimension' and context == 'register-info':
|
||||
X(f'{indent}\t\t<TypeReductionMode>{parsed.get("typeReductionMode") or "TransformValues"}</TypeReductionMode>')
|
||||
X(f'{indent}\t</Properties>')
|
||||
X(f'{indent}</{elem_tag}>')
|
||||
|
||||
@@ -2056,7 +2097,7 @@ def emit_command(indent, cmd_name, cmd):
|
||||
X(f'{indent}\t</Properties>')
|
||||
X(f'{indent}</Command>')
|
||||
|
||||
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None, ts_line_number=None, ts_fill_checking=None, ts_use=None):
|
||||
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None, ts_line_number=None, ts_fill_checking=None, ts_use=None, ts_line_number_length=None):
|
||||
uid = new_uuid()
|
||||
X(f'{indent}<TabularSection uuid="{uid}">')
|
||||
type_prefix = f'{object_type}TabularSection'
|
||||
@@ -2087,6 +2128,11 @@ def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_
|
||||
emit_tabular_standard_attributes(f'{indent}\t\t', ts_line_number)
|
||||
if object_type in ('Catalog', 'ChartOfCharacteristicTypes'):
|
||||
X(f'{indent}\t\t<Use>{ts_use if ts_use else "ForItem"}</Use>')
|
||||
# Формат 2.20 (8.3.27): длина номера строки ТЧ (5..9 → до 999 999 999 строк вместо 99 999).
|
||||
# Последним в Properties. Дефолт платформа берёт из режима совместимости на момент создания ТЧ.
|
||||
if is_format_220:
|
||||
lnl = int(ts_line_number_length) if ts_line_number_length not in (None, '') else line_number_length_default
|
||||
X(f'{indent}\t\t<LineNumberLength>{lnl}</LineNumberLength>')
|
||||
X(f'{indent}\t</Properties>')
|
||||
ts_context = 'processor-tabular' if object_type in ('DataProcessor', 'Report') else 'tabular'
|
||||
X(f'{indent}\t<ChildObjects>')
|
||||
@@ -2271,8 +2317,7 @@ def emit_catalog_properties(indent):
|
||||
if owners:
|
||||
X(f'{i}<Owners>')
|
||||
for owner_ref in owners:
|
||||
full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{full_ref}</xr:Item>')
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(owner_ref), "Catalog"))}</xr:Item>')
|
||||
X(f'{i}</Owners>')
|
||||
else:
|
||||
X(f'{i}<Owners/>')
|
||||
@@ -2412,7 +2457,7 @@ def emit_document_properties(indent):
|
||||
if reg_records:
|
||||
X(f'{i}<RegisterRecords>')
|
||||
for rr in reg_records:
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{rr}</xr:Item>')
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(rr)))}</xr:Item>')
|
||||
X(f'{i}</RegisterRecords>')
|
||||
else:
|
||||
X(f'{i}<RegisterRecords/>')
|
||||
@@ -3466,7 +3511,7 @@ def emit_chart_of_calculation_types_properties(indent):
|
||||
if base_types:
|
||||
X(f'{i}<BaseCalculationTypes>')
|
||||
for bt in base_types:
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(bt)}</xr:Item>')
|
||||
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(bt))}</xr:Item>')
|
||||
X(f'{i}</BaseCalculationTypes>')
|
||||
else:
|
||||
X(f'{i}<BaseCalculationTypes/>')
|
||||
@@ -3856,7 +3901,44 @@ def detect_format_version(d):
|
||||
d = parent
|
||||
return "2.17"
|
||||
|
||||
def detect_compatibility_mode(d):
|
||||
"""Режим совместимости конфигурации — из него выводится дефолт <LineNumberLength> табличной части
|
||||
(<=Version8_3_26 → 5, >=Version8_3_27 → 9; платформа фиксирует значение при СОЗДАНИИ ТЧ).
|
||||
NB: версия ФОРМАТА от режима совместимости не зависит (её задаёт платформа выгрузки).
|
||||
Читаем префикс побольше: <CompatibilityMode> лежит ~11-12 КБ от начала."""
|
||||
while d:
|
||||
cfg_path = os.path.join(d, "Configuration.xml")
|
||||
if os.path.isfile(cfg_path):
|
||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||
head = f.read(65536)
|
||||
m = re.search(r'<CompatibilityMode>([^<]+)</CompatibilityMode>', head)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return "Version8_3_24"
|
||||
|
||||
|
||||
def compat_mode_rank(mode):
|
||||
""""Version8_3_27" → 80327, "Version8_5_1" → 80501."""
|
||||
m = re.match(r'^Version(\d+)_(\d+)_(\d+)$', mode or '')
|
||||
return int(m.group(1)) * 10000 + int(m.group(2)) * 100 + int(m.group(3)) if m else 0
|
||||
|
||||
|
||||
def format_rank(ver):
|
||||
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||
|
||||
|
||||
format_version = detect_format_version(output_dir)
|
||||
compat_mode = detect_compatibility_mode(output_dir)
|
||||
# Формат 2.20+ (платформа 8.3.27) — только тогда эмитим новые свойства.
|
||||
is_format_220 = format_rank(format_version) >= 220
|
||||
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
||||
line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 15. Main assembler
|
||||
@@ -3949,10 +4031,10 @@ if obj_type in types_with_attr_ts:
|
||||
# Значение ТЧ: массив колонок (синоним авто) ЛИБО объект {attributes/columns, synonym, tooltip, comment}.
|
||||
def new_ts_entry(val):
|
||||
if isinstance(val, list):
|
||||
return {'columns': val, 'synonym': None, 'tooltip': None, 'comment': None, 'lineNumber': None, 'fillChecking': None, 'use': None}
|
||||
return {'columns': val, 'synonym': None, 'tooltip': None, 'comment': None, 'lineNumber': None, 'fillChecking': None, 'use': None, 'lineNumberLength': None}
|
||||
cols = _as_list(val.get('attributes') or val.get('columns') or [])
|
||||
return {'columns': cols, 'synonym': val.get('synonym'), 'tooltip': val.get('tooltip'),
|
||||
'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber'), 'fillChecking': val.get('fillChecking'), 'use': val.get('use')}
|
||||
'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber'), 'fillChecking': val.get('fillChecking'), 'use': val.get('use'), 'lineNumberLength': val.get('lineNumberLength')}
|
||||
if isinstance(ts_data, list):
|
||||
for ts in ts_data:
|
||||
ts_sections[ts['name']] = new_ts_entry(ts)
|
||||
@@ -4004,7 +4086,7 @@ if obj_type in types_with_attr_ts:
|
||||
emit_attribute('\t\t\t', a, context)
|
||||
for ts_name in ts_order:
|
||||
e = ts_sections[ts_name]
|
||||
emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber'), e.get('fillChecking'), e.get('use'))
|
||||
emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber'), e.get('fillChecking'), e.get('use'), e.get('lineNumberLength'))
|
||||
for af in acct_flags:
|
||||
emit_attribute('\t\t\t', af, 'account-flag', 'AccountingFlag')
|
||||
for edf in ext_dim_flags:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-decompile v0.54 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.55 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
||||
@@ -323,6 +323,9 @@ function Attr-ToDsl {
|
||||
$v = & $en 'MainFilter'; if ($v -eq 'true') { $extra['mainFilter'] = $true }
|
||||
$v = & $en 'DenyIncompleteValues'; if ($v -eq 'true') { $extra['denyIncompleteValues'] = $true }
|
||||
$v = & $en 'UseInTotals'; if ($v -eq 'false') { $extra['useInTotals'] = $false } # дефолт true → захват при false
|
||||
# Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
|
||||
# эмитит сам) → захватываем только отклонение.
|
||||
$v = & $en 'TypeReductionMode'; if ($v -and $v -ne 'TransformValues') { $extra['typeReductionMode'] = $v }
|
||||
$v = & $en 'BaseDimension'; if ($v -eq 'true') { $extra['baseDimension'] = $true }
|
||||
$v = & $en 'ScheduleLink'; if ($v) { $extra['scheduleLink'] = $v } # ссылка на измерение графика (пустой → пропуск)
|
||||
$v = & $en 'Balance'; if ($v -eq 'true') { $extra['balance'] = $true }
|
||||
@@ -1092,6 +1095,13 @@ if ($saNode) {
|
||||
$ov['linkByType'] = [ordered]@{ dataPath = $saLbtDp.InnerText; linkItem = $li }
|
||||
}
|
||||
}
|
||||
# Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues, у Owner —
|
||||
# Deny), поэтому захватываем только отклонение от этого правила.
|
||||
$saTrmN = $sa.SelectSingleNode('xr:TypeReductionMode', $nsm)
|
||||
if ($saTrmN -and $saTrmN.InnerText) {
|
||||
$saTrmDef = if ($an -ceq 'Owner') { 'Deny' } else { 'TransformValues' }
|
||||
if ($saTrmN.InnerText -ne $saTrmDef) { $ov['TypeReductionMode'] = $saTrmN.InnerText }
|
||||
}
|
||||
# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
|
||||
if ($ov.Count -gt 0 -or ($stdFixed -notcontains $an)) { $saMap[$an] = $ov }
|
||||
}
|
||||
@@ -1262,13 +1272,20 @@ if ($childObjs) {
|
||||
if ($lnFvT -match 'decimal$') { $lnObj['fillValue'] = if ($lnFvN.InnerText -match '^-?\d+$') { [long]$lnFvN.InnerText } else { [double]$lnFvN.InnerText } }
|
||||
}
|
||||
}
|
||||
if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $tsFc -or $tsUse -or $lnObj.Count -gt 0 -or (-not $hasBlock)) {
|
||||
# Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
|
||||
# omit-on-default: дефолт зависит от режима совместимости конфигурации (≤8_3_26 → 5,
|
||||
# ≥8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
|
||||
# значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
|
||||
$tsLnlN = $tsp.SelectSingleNode('md:LineNumberLength', $nsm)
|
||||
$tsLnl = if ($tsLnlN -and $tsLnlN.InnerText) { [int]$tsLnlN.InnerText } else { $null }
|
||||
if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $tsFc -or $tsUse -or $lnObj.Count -gt 0 -or (-not $hasBlock) -or ($null -ne $tsLnl)) {
|
||||
$to = [ordered]@{}
|
||||
if ($tsSynCustom) { $to['synonym'] = $tsSyn }
|
||||
if ($null -ne $tsTt) { $to['tooltip'] = $tsTt }
|
||||
if ($tsCmt) { $to['comment'] = $tsCmt }
|
||||
if ($tsFc) { $to['fillChecking'] = $tsFc }
|
||||
if ($tsUse) { $to['use'] = $tsUse }
|
||||
if ($null -ne $tsLnl) { $to['lineNumberLength'] = $tsLnl }
|
||||
if (-not $hasBlock) { $to['lineNumber'] = '' } elseif ($lnObj.Count -gt 0) { $to['lineNumber'] = $lnObj }
|
||||
$to['attributes'] = $cols
|
||||
$tsMap[$tsName] = $to
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-decompile v0.54 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.55 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
||||
@@ -456,6 +456,11 @@ def attr_to_dsl(attr_node):
|
||||
v = en('UseInTotals')
|
||||
if v == 'false':
|
||||
extra['useInTotals'] = False # дефолт true → захват при false
|
||||
# Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
|
||||
# эмитит сам) → захватываем только отклонение.
|
||||
v = en('TypeReductionMode')
|
||||
if v and v != 'TransformValues':
|
||||
extra['typeReductionMode'] = v
|
||||
v = en('BaseDimension')
|
||||
if v == 'true':
|
||||
extra['baseDimension'] = True
|
||||
@@ -1587,6 +1592,13 @@ def build_dsl():
|
||||
li = int(_text(sa_lbt_li)) if (sa_lbt_li is not None and _text(sa_lbt_li)) else 0
|
||||
ov['linkByType'] = {'dataPath': _text(sa_lbt_dp), 'linkItem': li}
|
||||
# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
|
||||
# Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues,
|
||||
# у Owner — Deny), поэтому захватываем только отклонение от этого правила.
|
||||
sa_trm_n = _single(sa, 'xr:TypeReductionMode')
|
||||
if sa_trm_n is not None and (sa_trm_n.text or '').strip():
|
||||
sa_trm_def = 'Deny' if an == 'Owner' else 'TransformValues'
|
||||
if sa_trm_n.text.strip() != sa_trm_def:
|
||||
ov['TypeReductionMode'] = sa_trm_n.text.strip()
|
||||
if len(ov) > 0 or (an not in std_fixed):
|
||||
sa_map[an] = ov
|
||||
if len(sa_map) > 0 or (obj_type in std_conditional_types):
|
||||
@@ -1782,7 +1794,13 @@ def build_dsl():
|
||||
ln_fv_t = _attr(ln_fv_n, 'type', NS_XSI)
|
||||
if re.search(r'decimal$', ln_fv_t, re.I):
|
||||
ln_obj['fillValue'] = int(_text(ln_fv_n)) if re.match(r'^-?\d+$', _text(ln_fv_n)) else float(_text(ln_fv_n))
|
||||
if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or ts_use or len(ln_obj) > 0 or (not has_block):
|
||||
# Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
|
||||
# omit-on-default: дефолт зависит от режима совместимости конфигурации (<=8_3_26 → 5,
|
||||
# >=8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
|
||||
# значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
|
||||
ts_lnl_n = _single(tsp, 'md:LineNumberLength')
|
||||
ts_lnl = int(ts_lnl_n.text) if ts_lnl_n is not None and (ts_lnl_n.text or '').strip() else None
|
||||
if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or ts_use or len(ln_obj) > 0 or (not has_block) or (ts_lnl is not None):
|
||||
to = {}
|
||||
if ts_syn_custom:
|
||||
to['synonym'] = ts_syn
|
||||
@@ -1794,6 +1812,8 @@ def build_dsl():
|
||||
to['fillChecking'] = ts_fc
|
||||
if ts_use:
|
||||
to['use'] = ts_use
|
||||
if ts_lnl is not None:
|
||||
to['lineNumberLength'] = ts_lnl
|
||||
if not has_block:
|
||||
to['lineNumber'] = ''
|
||||
elif len(ln_obj) > 0:
|
||||
|
||||
@@ -14,6 +14,16 @@
|
||||
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
||||
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта.
|
||||
|
||||
### Type — тип значения (Константа, ПВХ)
|
||||
|
||||
`Type=...` перестраивает дескриптор типа значения. Значение — тип 1С в том же синтаксисе,
|
||||
что у реквизитов: составной через `+`, с квалификаторами и ссылочными типами:
|
||||
```powershell
|
||||
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
||||
```
|
||||
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать
|
||||
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
|
||||
|
||||
## Свойства-списки
|
||||
|
||||
Свойства, значение которых — список ссылок. Управляются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-edit v1.19 — Edit existing 1C metadata object XML (+add-predefined предопределённые Ext/Predefined.xml)
|
||||
# meta-edit v1.23 — Edit existing 1C metadata object XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -170,6 +170,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -206,10 +216,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -1300,7 +1313,7 @@ function Build-ColumnFragment {
|
||||
if ($references.Count -gt 0) {
|
||||
$sb.AppendLine("$indent`t`t<References>") | Out-Null
|
||||
foreach ($ref in $references) {
|
||||
$sb.AppendLine("$indent`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">$ref</xr:Item>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml (Normalize-MDObjectRef "$ref"))</xr:Item>") | Out-Null
|
||||
}
|
||||
$sb.AppendLine("$indent`t`t</References>") | Out-Null
|
||||
} else {
|
||||
@@ -2028,6 +2041,32 @@ function Modify-Properties($propsDef) {
|
||||
$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
|
||||
Info "Modified property: $propName = $valueStr"
|
||||
$script:modifyCount++
|
||||
@@ -2377,13 +2416,56 @@ function Process-Modify($modifyDef) {
|
||||
# Section 12.5: Complex property helpers
|
||||
# ============================================================
|
||||
|
||||
# Прощающий ввод MDObjectRef-путей: русские корни метаданных → английские + ссылочные формы
|
||||
# ("CatalogRef.Валюты"/"СправочникСсылка.Валюты" → "Catalog.Валюты"). MDObjectRef ссылается на ОБЪЕКТ
|
||||
# метаданных, а не на тип ссылки; вида метаданных, оканчивающегося на Ref, не существует → схлопывание
|
||||
# однозначно. Виды стоят на ЧЁТНЫХ позициях (0,2,4…), имена (нечётные) не трогаем. Канонические
|
||||
# английские пути неизменны (в мапе только неканонические ключи). Зеркало meta-compile.
|
||||
$script:mdRefRoots = @{
|
||||
'справочник'='Catalog'; 'документ'='Document'; 'перечисление'='Enum'; 'константа'='Constant';
|
||||
'регистрсведений'='InformationRegister'; 'регистрнакопления'='AccumulationRegister';
|
||||
'регистрбухгалтерии'='AccountingRegister'; 'регистррасчета'='CalculationRegister'; 'регистррасчёта'='CalculationRegister';
|
||||
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
|
||||
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
|
||||
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
|
||||
'журналдокументов'='DocumentJournal'; 'отчет'='Report'; 'отчёт'='Report'; 'обработка'='DataProcessor';
|
||||
'табличнаячасть'='TabularSection'; 'реквизит'='Attribute'; 'измерение'='Dimension'; 'ресурс'='Resource';
|
||||
'стандартныйреквизит'='StandardAttribute'; 'значениеперечисления'='EnumValue'; 'команда'='Command';
|
||||
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag';
|
||||
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
|
||||
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
|
||||
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
|
||||
'businessprocessref'='BusinessProcess'; 'taskref'='Task';
|
||||
'справочникссылка'='Catalog'; 'документссылка'='Document'; 'перечислениессылка'='Enum';
|
||||
'плансчетовссылка'='ChartOfAccounts'; 'планвидовхарактеристикссылка'='ChartOfCharacteristicTypes';
|
||||
'планвидоврасчетассылка'='ChartOfCalculationTypes'; 'планвидоврасчётассылка'='ChartOfCalculationTypes';
|
||||
'планобменассылка'='ExchangePlan'; 'бизнеспроцессссылка'='BusinessProcess'; 'задачассылка'='Task'
|
||||
}
|
||||
# $defaultRoot — корень для ГОЛОГО имени без точки (owners: "Валюты" → "Catalog.Валюты").
|
||||
function Normalize-MDObjectRef {
|
||||
param([string]$ref, [string]$defaultRoot)
|
||||
if (-not $ref) { return $ref }
|
||||
if (-not $ref.Contains('.')) {
|
||||
if ($defaultRoot) { return "$defaultRoot.$ref" }
|
||||
return $ref
|
||||
}
|
||||
$parts = $ref -split '\.'
|
||||
for ($k = 0; $k -lt $parts.Count; $k += 2) {
|
||||
$t = $script:mdRefRoots[$parts[$k].ToLower()]
|
||||
if ($t) { $parts[$k] = $t }
|
||||
}
|
||||
return ($parts -join '.')
|
||||
}
|
||||
|
||||
# mdref — значения списка суть MDObjectRef-пути → прогоняем через Normalize-MDObjectRef.
|
||||
# root — корень для голого имени без точки.
|
||||
$script:complexPropertyMap = @{
|
||||
"Owners" = @{ 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"' }
|
||||
"Owners" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"'; mdref = $true; root = 'Catalog' }
|
||||
"RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"'; mdref = $true }
|
||||
"BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"'; mdref = $true }
|
||||
"InputByString" = @{ tag = "xr:Field"; attr = $null }
|
||||
"DataLockFields" = @{ tag = "xr:Field"; attr = $null; expand = $true }
|
||||
"RegisteredDocuments" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' }
|
||||
"RegisteredDocuments" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"'; mdref = $true }
|
||||
}
|
||||
|
||||
# Известные свойства объекта (union по корпусу acc+erp 8.3.24) — allowlist для modify-property.
|
||||
@@ -2834,6 +2916,7 @@ function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) {
|
||||
$mapEntry = $script:complexPropertyMap[$propertyName]
|
||||
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
||||
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
|
||||
if ($mapEntry.mdref) { $values = @($values | ForEach-Object { Normalize-MDObjectRef "$_" $mapEntry.root }) }
|
||||
|
||||
$propEl = Find-PropertyElement $propertyName
|
||||
if (-not $propEl) {
|
||||
@@ -2883,6 +2966,7 @@ function Add-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 "$_" }) }
|
||||
if ($mapEntry -and $mapEntry.mdref) { $values = @($values | ForEach-Object { Normalize-MDObjectRef "$_" $mapEntry.root }) }
|
||||
$propEl = Find-PropertyElement $propertyName
|
||||
if (-not $propEl) {
|
||||
Warn "Property element '$propertyName' not found in Properties"
|
||||
@@ -2921,6 +3005,7 @@ function Set-ComplexProperty([string]$propertyName, [string[]]$values) {
|
||||
$mapEntry = $script:complexPropertyMap[$propertyName]
|
||||
if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return }
|
||||
if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) }
|
||||
if ($mapEntry.mdref) { $values = @($values | ForEach-Object { Normalize-MDObjectRef "$_" $mapEntry.root }) }
|
||||
|
||||
$propEl = Find-PropertyElement $propertyName
|
||||
if (-not $propEl) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-edit v1.19 — Edit existing 1C metadata object XML (+add-predefined предопределённые Ext/Predefined.xml)
|
||||
# meta-edit v1.23 — Edit existing 1C metadata object XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -1271,7 +1288,7 @@ def build_column_fragment(col_def, indent):
|
||||
if references:
|
||||
lines.append(f"{indent}\t\t<References>")
|
||||
for ref in references:
|
||||
lines.append(f'{indent}\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{ref}</xr:Item>')
|
||||
lines.append(f'{indent}\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(normalize_md_object_ref(str(ref)))}</xr:Item>')
|
||||
lines.append(f"{indent}\t\t</References>")
|
||||
else:
|
||||
lines.append(f"{indent}\t\t<References/>")
|
||||
@@ -1898,6 +1915,27 @@ def modify_properties(props_def):
|
||||
if isinstance(prop_value, bool):
|
||||
value_str = "true" if prop_value else "false"
|
||||
|
||||
# Structural value-type property (корневой <Type> у Константы, ПВХ) —
|
||||
# перестроить дескриптор типа через build_value_type_xml (не расплющивать в скаляр)
|
||||
if prop_name == "Type":
|
||||
type_indent = get_child_indent(properties_el)
|
||||
new_type_xml = build_value_type_xml(type_indent, value_str)
|
||||
new_type_nodes = import_fragment(new_type_xml)
|
||||
if new_type_nodes:
|
||||
type_idx = list(properties_el).index(prop_el)
|
||||
new_type_nodes[0].tail = prop_el.tail
|
||||
properties_el.insert(type_idx + 1, new_type_nodes[0])
|
||||
remove_node_with_whitespace(prop_el)
|
||||
info(f"Modified property: Type = {value_str}")
|
||||
modify_count += 1
|
||||
continue
|
||||
|
||||
# Guard: не расплющивать структурное свойство (с дочерними узлами) в скалярный текст —
|
||||
# это молча повредит XML. Завершаем ошибкой ДО записи файла.
|
||||
if len(list(prop_el)) > 0:
|
||||
print(f"meta-edit: modify-property: свойство '{prop_name}' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Set inner text — clear children first, set text
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
@@ -2197,13 +2235,56 @@ def process_modify(modify_def):
|
||||
# Complex property helpers
|
||||
# ============================================================
|
||||
|
||||
# Прощающий ввод MDObjectRef-путей: русские корни метаданных → английские + ссылочные формы
|
||||
# ("CatalogRef.Валюты"/"СправочникСсылка.Валюты" → "Catalog.Валюты"). MDObjectRef ссылается на ОБЪЕКТ
|
||||
# метаданных, а не на тип ссылки; вида метаданных, оканчивающегося на Ref, не существует → схлопывание
|
||||
# однозначно. Виды на ЧЁТНЫХ позициях (0,2,4…), имена (нечётные) не трогаем. Канонические английские
|
||||
# пути неизменны. Зеркало meta-compile.
|
||||
md_ref_roots = {
|
||||
'справочник': 'Catalog', 'документ': 'Document', 'перечисление': 'Enum', 'константа': 'Constant',
|
||||
'регистрсведений': 'InformationRegister', 'регистрнакопления': 'AccumulationRegister',
|
||||
'регистрбухгалтерии': 'AccountingRegister', 'регистррасчета': 'CalculationRegister', 'регистррасчёта': 'CalculationRegister',
|
||||
'плансчетов': 'ChartOfAccounts', 'планвидовхарактеристик': 'ChartOfCharacteristicTypes',
|
||||
'планвидоврасчета': 'ChartOfCalculationTypes', 'планвидоврасчёта': 'ChartOfCalculationTypes',
|
||||
'планобмена': 'ExchangePlan', 'бизнеспроцесс': 'BusinessProcess', 'задача': 'Task',
|
||||
'журналдокументов': 'DocumentJournal', 'отчет': 'Report', 'отчёт': 'Report', 'обработка': 'DataProcessor',
|
||||
'табличнаячасть': 'TabularSection', 'реквизит': 'Attribute', 'измерение': 'Dimension', 'ресурс': 'Resource',
|
||||
'стандартныйреквизит': 'StandardAttribute', 'значениеперечисления': 'EnumValue', 'команда': 'Command',
|
||||
'признакучета': 'AccountingFlag', 'признакучёта': 'AccountingFlag',
|
||||
'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum',
|
||||
'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes',
|
||||
'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan',
|
||||
'businessprocessref': 'BusinessProcess', 'taskref': 'Task',
|
||||
'справочникссылка': 'Catalog', 'документссылка': 'Document', 'перечислениессылка': 'Enum',
|
||||
'плансчетовссылка': 'ChartOfAccounts', 'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypes',
|
||||
'планвидоврасчетассылка': 'ChartOfCalculationTypes', 'планвидоврасчётассылка': 'ChartOfCalculationTypes',
|
||||
'планобменассылка': 'ExchangePlan', 'бизнеспроцессссылка': 'BusinessProcess', 'задачассылка': 'Task',
|
||||
}
|
||||
|
||||
|
||||
def normalize_md_object_ref(ref, default_root=None):
|
||||
"""default_root — корень для ГОЛОГО имени без точки (owners: "Валюты" → "Catalog.Валюты")."""
|
||||
if not ref:
|
||||
return ref
|
||||
if '.' not in ref:
|
||||
return f'{default_root}.{ref}' if default_root else ref
|
||||
parts = ref.split('.')
|
||||
for k in range(0, len(parts), 2):
|
||||
t = md_ref_roots.get(parts[k].lower())
|
||||
if t:
|
||||
parts[k] = t
|
||||
return '.'.join(parts)
|
||||
|
||||
|
||||
# mdref — значения списка суть MDObjectRef-пути → прогоняем через normalize_md_object_ref.
|
||||
# root — корень для голого имени без точки.
|
||||
complex_property_map = {
|
||||
"Owners": {"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"'},
|
||||
"Owners": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"', "mdref": True, "root": "Catalog"},
|
||||
"RegisterRecords": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"', "mdref": True},
|
||||
"BasedOn": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"', "mdref": True},
|
||||
"InputByString": {"tag": "xr:Field", "attr": None},
|
||||
"DataLockFields": {"tag": "xr:Field", "attr": None, "expand": True},
|
||||
"RegisteredDocuments": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"'},
|
||||
"RegisteredDocuments": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"', "mdref": True},
|
||||
}
|
||||
|
||||
# Известные свойства объекта (union по корпусу acc+erp 8.3.24) — allowlist для modify-property.
|
||||
@@ -2739,6 +2820,8 @@ def add_complex_property_item(property_name, values):
|
||||
return
|
||||
if map_entry.get("expand"):
|
||||
values = [expand_data_path(str(v)) for v in values]
|
||||
if map_entry.get("mdref"):
|
||||
values = [normalize_md_object_ref(str(v), map_entry.get("root")) for v in values]
|
||||
|
||||
prop_el = find_property_element(property_name)
|
||||
if prop_el is None:
|
||||
@@ -2781,6 +2864,8 @@ def remove_complex_property_item(property_name, values):
|
||||
map_entry = complex_property_map.get(property_name)
|
||||
if map_entry and map_entry.get("expand"):
|
||||
values = [expand_data_path(str(v)) for v in values]
|
||||
if map_entry and map_entry.get("mdref"):
|
||||
values = [normalize_md_object_ref(str(v), map_entry.get("root")) for v in values]
|
||||
prop_el = find_property_element(property_name)
|
||||
if prop_el is None:
|
||||
warn(f"Property element '{property_name}' not found in Properties")
|
||||
@@ -2813,6 +2898,8 @@ def set_complex_property(property_name, values):
|
||||
return
|
||||
if map_entry.get("expand"):
|
||||
values = [expand_data_path(str(v)) for v in values]
|
||||
if map_entry.get("mdref"):
|
||||
values = [normalize_md_object_ref(str(v), map_entry.get("root")) for v in values]
|
||||
|
||||
prop_el = find_property_element(property_name)
|
||||
if prop_el is None:
|
||||
@@ -2858,11 +2945,46 @@ def set_complex_property(property_name, values):
|
||||
# ============================================================
|
||||
|
||||
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||
if style and style["crlf"]:
|
||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||
return xml_bytes
|
||||
|
||||
|
||||
def save_xml(tree, path):
|
||||
"""Save XML tree with BOM and proper encoding declaration."""
|
||||
"""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")
|
||||
# Fix XML declaration quotes
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
# Fix d5p1 namespace declarations stripped by lxml (it treats them as unused
|
||||
# because d5p1: appears only in text content, not in element/attribute names)
|
||||
xml_bytes = re.sub(
|
||||
@@ -2870,10 +2992,10 @@ def save_xml(tree, path):
|
||||
b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2',
|
||||
xml_bytes
|
||||
)
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
xml_bytes += b"\n"
|
||||
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.3 — Compact summary of 1C metadata object
|
||||
# meta-info v1.4 — Compact summary of 1C metadata object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
|
||||
@@ -418,8 +418,19 @@ function Get-WSOperations($childObjs) {
|
||||
# --- Support status of this object (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||||
# object's support rule. 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-ObjectSupportStatus([string]$objUuid) {
|
||||
try {
|
||||
if (Test-ExternalObjectRoot $ObjectPath) { return $null }
|
||||
# Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin).
|
||||
$d = [System.IO.Path]::GetDirectoryName($ObjectPath)
|
||||
$binPath = $null
|
||||
@@ -653,7 +664,8 @@ if (-not $drillDone) {
|
||||
if ($synonym -and $synonym -ne $objName) { $header += " — `"$synonym`"" }
|
||||
$header += " ==="
|
||||
Out $header
|
||||
Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))"
|
||||
$support = Get-ObjectSupportStatus $typeNode.GetAttribute('uuid')
|
||||
if ($null -ne $support) { Out "Поддержка: $support" }
|
||||
|
||||
# --- Type presentation (ref objects) ---
|
||||
if ($isRefObject) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.3 — Compact summary of 1C metadata object (Python port)
|
||||
# meta-info v1.4 — Compact summary of 1C metadata object (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -472,8 +472,23 @@ def get_ws_operations(child_objs):
|
||||
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
|
||||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||||
# object's support rule. Never throws — degrades to "не на поддержке".
|
||||
def _meta_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 get_object_support_status(obj_uuid):
|
||||
try:
|
||||
if _meta_is_external_root(object_path):
|
||||
return None
|
||||
d = os.path.dirname(object_path)
|
||||
bin_path = None
|
||||
for _ in range(8):
|
||||
@@ -703,7 +718,9 @@ if not drill_done:
|
||||
header += f' \u2014 "{synonym}"'
|
||||
header += " ==="
|
||||
out(header)
|
||||
out(f"Поддержка: {get_object_support_status(type_node.get('uuid', ''))}")
|
||||
_support = get_object_support_status(type_node.get('uuid', ''))
|
||||
if _support is not None:
|
||||
out(f"Поддержка: {_support}")
|
||||
|
||||
# Type presentation (ref objects)
|
||||
if is_ref_object:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.5 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -93,6 +93,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -129,10 +139,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.5 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -258,13 +275,49 @@ def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.9 — Validate 1C metadata object structure
|
||||
# meta-validate v1.12 — Validate 1C metadata object structure (+корневой <Type>: скаляр без структуры = ошибка)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -558,6 +558,26 @@ if ($propsNode) {
|
||||
}
|
||||
}
|
||||
|
||||
# Корневой <Type> (дескриптор типа значения — Константа, ПВХ) должен быть структурным:
|
||||
# <v8:Type>/<v8:TypeSet>, а не скалярный текст. Скаляр = повреждённый тип (напр. после
|
||||
# старого meta-edit modify-property Type). См. issue #42.
|
||||
$rootTypeEl = $propsNode.SelectSingleNode("md:Type", $ns)
|
||||
if ($rootTypeEl) {
|
||||
$v8Types = $rootTypeEl.SelectNodes("v8:Type", $ns)
|
||||
$v8TypeSets = $rootTypeEl.SelectNodes("v8:TypeSet", $ns)
|
||||
$scalarText = ""
|
||||
foreach ($cn in $rootTypeEl.ChildNodes) {
|
||||
if ($cn.NodeType -eq 'Text' -or $cn.NodeType -eq 'CDATA') {
|
||||
$t = $cn.Value.Trim()
|
||||
if ($t) { $scalarText = $t; break }
|
||||
}
|
||||
}
|
||||
if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0 -and $scalarText) {
|
||||
Report-Error "4. Property <Type> содержит скалярный текст '$scalarText' без структуры типа (<v8:Type>/<v8:TypeSet>) — повреждённый дескриптор типа значения"
|
||||
$check4Ok = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($check4Ok) {
|
||||
Report-OK "4. Property values: $enumChecked enum properties checked"
|
||||
}
|
||||
@@ -1473,6 +1493,71 @@ if ($script:configDir) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 18: свойства, появившиеся в новых версиях формата ---
|
||||
# Реестр «тег → минимальная версия формата». Служит двум целям: (1) поймать свойство в файле со
|
||||
# слишком старым штампом — при сборке на старой платформе оно будет молча отброшено (платформа
|
||||
# рапортует успех, а свойство теряется); (2) подсказать, что конструкция требует более нового
|
||||
# формата. Расширяется одной строкой на свойство — задел под 2.21 (8.5) и последующие.
|
||||
$versionedProps = @{
|
||||
"TypeReductionMode" = "2.20" # режим приведения типов (стандартные реквизиты, измерения РС)
|
||||
"LineNumberLength" = "2.20" # длина номера строки ТЧ (5..9)
|
||||
}
|
||||
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||
function Get-FormatRank([string]$v) {
|
||||
if ($v -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||
return 0
|
||||
}
|
||||
$fileRank = Get-FormatRank $version
|
||||
if ($fileRank -gt 0) {
|
||||
foreach ($vp in ($versionedProps.Keys | Sort-Object)) {
|
||||
$nodes = $xmlDoc.SelectNodes("//md:$vp | //xr:$vp", $ns)
|
||||
if ($nodes -and $nodes.Count -gt 0 -and $fileRank -lt (Get-FormatRank $versionedProps[$vp])) {
|
||||
Report-Error "18. <$vp> появился в формате $($versionedProps[$vp]), а файл объявлен как $version — на платформе этой версии свойство будет отброшено при загрузке"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 19: LineNumberLength — допустимый диапазон 5..9 ---
|
||||
# Длина номера строки ТЧ: 5 (до 99 999 строк) … 9 (до 999 999 999). Границы — из документации 1С.
|
||||
foreach ($lnl in @($xmlDoc.SelectNodes("//md:LineNumberLength", $ns))) {
|
||||
$raw = $lnl.InnerText.Trim()
|
||||
if ($raw -notmatch '^\d+$') {
|
||||
Report-Error "19. LineNumberLength='$raw' — должно быть целое число 5..9"
|
||||
} elseif ([int]$raw -lt 5 -or [int]$raw -gt 9) {
|
||||
Report-Error "19. LineNumberLength=$raw вне допустимого диапазона 5..9"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 17: MDObjectRef form — ссылка должна указывать на ОБЪЕКТ метаданных, а не на тип ссылки ---
|
||||
# Owners/BasedOn/RegisterRecords/RegisteredDocuments/References содержат путь вида "Catalog.Валюты".
|
||||
# "CatalogRef.Валюты" — частая ошибка (тип ссылки вместо объекта): платформа отвечает
|
||||
# «Неизвестный объект метаданных». Вида метаданных, оканчивающегося на Ref, не существует → ERROR.
|
||||
# Неизвестный первый сегмент без Ref — только WARN (список видов может быть неполон).
|
||||
|
||||
$mdRefNodes = $xmlDoc.SelectNodes("//*[@xsi:type='xr:MDObjectRef']", $ns)
|
||||
if ($mdRefNodes -and $mdRefNodes.Count -gt 0) {
|
||||
$knownRoots = @($validTypes) + @($structuralOnlyTypes)
|
||||
$badRefForm = @{} # значение -> $true (ссылочная форма, гарантированно нерабочая)
|
||||
$unknownRoot = @{} # значение -> корень
|
||||
foreach ($rn in $mdRefNodes) {
|
||||
$rv = $rn.InnerText.Trim()
|
||||
if (-not $rv) { continue }
|
||||
$root = $rv.Split('.')[0]
|
||||
if ($knownRoots -ccontains $root) { continue }
|
||||
if ($root -cmatch 'Ref$') { $badRefForm[$rv] = $true } else { $unknownRoot[$rv] = $root }
|
||||
}
|
||||
foreach ($bk in ($badRefForm.Keys | Sort-Object)) {
|
||||
$fixed = $bk -replace '^([A-Za-z]+)Ref\.', '$1.'
|
||||
Report-Error "17. MDObjectRef '$bk' — ссылка на ТИП, а не на объект метаданных; нужно '$fixed' (иначе «Неизвестный объект метаданных» при загрузке)"
|
||||
}
|
||||
foreach ($uk in ($unknownRoot.Keys | Sort-Object)) {
|
||||
Report-Warn "17. MDObjectRef '$uk' — неизвестный вид метаданных '$($unknownRoot[$uk])' (опечатка?)"
|
||||
}
|
||||
if ($badRefForm.Count -eq 0 -and $unknownRoot.Count -eq 0) {
|
||||
Report-OK "17. MDObjectRef form: $($mdRefNodes.Count) checked"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Final output ---
|
||||
|
||||
& $finalize
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.9 — Validate 1C metadata object structure (Python port)
|
||||
# meta-validate v1.12 — Validate 1C metadata object structure (Python port) (+корневой <Type>: скаляр без структуры = ошибка)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -555,6 +555,18 @@ if props_node is not None:
|
||||
check4_ok = False
|
||||
enum_checked += 1
|
||||
|
||||
# Корневой <Type> (дескриптор типа значения — Константа, ПВХ) должен быть структурным:
|
||||
# <v8:Type>/<v8:TypeSet>, а не скалярный текст. Скаляр = повреждённый тип (напр. после
|
||||
# старого meta-edit modify-property Type). См. issue #42.
|
||||
root_type_el = find(props_node, "md:Type")
|
||||
if root_type_el is not None:
|
||||
scalar_text = inner_text(root_type_el).strip()
|
||||
v8_types = find_all(root_type_el, "v8:Type")
|
||||
v8_type_sets = find_all(root_type_el, "v8:TypeSet")
|
||||
if len(v8_types) == 0 and len(v8_type_sets) == 0 and scalar_text:
|
||||
report_error(f"4. Property <Type> содержит скалярный текст '{scalar_text}' без структуры типа (<v8:Type>/<v8:TypeSet>) — повреждённый дескриптор типа значения")
|
||||
check4_ok = False
|
||||
|
||||
if check4_ok:
|
||||
report_ok(f"4. Property values: {enum_checked} enum properties checked")
|
||||
else:
|
||||
@@ -1383,6 +1395,69 @@ if config_dir:
|
||||
elif checked_refs:
|
||||
report_ok(f"16. Reference types: {len(checked_refs)} resolved")
|
||||
|
||||
# ── Check 18: свойства, появившиеся в новых версиях формата ──
|
||||
# Реестр «тег → минимальная версия формата». Служит двум целям: (1) поймать свойство в файле со
|
||||
# слишком старым штампом — при сборке на старой платформе оно будет молча отброшено (платформа
|
||||
# рапортует успех, а свойство теряется); (2) подсказать, что конструкция требует более нового
|
||||
# формата. Расширяется одной строкой на свойство — задел под 2.21 (8.5) и последующие.
|
||||
versioned_props = {
|
||||
"TypeReductionMode": "2.20", # режим приведения типов (стандартные реквизиты, измерения РС)
|
||||
"LineNumberLength": "2.20", # длина номера строки ТЧ (5..9)
|
||||
}
|
||||
|
||||
|
||||
def format_rank(v):
|
||||
""""2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||
m = re.match(r'^(\d+)\.(\d+)$', v or '')
|
||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||
|
||||
|
||||
file_rank = format_rank(version)
|
||||
if file_rank > 0:
|
||||
for vp in sorted(versioned_props):
|
||||
nodes = find_all(root, f"//md:{vp} | //xr:{vp}")
|
||||
if nodes and file_rank < format_rank(versioned_props[vp]):
|
||||
report_error(f"18. <{vp}> появился в формате {versioned_props[vp]}, а файл объявлен как {version} — на платформе этой версии свойство будет отброшено при загрузке")
|
||||
|
||||
# ── Check 19: LineNumberLength — допустимый диапазон 5..9 ──
|
||||
# Длина номера строки ТЧ: 5 (до 99 999 строк) … 9 (до 999 999 999). Границы — из документации 1С.
|
||||
for lnl in find_all(root, "//md:LineNumberLength"):
|
||||
raw = inner_text(lnl).strip()
|
||||
if not re.match(r'^\d+$', raw):
|
||||
report_error(f"19. LineNumberLength='{raw}' — должно быть целое число 5..9")
|
||||
elif int(raw) < 5 or int(raw) > 9:
|
||||
report_error(f"19. LineNumberLength={raw} вне допустимого диапазона 5..9")
|
||||
|
||||
# ── Check 17: MDObjectRef form — ссылка на ОБЪЕКТ метаданных, а не на тип ссылки ──
|
||||
# Owners/BasedOn/RegisterRecords/RegisteredDocuments/References содержат путь вида "Catalog.Валюты".
|
||||
# "CatalogRef.Валюты" — частая ошибка (тип ссылки вместо объекта): платформа отвечает
|
||||
# «Неизвестный объект метаданных». Вида метаданных, оканчивающегося на Ref, не существует → ERROR.
|
||||
# Неизвестный первый сегмент без Ref — только WARN (список видов может быть неполон).
|
||||
|
||||
md_ref_nodes = find_all(root, "//*[@xsi:type='xr:MDObjectRef']")
|
||||
if md_ref_nodes:
|
||||
known_roots = tuple(valid_types) + tuple(structural_only_types)
|
||||
bad_ref_form = {} # значение -> True (ссылочная форма, гарантированно нерабочая)
|
||||
unknown_root = {} # значение -> корень
|
||||
for rn in md_ref_nodes:
|
||||
rv = inner_text(rn).strip()
|
||||
if not rv:
|
||||
continue
|
||||
rroot = rv.split('.')[0]
|
||||
if rroot in known_roots:
|
||||
continue
|
||||
if rroot.endswith('Ref'):
|
||||
bad_ref_form[rv] = True
|
||||
else:
|
||||
unknown_root[rv] = rroot
|
||||
for bk in sorted(bad_ref_form):
|
||||
fixed = re.sub(r'^([A-Za-z]+)Ref\.', r'\1.', bk)
|
||||
report_error(f"17. MDObjectRef '{bk}' — ссылка на ТИП, а не на объект метаданных; нужно '{fixed}' (иначе «Неизвестный объект метаданных» при загрузке)")
|
||||
for uk in sorted(unknown_root):
|
||||
report_warn(f"17. MDObjectRef '{uk}' — неизвестный вид метаданных '{unknown_root[uk]}' (опечатка?)")
|
||||
if not bad_ref_form and not unknown_root:
|
||||
report_ok(f"17. MDObjectRef form: {len(md_ref_nodes)} checked")
|
||||
|
||||
# ── Final output ──────────────────────────────────────────────
|
||||
|
||||
finalize()
|
||||
|
||||
@@ -34,16 +34,16 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
|
||||
|
||||
## Рабочий процесс
|
||||
|
||||
1. Claude пишет JSON-определение (Write tool) → файл `.json`
|
||||
2. Claude вызывает `/mxl-compile` для генерации Template.xml
|
||||
3. Claude вызывает `/mxl-validate` для проверки корректности
|
||||
4. Claude вызывает `/mxl-info` для верификации структуры
|
||||
1. Написать JSON-определение (Write tool) → файл `.json`
|
||||
2. Вызвать `/mxl-compile` для генерации Template.xml
|
||||
3. Вызвать `/mxl-validate` для проверки корректности
|
||||
4. Вызвать `/mxl-info` для верификации структуры
|
||||
|
||||
**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров.
|
||||
|
||||
## JSON-схема DSL
|
||||
|
||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON).
|
||||
Ниже — компактная структура и ключевые правила, достаточные для типового макета. Полные таблицы полей (все свойства шрифтов, стилей, ячеек), развёрнутый пример и ограничения формата — в **`reference/dsl-spec.md`**; нужны не всегда, читать по необходимости.
|
||||
|
||||
Краткая структура:
|
||||
|
||||
@@ -63,3 +63,4 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
|
||||
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
|
||||
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
|
||||
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
|
||||
- `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# Спецификация MXL DSL — JSON-формат описания табличного документа
|
||||
|
||||
Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML).
|
||||
|
||||
## Пример
|
||||
|
||||
```json
|
||||
{
|
||||
"columns": 10,
|
||||
"defaultWidth": 30,
|
||||
"columnWidths": { "1": 15, "2-8": 40, "9-10": 50 },
|
||||
|
||||
"fonts": {
|
||||
"default": { "face": "Arial", "size": 10 },
|
||||
"bold": { "face": "Arial", "size": 10, "bold": true },
|
||||
"header": { "face": "Arial", "size": 14, "bold": true }
|
||||
},
|
||||
|
||||
"styles": {
|
||||
"default": {},
|
||||
"header": { "font": "header", "align": "center" },
|
||||
"label": { "font": "bold" },
|
||||
"bordered": { "border": "all" },
|
||||
"bordered-right": { "border": "all", "align": "right" },
|
||||
"total-right": { "font": "bold", "border": "top", "align": "right" }
|
||||
},
|
||||
|
||||
"areas": [
|
||||
{
|
||||
"name": "Заголовок",
|
||||
"rows": [
|
||||
{ "height": 20, "cells": [
|
||||
{ "col": 1, "span": 10, "style": "header", "param": "ТекстЗаголовка" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ШапкаТаблицы",
|
||||
"rows": [
|
||||
{ "rowStyle": "bordered", "cells": [
|
||||
{ "col": 1, "text": "№" },
|
||||
{ "col": 2, "span": 6, "text": "Наименование" },
|
||||
{ "col": 9, "text": "Кол-во" },
|
||||
{ "col": 10, "text": "Сумма" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Строка",
|
||||
"rows": [
|
||||
{ "rowStyle": "bordered", "cells": [
|
||||
{ "col": 1, "param": "НомерСтроки" },
|
||||
{ "col": 2, "span": 6, "param": "Товар", "detail": "Номенклатура" },
|
||||
{ "col": 9, "style": "bordered-right", "param": "Количество" },
|
||||
{ "col": 10, "style": "bordered-right", "param": "Сумма" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Итого",
|
||||
"rows": [
|
||||
{ "cells": [
|
||||
{ "col": 8, "span": 2, "style": "total-right", "text": "Итого:" },
|
||||
{ "col": 10, "style": "total-right", "param": "Всего" }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Верхний уровень
|
||||
|
||||
| Поле | Обяз. | По умолч. | Описание |
|
||||
|------|:-----:|-----------|----------|
|
||||
| `columns` | да | — | Количество колонок |
|
||||
| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` |
|
||||
| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` |
|
||||
| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
|
||||
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
|
||||
| `styles` | нет | `{}` | Именованные стили |
|
||||
| `areas` | да | — | Массив именованных областей (порядок = порядок в документе) |
|
||||
|
||||
## Шрифты (`fonts.<name>`)
|
||||
|
||||
| Поле | По умолч. | Описание |
|
||||
|------|-----------|----------|
|
||||
| `face` | `"Arial"` | Имя шрифта |
|
||||
| `size` | `10` | Размер |
|
||||
| `bold` | `false` | Жирный |
|
||||
| `italic` | `false` | Курсив |
|
||||
| `underline` | `false` | Подчёркнутый |
|
||||
| `strikeout` | `false` | Зачёркнутый |
|
||||
|
||||
Шрифт `"default"` используется когда стиль не указывает шрифт явно. Если не определён, создаётся автоматически (Arial 10).
|
||||
|
||||
## Стили (`styles.<name>`)
|
||||
|
||||
| Поле | По умолч. | Описание |
|
||||
|------|-----------|----------|
|
||||
| `font` | `"default"` | Ссылка на имя шрифта |
|
||||
| `align` | — | `left`, `center`, `right` |
|
||||
| `valign` | — | `top`, `center` |
|
||||
| `border` | — | Стороны рамки: `all`, `top`, `bottom`, `left`, `right`, `none`. Через запятую: `"top,bottom"` |
|
||||
| `borderWidth` | `"thin"` | Толщина рамки: `thin` (1px) или `thick` (2px) |
|
||||
| `wrap` | `false` | Перенос текста |
|
||||
| `format` | — | Формат данных 1С: `"ЧЦ=15; ЧДЦ=2"`, `"ДФ=dd.MM.yyyy"` и т.д. |
|
||||
|
||||
## Области (`areas[]`)
|
||||
|
||||
| Поле | Обяз. | Описание |
|
||||
|------|:-----:|----------|
|
||||
| `name` | да | Имя области для `Макет.ПолучитьОбласть("Имя")` |
|
||||
| `rows` | да | Массив строк |
|
||||
|
||||
## Строки (`rows[]`)
|
||||
|
||||
| Поле | По умолч. | Описание |
|
||||
|------|-----------|----------|
|
||||
| `height` | — | Высота строки (если не задана, используется авто) |
|
||||
| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) |
|
||||
| `cells` | `[]` | Массив ячеек |
|
||||
| `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) |
|
||||
|
||||
Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`.
|
||||
|
||||
## Ячейки (`cells[]`)
|
||||
|
||||
| Поле | Обяз. | По умолч. | Описание |
|
||||
|------|:-----:|-----------|----------|
|
||||
| `col` | да | — | Позиция колонки (1-based) |
|
||||
| `span` | нет | `1` | Объединение по горизонтали (количество колонок) |
|
||||
| `rowspan` | нет | `1` | Объединение по вертикали (количество строк) |
|
||||
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
|
||||
| `param` | нет | — | Параметр заполнения |
|
||||
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
|
||||
| `text` | нет | — | Статический текст |
|
||||
| `template` | нет | — | Шаблонный текст с `[Параметр]` |
|
||||
|
||||
### Тип заполнения
|
||||
|
||||
Определяется автоматически по содержимому ячейки:
|
||||
- `param` → fillType=Parameter
|
||||
- `template` → fillType=Template
|
||||
- `text` → fillType=Text
|
||||
- ничего → без fillType (пустая ячейка или рамка)
|
||||
|
||||
## `rowStyle` — автозаполнение
|
||||
|
||||
Когда задан `rowStyle`, компилятор создаёт ячейки для ВСЕХ колонок строки. Позиции без явных ячеек заполняются пустыми ячейками с указанным стилем. Это обеспечивает сплошные рамки в табличных строках.
|
||||
|
||||
Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются.
|
||||
|
||||
## Ограничения
|
||||
|
||||
Текущая версия не поддерживает:
|
||||
- Множественные наборы колонок (`columnsID`)
|
||||
- Области типа Columns / Rectangle
|
||||
- Рисунки (штрихкоды, картинки)
|
||||
- Фон ячеек
|
||||
@@ -1,4 +1,4 @@
|
||||
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
|
||||
# mxl-compile v1.4 — Compile 1C spreadsheet from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -26,6 +26,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -62,10 +72,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
|
||||
# mxl-compile v1.4 — Compile 1C spreadsheet from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
|
||||
@@ -36,22 +36,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1"
|
||||
|
||||
Декомпиляция существующего макета для анализа или доработки:
|
||||
|
||||
1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml
|
||||
2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили)
|
||||
3. Claude вызывает `/mxl-compile` для генерации нового Template.xml
|
||||
4. Claude вызывает `/mxl-validate` для проверки
|
||||
1. Вызвать `/mxl-decompile` для получения JSON из Template.xml
|
||||
2. Проанализировать или изменить JSON (добавить области, поменять стили)
|
||||
3. Вызвать `/mxl-compile` для генерации нового Template.xml
|
||||
4. Вызвать `/mxl-validate` для проверки
|
||||
|
||||
## JSON-схема DSL
|
||||
|
||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool).
|
||||
|
||||
## Генерация имён
|
||||
|
||||
Скрипт автоматически генерирует осмысленные имена:
|
||||
|
||||
- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам
|
||||
- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств
|
||||
|
||||
## Детектирование `rowStyle`
|
||||
|
||||
Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода.
|
||||
Формат JSON на выходе — тот же DSL, что принимает `/mxl-compile`; его полное описание живёт в навыке `/mxl-compile`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# mxl-info v1.1 — Analyze 1C spreadsheet structure
|
||||
# mxl-info v1.2 — Analyze 1C spreadsheet structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Alias('Path')]
|
||||
@@ -321,6 +321,16 @@ if ($Format -eq "json") {
|
||||
exit 0
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
@@ -339,8 +349,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
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 $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -385,7 +397,8 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
$lines = @()
|
||||
|
||||
$lines += "=== $templateName ==="
|
||||
$lines += "Поддержка: $(Get-SupportStatusForPath $TemplatePath)"
|
||||
$support = Get-SupportStatusForPath $TemplatePath
|
||||
if ($null -ne $support) { $lines += "Поддержка: $support" }
|
||||
$lines += " Rows: $docHeight, Columns: $defaultColCount"
|
||||
|
||||
if ($columnSets.Count -eq 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# mxl-info v1.1 — Analyze 1C spreadsheet structure
|
||||
# mxl-info v1.2 — Analyze 1C spreadsheet structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -321,14 +321,29 @@ def get_support_status_for_path(target_path):
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
if is_external_root(rp):
|
||||
return None
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if is_external_root(d + ".xml"):
|
||||
return None
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
@@ -380,7 +395,9 @@ def get_support_status_for_path(target_path):
|
||||
lines = []
|
||||
|
||||
lines.append(f"=== {template_name} ===")
|
||||
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
|
||||
_support = get_support_status_for_path(template_path)
|
||||
if _support is not None:
|
||||
lines.append(f"Поддержка: {_support}")
|
||||
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
|
||||
|
||||
if len(column_sets) == 0:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# role-compile v1.7 — Compile 1C role from JSON
|
||||
# role-compile v1.8 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -26,6 +26,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -62,10 +72,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-compile v1.7 — Compile 1C role from JSON
|
||||
# role-compile v1.8 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# role-info v1.1 — Analyze 1C role rights
|
||||
# role-info v1.2 — Analyze 1C role rights
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath,
|
||||
@@ -145,6 +145,16 @@ foreach ($tpl in $tplNodes) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
@@ -163,8 +173,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
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 $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -209,7 +221,8 @@ $header = "=== Role: $roleName"
|
||||
if ($roleSynonym) { $header += " --- `"$roleSynonym`"" }
|
||||
$header += " ==="
|
||||
Out $header
|
||||
Out "Поддержка: $(Get-SupportStatusForPath $RightsPath)"
|
||||
$support = Get-SupportStatusForPath $RightsPath
|
||||
if ($null -ne $support) { Out "Поддержка: $support" }
|
||||
Out ""
|
||||
|
||||
Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-info v1.1 — Analyze 1C role rights
|
||||
# role-info v1.2 — Analyze 1C role rights
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -161,14 +161,29 @@ def get_support_status_for_path(target_path):
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
if is_external_root(rp):
|
||||
return None
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if is_external_root(d + ".xml"):
|
||||
return None
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
@@ -222,7 +237,9 @@ if role_synonym:
|
||||
header += f' --- "{role_synonym}"'
|
||||
header += " ==="
|
||||
out(header)
|
||||
out(f"Поддержка: {get_support_status_for_path(rights_path)}")
|
||||
_support = get_support_status_for_path(rights_path)
|
||||
if _support is not None:
|
||||
out(f"Поддержка: {_support}")
|
||||
out()
|
||||
|
||||
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-compile v1.107 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.109 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -25,6 +25,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -61,10 +71,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -3189,12 +3202,15 @@ function Emit-TableAxisBlock {
|
||||
if ($block.filter) {
|
||||
Emit-Filter -items $block.filter -indent $indent
|
||||
}
|
||||
if ($block.order) {
|
||||
Emit-Order -items $block.order -indent $indent
|
||||
}
|
||||
if ($block.selection) {
|
||||
Emit-Selection -items $block.selection -indent $indent
|
||||
}
|
||||
# Платформа на осях (column/row/point/series) всегда пишет order+selection; при отсутствии
|
||||
# ключа кладёт Auto (как ручное добавление оси в конфигураторе). Ключ присутствует (в т.ч.
|
||||
# пустой [] ) — уважаем как задано.
|
||||
$hasOrderKey = $block.PSObject.Properties.Match('order').Count -gt 0
|
||||
$orderItems = if ($hasOrderKey) { $block.order } else { @('Auto') }
|
||||
Emit-Order -items $orderItems -indent $indent
|
||||
$hasSelKey = $block.PSObject.Properties.Match('selection').Count -gt 0
|
||||
$selItems = if ($hasSelKey) { $block.selection } else { @('Auto') }
|
||||
Emit-Selection -items $selItems -indent $indent
|
||||
if ($block.conditionalAppearance) {
|
||||
Emit-ConditionalAppearance -items $block.conditionalAppearance -indent $indent
|
||||
}
|
||||
@@ -3249,13 +3265,15 @@ function Emit-StructureItem {
|
||||
$gb = if ($item.groupBy) { $item.groupBy } else { $item.groupFields }
|
||||
Emit-GroupItems -groupBy $gb -indent "$indent`t"
|
||||
|
||||
# Emit order/selection only if specified — platform doesn't always emit them on group
|
||||
if ($item.order) {
|
||||
Emit-Order -items $item.order -indent "$indent`t" -blockViewMode $item.orderViewMode -blockUserSettingID $item.orderUserSettingID
|
||||
}
|
||||
if ($item.selection) {
|
||||
Emit-Selection -items $item.selection -indent "$indent`t"
|
||||
}
|
||||
# Платформа на группировке (плоской и вложенной в ось, short/explicit) всегда пишет
|
||||
# order+selection; при отсутствии ключа кладёт Auto. Ключ присутствует (в т.ч. пустой [])
|
||||
# — уважаем как задано (blockViewMode/userSettingID имеют смысл только при явном order).
|
||||
$hasGrpOrderKey = $item.PSObject.Properties.Match('order').Count -gt 0
|
||||
$grpOrderItems = if ($hasGrpOrderKey) { $item.order } else { @('Auto') }
|
||||
Emit-Order -items $grpOrderItems -indent "$indent`t" -blockViewMode $item.orderViewMode -blockUserSettingID $item.orderUserSettingID
|
||||
$hasGrpSelKey = $item.PSObject.Properties.Match('selection').Count -gt 0
|
||||
$grpSelItems = if ($hasGrpSelKey) { $item.selection } else { @('Auto') }
|
||||
Emit-Selection -items $grpSelItems -indent "$indent`t"
|
||||
|
||||
Emit-Filter -items $item.filter -indent "$indent`t"
|
||||
|
||||
@@ -3409,8 +3427,11 @@ function Emit-StructureItem {
|
||||
}
|
||||
}
|
||||
|
||||
# Selection (chart values)
|
||||
Emit-Selection -items $item.selection -indent "$indent`t"
|
||||
# Selection (chart values) — платформа всегда пишет chart-level selection; при отсутствии
|
||||
# ключа кладёт Auto.
|
||||
$hasChartSelKey = $item.PSObject.Properties.Match('selection').Count -gt 0
|
||||
$chartSelItems = if ($hasChartSelKey) { $item.selection } else { @('Auto') }
|
||||
Emit-Selection -items $chartSelItems -indent "$indent`t"
|
||||
|
||||
if ($item.outputParameters) {
|
||||
Emit-OutputParameters -params $item.outputParameters -indent "$indent`t"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-compile v1.107 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.109 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -2614,10 +2631,13 @@ def emit_table_axis_block(lines, block, indent, emit_name=True):
|
||||
emit_group_items(lines, gb, indent)
|
||||
if block.get('filter'):
|
||||
emit_filter(lines, block['filter'], indent)
|
||||
if block.get('order'):
|
||||
emit_order(lines, block['order'], indent)
|
||||
if block.get('selection'):
|
||||
emit_selection(lines, block['selection'], indent)
|
||||
# Платформа на осях (column/row/point/series) всегда пишет order+selection; при отсутствии
|
||||
# ключа кладёт Auto (как ручное добавление оси в конфигураторе). Ключ присутствует (в т.ч.
|
||||
# пустой [] ) — уважаем как задано.
|
||||
order_items = block['order'] if 'order' in block else ['Auto']
|
||||
emit_order(lines, order_items, indent)
|
||||
sel_items = block['selection'] if 'selection' in block else ['Auto']
|
||||
emit_selection(lines, sel_items, indent)
|
||||
if block.get('conditionalAppearance'):
|
||||
emit_conditional_appearance(lines, block['conditionalAppearance'], indent)
|
||||
if block.get('outputParameters'):
|
||||
@@ -2657,11 +2677,13 @@ def emit_structure_item(lines, item, indent, short_group=False):
|
||||
|
||||
emit_group_items(lines, item.get('groupBy') or item.get('groupFields'), f'{indent}\t')
|
||||
|
||||
# Emit order/selection only if specified — platform doesn't always emit them on group
|
||||
if item.get('order'):
|
||||
emit_order(lines, item['order'], f'{indent}\t', block_view_mode=item.get('orderViewMode'), block_user_setting_id=item.get('orderUserSettingID'))
|
||||
if item.get('selection'):
|
||||
emit_selection(lines, item['selection'], f'{indent}\t')
|
||||
# Платформа на группировке (плоской и вложенной в ось, short/explicit) всегда пишет
|
||||
# order+selection; при отсутствии ключа кладёт Auto. Ключ присутствует (в т.ч. пустой [])
|
||||
# — уважаем как задано (blockViewMode/userSettingID имеют смысл только при явном order).
|
||||
grp_order_items = item['order'] if 'order' in item else ['Auto']
|
||||
emit_order(lines, grp_order_items, f'{indent}\t', block_view_mode=item.get('orderViewMode'), block_user_setting_id=item.get('orderUserSettingID'))
|
||||
grp_sel_items = item['selection'] if 'selection' in item else ['Auto']
|
||||
emit_selection(lines, grp_sel_items, f'{indent}\t')
|
||||
|
||||
emit_filter(lines, item.get('filter'), f'{indent}\t')
|
||||
|
||||
@@ -2766,8 +2788,10 @@ def emit_structure_item(lines, item, indent, short_group=False):
|
||||
emit_table_axis_block(lines, sb, f'{indent}\t\t')
|
||||
lines.append(f'{indent}\t</dcsset:series>')
|
||||
|
||||
# Selection (chart values)
|
||||
emit_selection(lines, item.get('selection'), f'{indent}\t')
|
||||
# Selection (chart values) — платформа всегда пишет chart-level selection; при отсутствии
|
||||
# ключа кладёт Auto.
|
||||
chart_sel_items = item['selection'] if 'selection' in item else ['Auto']
|
||||
emit_selection(lines, chart_sel_items, f'{indent}\t')
|
||||
|
||||
if item.get('outputParameters'):
|
||||
emit_output_parameters(lines, item['outputParameters'], f'{indent}\t')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-decompile v0.90 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# skd-decompile v0.91 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -2223,18 +2223,20 @@ function Build-TableAxisBlock {
|
||||
foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "$loc/filter") }
|
||||
$entry['filter'] = $fa
|
||||
}
|
||||
# order — preserve presence (even [Auto]) for bit-perfect round-trip
|
||||
# order/selection — всегда явные (принцип «декомпилятор всегда явный»): [Auto] сохраняем как есть,
|
||||
# отсутствие/пустоту эмитим как [] — иначе compile впаяет дефолтный Auto (round-trip рвётся на
|
||||
# осях без выбора, напр. ветки use=false). [] на входе compile → эмитит ничего = «нет выбора».
|
||||
# NB: прямое присваивание @() (не через if-выражение — там пустой массив схлопнется в $null).
|
||||
$ordNode = $node.SelectSingleNode("dcsset:order", $ns)
|
||||
if ($ordNode) {
|
||||
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
|
||||
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems }
|
||||
}
|
||||
# selection — preserve presence (even [Auto])
|
||||
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems } else { $entry['order'] = @() }
|
||||
} else { $entry['order'] = @() }
|
||||
$selNode = $node.SelectSingleNode("dcsset:selection", $ns)
|
||||
if ($selNode) {
|
||||
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
|
||||
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems }
|
||||
}
|
||||
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems } else { $entry['selection'] = @() }
|
||||
} else { $entry['selection'] = @() }
|
||||
# conditionalAppearance block
|
||||
$caN = $node.SelectSingleNode("dcsset:conditionalAppearance", $ns)
|
||||
if ($caN) {
|
||||
@@ -2381,11 +2383,13 @@ function Build-Structure {
|
||||
$entry['series'] = $sArr
|
||||
}
|
||||
# Selection (chart values) — сохраняем даже [Auto] для bit-perfect presence
|
||||
# chart-level selection — всегда явно ([] при отсутствии/пустоте, иначе compile впаяет Auto).
|
||||
# NB: прямое присваивание @() (не через if-выражение — пустой массив там схлопнется в $null).
|
||||
$selN = $it.SelectSingleNode("dcsset:selection", $ns)
|
||||
if ($selN) {
|
||||
$selI = Build-Selection -selNode $selN -loc "$loc/$idx/selection"
|
||||
if ($selI.Count -gt 0) { $entry['selection'] = $selI }
|
||||
}
|
||||
if ($selI.Count -gt 0) { $entry['selection'] = $selI } else { $entry['selection'] = @() }
|
||||
} else { $entry['selection'] = @() }
|
||||
$opN = $it.SelectSingleNode("dcsset:outputParameters", $ns)
|
||||
$op = Build-OutputParameters -opNode $opN
|
||||
if ($op -and $op.Count -gt 0) { $entry['outputParameters'] = $op }
|
||||
@@ -2427,17 +2431,21 @@ function Build-Structure {
|
||||
$gFields = Get-GroupFields -parentNode $it -loc $loc
|
||||
if ($gFields.Count -gt 0) { $entry['groupFields'] = $gFields }
|
||||
|
||||
# Local selection — preserve presence (even [Auto]) for bit-perfect round-trip
|
||||
# Local selection/order — всегда явные: [Auto] как есть, отсутствие/пустоту как [] (иначе compile
|
||||
# впаяет дефолтный Auto → round-trip рвётся на группах без выбора, напр. ветки use=false).
|
||||
# [] не Auto-only → Try-StructureShorthand не свернёт такую группу в shorthand (и не добавит Auto).
|
||||
# NB: прямое присваивание @() (не через if-выражение — пустой массив там схлопнется в $null).
|
||||
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
|
||||
if ($selNode) {
|
||||
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
|
||||
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems }
|
||||
}
|
||||
# Local order — same
|
||||
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems } else { $entry['selection'] = @() }
|
||||
} else { $entry['selection'] = @() }
|
||||
$ordNode = $it.SelectSingleNode("dcsset:order", $ns)
|
||||
if ($ordNode) {
|
||||
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
|
||||
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems }
|
||||
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems } else { $entry['order'] = @() }
|
||||
} else { $entry['order'] = @() }
|
||||
if ($ordNode) {
|
||||
# Block-level viewMode/userSettingID на <dcsset:order>
|
||||
foreach ($ch in $ordNode.ChildNodes) {
|
||||
if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne 'http://v8.1c.ru/8.1/data-composition-system/settings') { continue }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-decompile v0.90 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# skd-decompile v0.91 — Decompile 1C DCS Template.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -2327,16 +2327,14 @@ def build_table_axis_block(node, loc, include_name=False):
|
||||
for fc in f_node.select_nodes("dcsset:item"):
|
||||
fa.append(build_filter_item(fc, "%s/filter" % loc))
|
||||
entry['filter'] = fa
|
||||
# order/selection — всегда явные ([Auto] как есть, отсутствие/пустоту как []): иначе compile
|
||||
# впаяет дефолтный Auto → round-trip рвётся на осях без выбора (напр. ветки use=false).
|
||||
ord_node = node.select_single_node("dcsset:order")
|
||||
if ord_node:
|
||||
ord_items = build_order(ord_node, "%s/order" % loc)
|
||||
if len(ord_items) > 0:
|
||||
entry['order'] = ord_items
|
||||
ord_items = build_order(ord_node, "%s/order" % loc) if ord_node else []
|
||||
entry['order'] = ord_items if len(ord_items) > 0 else []
|
||||
sel_node = node.select_single_node("dcsset:selection")
|
||||
if sel_node:
|
||||
sel_items = build_selection(sel_node, "%s/selection" % loc)
|
||||
if len(sel_items) > 0:
|
||||
entry['selection'] = sel_items
|
||||
sel_items = build_selection(sel_node, "%s/selection" % loc) if sel_node else []
|
||||
entry['selection'] = sel_items if len(sel_items) > 0 else []
|
||||
ca_n = node.select_single_node("dcsset:conditionalAppearance")
|
||||
if ca_n:
|
||||
ca = build_conditional_appearance(ca_n, "%s/ca" % loc)
|
||||
@@ -2485,11 +2483,10 @@ def build_structure(node, loc):
|
||||
s_arr.append(build_table_axis_block(s, "%s/%d/series[%d]" % (loc, idx, si)))
|
||||
si += 1
|
||||
entry['series'] = s_arr
|
||||
# chart-level selection — всегда явно ([] при отсутствии/пустоте, иначе compile впаяет Auto)
|
||||
sel_n = it.select_single_node("dcsset:selection")
|
||||
if sel_n:
|
||||
sel_i = build_selection(sel_n, "%s/%d/selection" % (loc, idx))
|
||||
if len(sel_i) > 0:
|
||||
entry['selection'] = sel_i
|
||||
sel_i = build_selection(sel_n, "%s/%d/selection" % (loc, idx)) if sel_n else []
|
||||
entry['selection'] = sel_i if len(sel_i) > 0 else []
|
||||
op_n = it.select_single_node("dcsset:outputParameters")
|
||||
op = build_output_parameters(op_n)
|
||||
if op and len(op) > 0:
|
||||
@@ -2532,16 +2529,16 @@ def build_structure(node, loc):
|
||||
if len(g_fields) > 0:
|
||||
entry['groupFields'] = g_fields
|
||||
|
||||
# Local selection/order — всегда явные ([Auto] как есть, отсутствие/пустоту как []): иначе
|
||||
# compile впаяет дефолтный Auto → round-trip рвётся на группах без выбора (напр. use=false).
|
||||
# [] не Auto-only → try_structure_shorthand не свернёт такую группу в shorthand (без Auto).
|
||||
sel_node = it.select_single_node("dcsset:selection")
|
||||
if sel_node:
|
||||
sel_items = build_selection(sel_node, "%s/selection" % loc)
|
||||
if len(sel_items) > 0:
|
||||
entry['selection'] = sel_items
|
||||
sel_items = build_selection(sel_node, "%s/selection" % loc) if sel_node else []
|
||||
entry['selection'] = sel_items if len(sel_items) > 0 else []
|
||||
ord_node = it.select_single_node("dcsset:order")
|
||||
ord_items = build_order(ord_node, "%s/order" % loc) if ord_node else []
|
||||
entry['order'] = ord_items if len(ord_items) > 0 else []
|
||||
if ord_node:
|
||||
ord_items = build_order(ord_node, "%s/order" % loc)
|
||||
if len(ord_items) > 0:
|
||||
entry['order'] = ord_items
|
||||
for ch in ord_node.child_nodes:
|
||||
if ch.namespace_uri != NS_SET:
|
||||
continue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.28 — Atomic 1C DCS editor
|
||||
# skd-edit v1.30 — Atomic 1C DCS editor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: парный .py собирает выражения автодат вне f-string ради совместимости с python 3.9 (PEP 701).
|
||||
param(
|
||||
@@ -64,6 +64,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -100,10 +110,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.28 — Atomic 1C DCS editor (Python port)
|
||||
# skd-edit v1.30 — Atomic 1C DCS editor (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -141,6 +141,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -180,6 +192,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -187,6 +202,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -1967,6 +1984,12 @@ raw_root_opening = _root_open_m.group(0) if _root_open_m else None
|
||||
|
||||
# Detect line ending convention so save can normalize back to whatever the source used.
|
||||
line_ending = "\r\n" if "\r\n" in raw_original_text else "\n"
|
||||
# Round-trip: сохранить BOM / регистр encoding / финальный перенос как в оригинале.
|
||||
_skd_had_bom = raw_original_bytes.startswith(b"\xef\xbb\xbf")
|
||||
_skd_body = raw_original_bytes[3:] if _skd_had_bom else raw_original_bytes
|
||||
_skd_enc_m = re.search(rb'encoding="([^"]+)"', _skd_body[:200])
|
||||
_skd_enc = _skd_enc_m.group(1).decode("ascii") if _skd_enc_m else "utf-8"
|
||||
_skd_final_nl = _skd_body.endswith(b"\n")
|
||||
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
@@ -3406,7 +3429,10 @@ if not dirty:
|
||||
sys.exit(0)
|
||||
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
# Round-trip: восстановить регистр encoding как в оригинале.
|
||||
xml_bytes = xml_bytes.replace(
|
||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||
b'<?xml version="1.0" encoding="' + _skd_enc.encode("ascii") + b'"?>')
|
||||
|
||||
# Format-preserve post-processing (mirrors PS path):
|
||||
# (1) restore the original raw <DataCompositionSchema ...> opening tag — lxml collapses
|
||||
@@ -3418,17 +3444,24 @@ if raw_root_opening:
|
||||
# defensive — strip any space before `/>` so PS and PY ports stay byte-equivalent.
|
||||
xml_text = re.sub(r"(?<=\S) />", "/>", xml_text)
|
||||
|
||||
# Normalize line endings to match source.
|
||||
# Канонизировать переносы к LF (убирает возможный ), затем к стилю источника.
|
||||
xml_text = xml_text.replace(" \n", "\n").replace(" ", "").replace("\r\n", "\n").replace("\r", "\n")
|
||||
if line_ending == "\r\n":
|
||||
xml_text = re.sub(r"(?<!\r)\n", "\r\n", xml_text)
|
||||
else:
|
||||
xml_text = xml_text.replace("\r\n", "\n")
|
||||
xml_text = xml_text.replace("\n", "\r\n")
|
||||
xml_bytes = xml_text.encode("utf-8")
|
||||
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
xml_bytes += b"\n"
|
||||
# Финальный перенос — как в оригинале.
|
||||
if line_ending == "\r\n":
|
||||
xml_bytes = xml_bytes.rstrip(b"\r\n")
|
||||
if _skd_final_nl:
|
||||
xml_bytes += b"\r\n"
|
||||
else:
|
||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||
if _skd_final_nl:
|
||||
xml_bytes += b"\n"
|
||||
with open(resolved_path, "wb") as f:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
if _skd_had_bom:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
f.write(xml_bytes)
|
||||
|
||||
print(f"[OK] Saved {resolved_path}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-info v1.7 — Analyze 1C DCS structure
|
||||
# skd-info v1.8 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
@@ -334,6 +334,16 @@ for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
|
||||
|
||||
$totalXmlLines = (Get-Content $resolvedPath).Count
|
||||
|
||||
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) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
@@ -352,8 +362,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
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 $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -395,7 +407,8 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
|
||||
function Show-Overview {
|
||||
$lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===")
|
||||
$lines.Add("Поддержка: $(Get-SupportStatusForPath $TemplatePath)")
|
||||
$support = Get-SupportStatusForPath $TemplatePath
|
||||
if ($null -ne $support) { $lines.Add("Поддержка: $support") }
|
||||
$lines.Add("")
|
||||
|
||||
# Sources
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-info v1.7 — Analyze 1C DCS structure
|
||||
# skd-info v1.8 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -278,14 +278,29 @@ def get_support_status_for_path(target_path):
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
if is_external_root(rp):
|
||||
return None
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if is_external_root(d + ".xml"):
|
||||
return None
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
@@ -424,7 +439,9 @@ def main():
|
||||
|
||||
def show_overview():
|
||||
lines.append(f"=== DCS: {template_name} ({total_xml_lines} lines) ===")
|
||||
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
|
||||
_support = get_support_status_for_path(template_path)
|
||||
if _support is not None:
|
||||
lines.append(f"Поддержка: {_support}")
|
||||
lines.append("")
|
||||
|
||||
# Sources
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-compile v1.8 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.9 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -63,6 +63,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -99,10 +109,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-compile v1.8 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.9 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-edit v1.5 — Edit existing 1C subsystem XML
|
||||
# subsystem-edit v1.7 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
|
||||
@@ -136,6 +136,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -172,10 +182,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-edit v1.5 — Edit existing 1C subsystem XML
|
||||
# subsystem-edit v1.7 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -416,13 +433,49 @@ def parse_value_list(val):
|
||||
return [val]
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-info v1.1 — Compact summary of 1C subsystem structure
|
||||
# subsystem-info v1.2 — Compact summary of 1C subsystem structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath,
|
||||
@@ -17,6 +17,16 @@ $ErrorActionPreference = 'Stop'
|
||||
$script:lines = @()
|
||||
function Out([string]$text) { $script:lines += $text }
|
||||
|
||||
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) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
@@ -35,8 +45,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
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 $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -175,7 +187,8 @@ function Get-SubsystemDir([string]$xmlPath) {
|
||||
# --- Show functions for full mode ---
|
||||
function Show-Overview {
|
||||
Out "Подсистема: $subName"
|
||||
Out "Поддержка: $(Get-SupportStatusForPath $SubsystemPath)"
|
||||
$support = Get-SupportStatusForPath $SubsystemPath
|
||||
if ($null -ne $support) { Out "Поддержка: $support" }
|
||||
if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" }
|
||||
if ($commentText) { Out "Комментарий: $commentText" }
|
||||
Out "ВключатьВКомандныйИнтерфейс: $inclCI"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-info v1.1 — Compact summary of 1C subsystem structure
|
||||
# subsystem-info v1.2 — Compact summary of 1C subsystem structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -150,14 +150,29 @@ def get_support_status_for_path(target_path):
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
if is_external_root(rp):
|
||||
return None
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if is_external_root(d + ".xml"):
|
||||
return None
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
@@ -208,7 +223,9 @@ def get_support_status_for_path(target_path):
|
||||
def show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
|
||||
explanation, pic_text, content_items, groups, child_names, has_ci):
|
||||
out(f"Подсистема: {sub_name}")
|
||||
out(f"Поддержка: {get_support_status_for_path(subsystem_path)}")
|
||||
_support = get_support_status_for_path(subsystem_path)
|
||||
if _support is not None:
|
||||
out(f"Поддержка: {_support}")
|
||||
if synonym and synonym != sub_name:
|
||||
out(f"Синоним: {synonym}")
|
||||
if comment_text:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# template-add v1.7 — Add template to 1C object
|
||||
# template-add v1.10 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -38,6 +38,16 @@ function Get-RootUuid([string]$xmlPath) {
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
@@ -74,10 +84,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if (Test-ExternalObjectRoot $rp) { return }
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
@@ -316,7 +329,10 @@ if (-not $childObjects) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Добавить <Template> в конец ChildObjects
|
||||
# Добавить <Template> в конец ChildObjects — идемпотентно (не дублировать уже зарегистрированный)
|
||||
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Template[text()='$TemplateName']", $nsMgr)
|
||||
|
||||
if (-not $alreadyRegistered) {
|
||||
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$templateElem.InnerText = $TemplateName
|
||||
|
||||
@@ -336,6 +352,7 @@ if ($childObjects.ChildNodes.Count -eq 0) {
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
|
||||
|
||||
@@ -379,6 +396,9 @@ $writer.Close()
|
||||
$stream.Close()
|
||||
|
||||
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
|
||||
if ($alreadyRegistered) {
|
||||
Write-Host " Already registered: <Template>$TemplateName</Template> in ChildObjects (skipped duplicate)"
|
||||
}
|
||||
Write-Host " Метаданные: $templateMetaPath"
|
||||
Write-Host " Содержимое: $templateFilePath"
|
||||
if ($mainDCSUpdated) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-template v1.7 — Add template to 1C object
|
||||
# add-template v1.10 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
|
||||
return None
|
||||
|
||||
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||
if _sg_is_external_root(rp):
|
||||
return
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
@@ -181,14 +198,50 @@ TYPE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
@@ -393,31 +446,34 @@ def main():
|
||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Add <Template> to end of ChildObjects
|
||||
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
|
||||
template_elem.text = template_name
|
||||
# Remove auto-appended element to reinsert with proper whitespace
|
||||
child_objects.remove(template_elem)
|
||||
# Add <Template> to end of ChildObjects — idempotent (do not duplicate already-registered template)
|
||||
already_registered = child_objects.find(f"md:Template[.='{template_name}']", NSMAP) is not None
|
||||
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = "\n\t\t"
|
||||
else:
|
||||
if len(children) > 0:
|
||||
last_child = children[-1]
|
||||
# last_child.tail is the trailing whitespace before </ChildObjects>
|
||||
old_tail = last_child.tail
|
||||
last_child.tail = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||
else:
|
||||
# Has text content but no element children
|
||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||
if not already_registered:
|
||||
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
|
||||
template_elem.text = template_name
|
||||
# Remove auto-appended element to reinsert with proper whitespace
|
||||
child_objects.remove(template_elem)
|
||||
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = "\n\t\t"
|
||||
else:
|
||||
if len(children) > 0:
|
||||
last_child = children[-1]
|
||||
# last_child.tail is the trailing whitespace before </ChildObjects>
|
||||
old_tail = last_child.tail
|
||||
last_child.tail = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||
else:
|
||||
# Has text content but no element children
|
||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = "\n\t\t"
|
||||
|
||||
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
|
||||
|
||||
@@ -447,6 +503,8 @@ def main():
|
||||
save_xml_with_bom(tree, root_xml_full)
|
||||
|
||||
print(f"[OK] Создан макет: {template_name} ({template_type})")
|
||||
if already_registered:
|
||||
print(f" Already registered: <Template>{template_name}</Template> in ChildObjects (skipped duplicate)")
|
||||
print(f" Метаданные: {template_meta_path}")
|
||||
print(f" Содержимое: {template_file_path}")
|
||||
if main_dcs_updated:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# template-remove v1.2 — Remove template from 1C object
|
||||
# template-remove v1.3 — Remove template from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# remove-template v1.1 — Remove template from 1C object
|
||||
# remove-template v1.3 — Remove template from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -13,14 +13,50 @@ from lxml import etree
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
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"
|
||||
# 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:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,10 @@ node $RUN run <url> script.js # exits when done, no session
|
||||
### Interactive mode (step-by-step development)
|
||||
|
||||
```bash
|
||||
# 1. Start session (run_in_background=true, prints JSON when ready)
|
||||
node $RUN start <url>
|
||||
# 1. Start session in the background — `start` stays running as the server, so don't wait on
|
||||
# its stdout. Poll `status` instead: it exits 0 only once the session is loaded and live.
|
||||
node $RUN start <url> # run_in_background=true
|
||||
until node $RUN status >/dev/null 2>&1; do sleep 2; done # exit 0 = ready
|
||||
|
||||
# 2. Execute scripts against running session
|
||||
cat <<'SCRIPT' | node $RUN exec -
|
||||
@@ -127,7 +129,7 @@ Switch to an already-open tab/window (fuzzy match).
|
||||
|
||||
### Reading form state
|
||||
|
||||
#### `getFormState()` → `{ form, formCount, openForms, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }`
|
||||
#### `getFormState()` → `{ form, formCount, openForms, title, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }`
|
||||
Returns current form structure. This is the primary way to understand what's on screen.
|
||||
|
||||
**form** — active form number, or `null` when no form is open (desktop).
|
||||
@@ -140,10 +142,19 @@ Returns current form structure. This is the primary way to understand what's on
|
||||
|
||||
**openTabs** — array of `{ name, active? }` from the open-windows tab bar. Only present when the tab bar is enabled in 1C settings. Do NOT rely on this — use `formCount`/`openForms` instead.
|
||||
|
||||
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields)
|
||||
**title** — caption of the active form (`"Контрагенты"`, `"Заказ поставщику ТД00-000052 от 05.07.2022"`). Read from the form's own header, which does not depend on the open-windows tab bar; when the form shows no header, falls back to the active tab's caption, and is `null` when neither is available.
|
||||
|
||||
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields), `disabled?` (control is unavailable). `buttons[]` carry `disabled?` too.
|
||||
|
||||
**navigation** — form navigation panel links (for objects with subordinate catalogs): `[{ name, active? }]`. Clickable via `clickElement()`. Only present when the form has a navigation panel (e.g. "Основное", "Объекты метаданных", "Подсистемы").
|
||||
|
||||
**groups** — collapsible and pop-up form groups: `[{ name, title, collapsed, behavior? }]`. `collapsed: true` means the group's content is hidden — part of the form is not shown until you expand it (common on settings pages like "Администрирование → Интернет-поддержка и сервисы"). `behavior: 'popup'` marks a pop-up group (content shows in a floating panel); absent for ordinary collapsible groups. Expand/collapse (or open/close a pop-up) by the group title with `clickElement`, same vocabulary as tree nodes: `{ expand: true }` reveals (idempotent), `{ expand: false }` hides, `{ toggle: true }` flips. After expanding, the group's content becomes readable in the next `getFormState()` (its fields/hyperlinks/texts appear). Plain (non-collapsible) groups are not listed.
|
||||
```js
|
||||
const form = await getFormState();
|
||||
// form.groups = [{ name: "ГруппаНовости", title: "Новости", collapsed: true }, ...]
|
||||
await clickElement('Новости', { expand: true }); // reveal the group's content
|
||||
```
|
||||
|
||||
**tables** — array of all visible grids: `[{ name, columns, rowCount, label? }]`. `label` is the visual group title shown on screen (e.g. "Входящие"), absent when grid has no visible title. Use `readTable()` for actual data.
|
||||
|
||||
**table** — backward-compatible alias for the first grid: `{ present, columns, rowCount }`.
|
||||
@@ -161,7 +172,7 @@ const form = await getFormState();
|
||||
|
||||
**confirmation** — if present, a Yes/No dialog is shown. Call `clickElement('Да')` or `clickElement('Нет')`.
|
||||
|
||||
**errors.stateText** — array of SpreadsheetDocument state messages (e.g. `"Не установлено значение параметра \"X\""`, `"Отчет не сформирован..."`, `"Изменились настройки..."`). Present when the report area shows an info bar instead of data.
|
||||
**errors.stateText** — array of SpreadsheetDocument state messages (e.g. `"Не установлено значение параметра \"X\""`, `"Отчет не сформирован..."`, `"Изменились настройки..."`). Present when the report area shows an info bar instead of data. The same info bar carries `"Поиск..."` while a list is still searching — actions do not return while it is up, so a filtered list never hands you the previous rows.
|
||||
|
||||
### Reading data
|
||||
|
||||
@@ -189,6 +200,22 @@ Special row fields:
|
||||
- `hierarchical: true` — list has groups (on result object)
|
||||
- `viewMode: 'tree'` — tree view active (on result object)
|
||||
|
||||
Row state — in object lists, decoded from the row's state icon (no need to add a column to the list):
|
||||
- `_deleted: true|false` — marked for deletion (catalogs, documents, tasks, business processes, charts of accounts/calculation types)
|
||||
- `_posted: true|false` — documents
|
||||
- `_predefined: true|false` — catalogs, charts of accounts/calculation types
|
||||
- `_completed: true|false` — tasks
|
||||
- `_started`, `_finished` — business processes
|
||||
- `_rowPic: '<icon>:<N>'` — raw icon id, for diagnostics
|
||||
|
||||
```js
|
||||
const t = await readTable();
|
||||
const doc = t.rows.find(r => r['Номер'] === 'ТД00-000005');
|
||||
if (doc._deleted === true) { /* marked for deletion */ }
|
||||
```
|
||||
|
||||
**A missing state field means "unknown", never `false`** — the property may not apply (documents have no `_predefined`), or the icon may be unrecognised. So `if (!row._deleted)` is unsafe: it reads "unknown" as "not deleted". Compare explicitly (`=== true` / `=== false`) and treat `undefined` as a third outcome. Rows outside object lists (form tabular sections, value lists) have no state fields at all. If `_rowPic` is present but the booleans aren't, report its value — that icon needs decoding support.
|
||||
|
||||
**`total` is misleading for long lists.** 1С virtualizes both dynamic lists and form tabular sections — the DOM holds only a window of visible rows. `total` / `shown` count what's *loaded right now*, not the size of the underlying collection. Use **`hasMore`** to know if there's more data outside the window:
|
||||
|
||||
```js
|
||||
@@ -240,6 +267,8 @@ Sections + all open tabs.
|
||||
#### `clickElement(text, { dblclick?, table?, expand?, modifier?, scroll? })` → form state
|
||||
Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match).
|
||||
|
||||
**Disabled controls throw.** `clickElement`, `fillFields`, and `selectValue` throw `"X" is disabled` on an unavailable control instead of reporting a fake success — check `getFormState().buttons[].disabled` / `fields[].disabled` first.
|
||||
|
||||
- `table` — scope button search to a specific grid's command panel (by name from `tables[]`):
|
||||
```js
|
||||
await clickElement('Добавить', { table: 'Исходящие' }); // clicks "Добавить" near "Исходящие" grid
|
||||
|
||||
@@ -10,6 +10,8 @@ node $RUN test <dir|file>... [flags]
|
||||
|
||||
Positional args are test paths (files and/or dirs, multiple allowed). URL is NOT positional — it comes from `webtest.config.mjs`; override with `--url=<url>`.
|
||||
|
||||
`webtest.config.mjs` and `_hooks.mjs` always come from the suite root, whatever path you pass: `test tests/myapp/sales/` and `test tests/myapp/sales/01-order.test.mjs` both run under the config and hooks of `tests/myapp/`, no `--url=` needed. Paths from two different suites in one run are refused — pass one suite and narrow with `--grep=` / `--tags=`.
|
||||
|
||||
Tests live next to the project they cover (not inside the skill). Convention: `tests/` at the project root, with `_hooks.mjs` and `webtest.config.mjs` at the suite root. Tests are ES modules with `*.test.mjs` suffix.
|
||||
|
||||
## When to choose `test` over `exec`
|
||||
@@ -69,7 +71,7 @@ tests/<app-name>/
|
||||
01-end-to-end.test.mjs # multi-user
|
||||
```
|
||||
|
||||
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported — only the application-root copies are loaded.
|
||||
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported — only the application-root copies are loaded, whichever subfolder you point the runner at.
|
||||
|
||||
## Test file anatomy
|
||||
|
||||
@@ -184,8 +186,8 @@ assert.match(string, regex, msg?) // regex.test(string)
|
||||
await assert.throws(asyncFn, msg?) // passes if fn throws (use await)
|
||||
|
||||
// 1C-specific — operate on getFormState() / readTable() output
|
||||
assert.formHasField(state, 'Контрагент', msg?) // state.fields[name] exists
|
||||
assert.formTitle(state, expected, msg?) // state.title includes expected
|
||||
assert.formHasField(state, 'Контрагент', msg?) // fields[] contains a field with that name
|
||||
assert.formTitle(state, expected, msg?) // state.title includes expected (null title → fails saying so)
|
||||
assert.tableHasRow(table, predicate, msg?) // predicate: object (partial match) or fn(row) => bool
|
||||
// object form: { 'Наименование': 'Тест' }
|
||||
// fn form: r => r['Сумма'] > 100
|
||||
@@ -209,6 +211,11 @@ export default {
|
||||
// },
|
||||
// defaultContext: 'clerk',
|
||||
|
||||
// Context-pool / 1C license management (all optional; omit = no cap, default stays open).
|
||||
// maxContexts: 2, // cap on simultaneous 1C sessions; omit for unlimited
|
||||
// contextPolicy: 'reuse', // 'reuse' (keep open within cap) | 'strict' (close after each test)
|
||||
// pinnedContexts: [], // never evicted; defaults to [defaultContext], [] makes default evictable
|
||||
|
||||
timeout: 30000,
|
||||
retries: 0,
|
||||
screenshot: 'on-failure', // 'every-step' | 'off'
|
||||
@@ -311,7 +318,7 @@ export default async function({ clerk, manager, step, assert }) {
|
||||
});
|
||||
await step('Кладовщик видит новый статус', async () => {
|
||||
const s = await clerk.getFormState();
|
||||
assert.equal(s.fields['Статус']?.value, 'Утверждён');
|
||||
assert.equal(s.fields.find(f => f.name === 'Статус')?.value, 'Утверждён');
|
||||
});
|
||||
await step('Освободить сессию кладовщика', async () => {
|
||||
await manager.closeContext('clerk'); // free a 1C license for the next test
|
||||
@@ -319,7 +326,9 @@ export default async function({ clerk, manager, step, assert }) {
|
||||
}
|
||||
```
|
||||
|
||||
Close contexts you no longer need (`manager.closeContext('clerk')`) before the next multi-user test starts — frees a 1C web-client license and stops the previous role from holding state.
|
||||
Close contexts you no longer need (`manager.closeContext('clerk')`) before the next multi-user test starts — frees a 1C web-client license and stops the previous role from holding state. On tight-license stands prefer configuring the pool (`maxContexts` + `contextPolicy` + `pinnedContexts`) over manual per-test closing — the runner then evicts and reuses sessions automatically.
|
||||
|
||||
**Context pool (1C licenses).** With `maxContexts` set, the runner caps simultaneous 1C sessions: before each test it evicts least-recently-used contexts that are neither pinned nor needed, reusing already-open ones. `contextPolicy: 'reuse'` (default) keeps sessions for speed; `'strict'` closes a test's non-pinned contexts right after it. `pinnedContexts` are never evicted (default `[defaultContext]`; set `[]` to make the default context evictable on a tight stand). If the pool can't fit even after eviction, the test fails with a clear `context pool exhausted` error instead of an opaque connection failure.
|
||||
|
||||
### Failing-test repro
|
||||
|
||||
@@ -332,7 +341,7 @@ export default async function({ openCommand, clickElement, getFormState, assert,
|
||||
await clickElement('Создать');
|
||||
await clickElement('Провести');
|
||||
const s = await getFormState();
|
||||
assert.ok(s.errorModal || s.fields['Контрагент']?.required,
|
||||
assert.ok(s.errorModal || s.fields.find(f => f.name === 'Контрагент')?.required,
|
||||
'Должна быть ошибка валидации или поле помечено обязательным');
|
||||
}
|
||||
```
|
||||
@@ -352,7 +361,7 @@ export const params = [
|
||||
export default async function({ fillFields, getFormState, assert }, { type, field, value }) {
|
||||
await fillFields({ [field]: value });
|
||||
const state = await getFormState();
|
||||
assert.equal(state.fields[field]?.value, String(value));
|
||||
assert.equal(state.fields.find(f => f.name === field)?.value, String(value));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -370,9 +379,18 @@ node $RUN test tests/<app-name>/ --grep='накладн' #
|
||||
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
|
||||
node $RUN test tests/<app-name>/ --report=allure-results --format=allure --report-dir=allure-results
|
||||
node $RUN test tests/<app-name>/ --report=- # machine JSON to stdout, progress to stderr
|
||||
node $RUN test tests/<app-name>/ --global-timeout=3600000 # ceiling for the whole run (exit 2)
|
||||
node $RUN test tests/<app-name>/ -- --rebuild-stand # after `--` → hookArgs
|
||||
```
|
||||
|
||||
**Timeouts and hangs.** A test's `timeout` is a contract, not a wish: when it expires the runner probes the
|
||||
context and destroys whatever is wedged, so the run always moves on. The failure carries a verdict — `hang`
|
||||
(browser alive, renderer's JS thread blocked; the context is aborted, its 1C seance released from Node, and
|
||||
the next test recreates it) versus `slow`/`slow-network` (nothing is broken — raise `export const timeout`).
|
||||
A `hang` is never retried. Exit codes: `1` red tests, `2` `--global-timeout` fired (report written, seances
|
||||
released), `3` the shutdown itself wedged. Allure results are written per test as it finishes, so a hang
|
||||
cannot destroy the results collected before it — no external watchdog needed.
|
||||
|
||||
**Output contract.** `test` behaves like a test runner: by default the human report (with the summary as the last line) goes to **stdout** — read the tail of stdout + exit code. The machine report is opt-in via `--report`: `--report=path` writes it to a file (default JSON; XML for `--format=junit`), `--report=-` writes it to stdout while progress moves to stderr. Allure needs `--format=allure` + a directory (`-` is invalid for allure). For detailed triage use `--report=path` or `--report=-`. **In `--report=-` mode never use `2>&1`** — it merges stderr progress into the stdout JSON. (In the default mode there is no JSON in stdout, so `… | tail` is safe.)
|
||||
|
||||
### Allure static config — `_allure/`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test browser v1.18 — engine facade: re-exports the public API from engine/*
|
||||
// web-test browser v1.19 — engine facade: re-exports the public API from engine/*
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Public API of the web-test engine. Pure re-export facade — no logic here.
|
||||
@@ -22,6 +22,9 @@ export {
|
||||
connect, disconnect, attach, detach, getSession,
|
||||
createContext, setActiveContext, listContexts, getActiveContext,
|
||||
hasContext, closeContext,
|
||||
// Unresponsive-context handling (test runner). abortContext is the sanctioned way to
|
||||
// mutate the registry from outside — the `contexts` Map itself stays private.
|
||||
abortContext, probeContext, getContextDiagnostics,
|
||||
} from './engine/core/session.mjs';
|
||||
|
||||
// ── navigation ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test cli/commands/run v1.0 — autonomous connect → exec → disconnect (no server)
|
||||
// web-test cli/commands/run v1.1 — autonomous connect → exec → disconnect (no server)
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
@@ -13,7 +13,14 @@ export async function cmdRun(url, fileOrDash) {
|
||||
? await readStdin()
|
||||
: readFileSync(resolve(fileOrDash), 'utf-8');
|
||||
|
||||
await browser.connect(url);
|
||||
// Same as cmdStart: a startup blocker is a diagnosis, not a crash. connect() has already
|
||||
// released the seance and closed the browser; a stack trace pointing into session.mjs would
|
||||
// read as an engine bug and send the reader off to debug the wrong thing.
|
||||
try {
|
||||
await browser.connect(url);
|
||||
} catch (e) {
|
||||
die(e.message);
|
||||
}
|
||||
const result = await executeScript(code);
|
||||
await browser.disconnect();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test cli/commands/start v1.0
|
||||
// web-test cli/commands/start v1.1
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import http from 'http';
|
||||
import { writeFileSync } from 'fs';
|
||||
@@ -10,7 +10,15 @@ import { handleRequest } from '../server.mjs';
|
||||
export async function cmdStart(url) {
|
||||
if (!url) die('Usage: node src/run.mjs start <url>');
|
||||
|
||||
const state = await browser.connect(url);
|
||||
// A startup blocker (no free 1C licence, publication asking for credentials) is a diagnosis,
|
||||
// not a crash — connect() already released the seance and closed the browser, so print the
|
||||
// message and leave instead of dumping a stack trace that reads like an engine failure.
|
||||
let state;
|
||||
try {
|
||||
state = await browser.connect(url);
|
||||
} catch (e) {
|
||||
die(e.message);
|
||||
}
|
||||
|
||||
const httpServer = http.createServer(handleRequest);
|
||||
httpServer.listen(0, '127.0.0.1', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user