mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-15 07:49:01 +03:00
Compare commits
165
Commits
w-2026-08-02
...
main
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -163,6 +163,8 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
Assert-EditAllowed $resolvedPath 'editable'
|
Assert-EditAllowed $resolvedPath 'editable'
|
||||||
|
|
||||||
# --- Load XML with PreserveWhitespace ---
|
# --- Load XML with PreserveWhitespace ---
|
||||||
|
# NB: парсер XML по спецификации схлопывает CRLF в LF, а вставки ниже собираются с
|
||||||
|
# явным CRLF — поэтому EOL приводится к целевому в точке записи (см. финализацию).
|
||||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$script:xmlDoc.PreserveWhitespace = $true
|
$script:xmlDoc.PreserveWhitespace = $true
|
||||||
$script:xmlDoc.Load($resolvedPath)
|
$script:xmlDoc.Load($resolvedPath)
|
||||||
@@ -691,7 +693,9 @@ $bodyBlock$declarations
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$caiXml = ($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($caiPath, $caiXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote panel layout: $caiPath"
|
Info "Wrote panel layout: $caiPath"
|
||||||
}
|
}
|
||||||
@@ -880,7 +884,9 @@ $rightXml
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$hpXml = ($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($hpPath, $hpXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote home page layout: $hpPath"
|
Info "Wrote home page layout: $hpPath"
|
||||||
}
|
}
|
||||||
@@ -982,6 +988,14 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,65 @@ import uuid as _uuid
|
|||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -341,21 +400,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -379,7 +439,7 @@ def main():
|
|||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||||
@@ -762,7 +822,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -917,7 +977,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -985,7 +1045,7 @@ def main():
|
|||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
ops = json.loads(fh.read())
|
ops = ci_json(json.loads(fh.read()))
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -995,23 +1055,25 @@ def main():
|
|||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
op_name = op.get("operation", args.Operation or "")
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
# PS сравнивает имя операции через switch, а он регистронезависим.
|
||||||
|
op_key = str(op_name).lower()
|
||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "modify-property":
|
if op_key == "modify-property":
|
||||||
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-childObject":
|
elif op_key == "add-childobject":
|
||||||
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-childObject":
|
elif op_key == "remove-childobject":
|
||||||
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-defaultRole":
|
elif op_key == "add-defaultrole":
|
||||||
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-defaultRole":
|
elif op_key == "remove-defaultrole":
|
||||||
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-defaultRoles":
|
elif op_key == "set-defaultroles":
|
||||||
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-panels":
|
elif op_key == "set-panels":
|
||||||
do_set_panels(op_value)
|
do_set_panels(op_value)
|
||||||
elif op_name == "set-home-page":
|
elif op_key == "set-home-page":
|
||||||
do_set_home_page(op_value)
|
do_set_home_page(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||||
@@ -20,7 +42,7 @@ parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, he
|
|||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Output helper (collect all, paginate at the end) ---
|
# --- Output helper (collect all, paginate at the end) ---
|
||||||
lines_buf = []
|
lines_buf = []
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ allowed-tools:
|
|||||||
| `Version` | Версия конфигурации |
|
| `Version` | Версия конфигурации |
|
||||||
| `Vendor` | Поставщик |
|
| `Vendor` | Поставщик |
|
||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
| `FormatVersion` | Версия формата выгрузки (default: `2.17`) |
|
||||||
|
|
||||||
|
Оба параметра задаются от **целевой платформы** — той, на которой конфигурация будет работать, — но по
|
||||||
|
разным правилам.
|
||||||
|
|
||||||
|
`FormatVersion` — **не выше** версии формата целевой платформы: 8.3.24 — `2.17`, 8.3.25 — `2.18`,
|
||||||
|
8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно: платформа читает свой формат и любой
|
||||||
|
более старый, поэтому дефолт `2.17` подходит для всей линейки 8.3.24 и выше.
|
||||||
|
|
||||||
|
`CompatibilityMode` влияет на доступную функциональность. Если в задаче не оговорено иное — **равен
|
||||||
|
целевой платформе** (`Version8_3_27` для 8.3.27); это то же самое, что «Не использовать» в
|
||||||
|
Конфигураторе, и именно такое значение платформа проставляет новой базе. Более низкий режим берут
|
||||||
|
осознанно — когда конфигурация должна работать и на старых платформах; он отключает возможности,
|
||||||
|
появившиеся позже. Выше целевой платформы — нельзя: такая конфигурация загрузится, но работать на ней
|
||||||
|
не будет.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
||||||
@@ -36,8 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name
|
|||||||
# С версией и поставщиком
|
# С версией и поставщиком
|
||||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||||
|
|
||||||
# Другой режим совместимости
|
# Под платформу 8.3.27 — версия формата и режим совместимости вместе
|
||||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
... -Name TestCfg -FormatVersion 2.20 -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.4 — Create empty 1C configuration scaffold
|
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -9,15 +9,54 @@ param(
|
|||||||
[string]$Vendor,
|
[string]$Vendor,
|
||||||
[string]$CompatibilityMode = "Version8_3_24",
|
[string]$CompatibilityMode = "Version8_3_24",
|
||||||
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||||
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19,
|
# совместимости она не зависит. Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
|
|
||||||
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
|
|
||||||
[string]$FormatVersion = "2.17"
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
|
||||||
|
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
|
||||||
|
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
|
||||||
|
# на нечисловое значение: это опечатка, а не версия.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$formatRank = Get-FormatRank $FormatVersion
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
|
||||||
|
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
|
||||||
|
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
|
||||||
|
if ($formatRank -eq 0) {
|
||||||
|
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if ($CompatibilityMode -and $CompatibilityMode.ToLowerInvariant() -eq 'dontuse') {
|
||||||
|
[Console]::Error.WriteLine("WARNING: CompatibilityMode 'DontUse' is not `"no restrictions`" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).")
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve output dir ---
|
# --- Resolve output dir ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||||
@@ -43,6 +82,11 @@ $co6 = [guid]::NewGuid().ToString()
|
|||||||
$co7 = [guid]::NewGuid().ToString()
|
$co7 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
$is221 = ($formatRank -ge 221)
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
$is218 = ($formatRank -ge 218)
|
||||||
|
|
||||||
$mobileFuncs = @(
|
$mobileFuncs = @(
|
||||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||||
@@ -59,6 +103,12 @@ $mobileFuncs = @(
|
|||||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||||
)
|
)
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if ($is218) { $mobileFuncs += ,@("TextToSpeech","false") }
|
||||||
|
|
||||||
$mobileXml = ""
|
$mobileXml = ""
|
||||||
foreach ($mf in $mobileFuncs) {
|
foreach ($mf in $mobileFuncs) {
|
||||||
@@ -68,17 +118,43 @@ foreach ($mf in $mobileFuncs) {
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
$synonymXml = ""
|
$synonymXml = ""
|
||||||
if ($Synonym) {
|
if ($Synonym) {
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional properties ---
|
# --- Optional properties ---
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
|
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
|
||||||
|
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
|
||||||
|
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
|
||||||
|
# на своё место, а не в конец.
|
||||||
|
$nl = "`r`n"
|
||||||
|
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
|
||||||
|
$palNs = ""
|
||||||
|
if ($is221) {
|
||||||
|
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
|
||||||
|
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
|
||||||
|
$f221AuxForms = $nl + ((@(
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
|
||||||
|
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
|
||||||
|
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
|
||||||
|
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
|
||||||
|
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
|
||||||
|
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
|
||||||
|
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -111,7 +187,7 @@ $cfgXml = @"
|
|||||||
</xr:ContainedObject>
|
</xr:ContainedObject>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
<Synonym>$synonymXml</Synonym>
|
<Synonym>$synonymXml</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<NamePrefix/>
|
<NamePrefix/>
|
||||||
@@ -122,8 +198,8 @@ $cfgXml = @"
|
|||||||
</UsePurposes>
|
</UsePurposes>
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
<DefaultRoles/>
|
<DefaultRoles/>
|
||||||
<Vendor>$vendorXml</Vendor>
|
$vendorEl
|
||||||
<Version>$versionXml</Version>
|
$versionEl
|
||||||
<UpdateCatalogAddress/>
|
<UpdateCatalogAddress/>
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
@@ -145,15 +221,15 @@ $cfgXml = @"
|
|||||||
<DefaultDataHistoryChangeHistoryForm/>
|
<DefaultDataHistoryChangeHistoryForm/>
|
||||||
<DefaultDataHistoryVersionDataForm/>
|
<DefaultDataHistoryVersionDataForm/>
|
||||||
<DefaultDataHistoryVersionDifferencesForm/>
|
<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
<DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
|
||||||
<RequiredMobileApplicationPermissions/>
|
<RequiredMobileApplicationPermissions/>
|
||||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||||
</UsedMobileApplicationFunctionalities>
|
</UsedMobileApplicationFunctionalities>
|
||||||
<StandaloneConfigurationRestrictionRoles/>
|
<StandaloneConfigurationRestrictionRoles/>
|
||||||
<MobileApplicationURLs/>
|
<MobileApplicationURLs/>
|
||||||
<AllowedIncomingShareRequestTypes/>
|
<AllowedIncomingShareRequestTypes/>$f221WindowVariant
|
||||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
|
||||||
<DefaultInterface/>
|
<DefaultInterface/>$f221Captions
|
||||||
<DefaultStyle/>
|
<DefaultStyle/>
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
<BriefInformation/>
|
<BriefInformation/>
|
||||||
@@ -165,7 +241,7 @@ $cfgXml = @"
|
|||||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
|
||||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||||
<DefaultConstantsForm/>
|
<DefaultConstantsForm/>
|
||||||
@@ -180,7 +256,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>Русский</Name>
|
<Name>Русский</Name>
|
||||||
@@ -240,11 +316,20 @@ if (-not (Test-Path $extDir)) {
|
|||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
Write-XmlFile $langFile $langXml $enc
|
||||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
Write-XmlFile $caiFile $caiXml $enc
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.4 — Create empty 1C configuration scaffold
|
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, re, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -24,12 +70,32 @@ def main():
|
|||||||
parser.add_argument('-Version', dest='Version', default='')
|
parser.add_argument('-Version', dest='Version', default='')
|
||||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости.
|
||||||
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20.
|
# Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
# Дефолт консервативный: 2.17 читается всеми платформами.
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
|
||||||
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
|
args = ci_parse_args(parser)
|
||||||
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
|
|
||||||
args = parser.parse_args()
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
|
||||||
|
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
|
||||||
|
format_rank_value = format_rank(args.FormatVersion)
|
||||||
|
if format_rank_value == 0:
|
||||||
|
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
|
||||||
|
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
|
||||||
|
f"but was not verified on that platform", file=sys.stderr)
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if (args.CompatibilityMode or "").lower() == "dontuse":
|
||||||
|
print("WARNING: CompatibilityMode 'DontUse' is not \"no restrictions\" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).", file=sys.stderr)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -54,6 +120,11 @@ def main():
|
|||||||
co = [new_uuid() for _ in range(7)]
|
co = [new_uuid() for _ in range(7)]
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
is_221 = format_rank_value >= 221
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
is_218 = format_rank_value >= 218
|
||||||
|
|
||||||
mobile_funcs = [
|
mobile_funcs = [
|
||||||
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
||||||
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
||||||
@@ -70,6 +141,13 @@ def main():
|
|||||||
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
||||||
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
||||||
]
|
]
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if is_218:
|
||||||
|
mobile_funcs.append(("TextToSpeech", "false"))
|
||||||
|
|
||||||
mobile_xml = ""
|
mobile_xml = ""
|
||||||
for func_name, func_use in mobile_funcs:
|
for func_name, func_use in mobile_funcs:
|
||||||
@@ -78,10 +156,12 @@ def main():
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
synonym_xml = ""
|
synonym_xml = ""
|
||||||
if synonym:
|
if synonym:
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
version_xml = esc_xml(version) if version else ""
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
class_ids = [
|
class_ids = [
|
||||||
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
||||||
@@ -93,6 +173,28 @@ def main():
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
|
||||||
|
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
|
||||||
|
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
|
||||||
|
pal_ns = ""
|
||||||
|
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
|
||||||
|
if is_221:
|
||||||
|
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
f221_aux_forms = "\r\n" + "\r\n".join(
|
||||||
|
f"\t\t\t{t}" for t in (
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
|
||||||
|
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
|
||||||
|
"</MainClientApplicationWindowInterfaceVariant>"
|
||||||
|
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
|
||||||
|
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
|
||||||
|
"</ClientApplicationWindowsOpenVariant>")
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
|
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
|
||||||
|
"</Version85InterfaceMigrationMode>")
|
||||||
|
|
||||||
contained_objects = ""
|
contained_objects = ""
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
@@ -101,12 +203,12 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<NamePrefix/>
|
\t\t\t<NamePrefix/>
|
||||||
@@ -117,8 +219,8 @@ def main():
|
|||||||
\t\t\t</UsePurposes>
|
\t\t\t</UsePurposes>
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
\t\t\t<DefaultRoles/>
|
\t\t\t<DefaultRoles/>
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
\t\t\t{vendor_el}
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
\t\t\t{version_el}
|
||||||
\t\t\t<UpdateCatalogAddress/>
|
\t\t\t<UpdateCatalogAddress/>
|
||||||
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
@@ -140,15 +242,15 @@ def main():
|
|||||||
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
||||||
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
||||||
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
|
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
|
||||||
\t\t\t<RequiredMobileApplicationPermissions/>
|
\t\t\t<RequiredMobileApplicationPermissions/>
|
||||||
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
||||||
\t\t\t</UsedMobileApplicationFunctionalities>
|
\t\t\t</UsedMobileApplicationFunctionalities>
|
||||||
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
||||||
\t\t\t<MobileApplicationURLs/>
|
\t\t\t<MobileApplicationURLs/>
|
||||||
\t\t\t<AllowedIncomingShareRequestTypes/>
|
\t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
|
||||||
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
|
||||||
\t\t\t<DefaultInterface/>
|
\t\t\t<DefaultInterface/>{f221_captions}
|
||||||
\t\t\t<DefaultStyle/>
|
\t\t\t<DefaultStyle/>
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
\t\t\t<BriefInformation/>
|
\t\t\t<BriefInformation/>
|
||||||
@@ -160,7 +262,7 @@ def main():
|
|||||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||||
\t\t\t<DefaultConstantsForm/>
|
\t\t\t<DefaultConstantsForm/>
|
||||||
@@ -173,7 +275,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>Русский</Name>
|
\t\t\t<Name>Русский</Name>
|
||||||
@@ -222,11 +324,11 @@ def main():
|
|||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_xml_file(lang_file, lang_xml)
|
||||||
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
write_utf8_bom(cai_file, cai_xml)
|
write_xml_file(cai_file, cai_xml)
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
print(f" Каталог: {output_dir}")
|
print(f" Каталог: {output_dir}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.5 — Validate 1C configuration root structure
|
# cf-validate v1.7 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -89,6 +89,19 @@ $finalize = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||||
@@ -203,11 +216,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
|
$versionRank = Get-FormatRank $version
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
} elseif ($versionRank -eq 0) {
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
Report-Error "1. Malformed version '$version' (expected N.N)"
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.5 — Validate 1C configuration XML structure
|
# cf-validate v1.7 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -110,6 +132,20 @@ VALID_ENUM_VALUES = {
|
|||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
class Reporter:
|
class Reporter:
|
||||||
def __init__(self, max_errors, detailed=False):
|
def __init__(self, max_errors, detailed=False):
|
||||||
@@ -170,7 +206,7 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
@@ -230,11 +266,17 @@ def main():
|
|||||||
check1_ok = False
|
check1_ok = False
|
||||||
|
|
||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
elif version_rank == 0:
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
r.error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
r.warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
r.warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ allowed-tools:
|
|||||||
- `Enum.ВидыОплат` — перечисление
|
- `Enum.ВидыОплат` — перечисление
|
||||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||||
Поддерживаются все 44 типа объектов конфигурации.
|
|
||||||
|
|
||||||
### Заимствование форм
|
### Заимствование форм
|
||||||
|
|
||||||
@@ -71,31 +70,33 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Заимствовать один объект
|
# Заимствовать один объект
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||||
|
|
||||||
# Несколько объектов за раз
|
# Несколько объектов за раз
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||||
|
|
||||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||||
|
|
||||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <ExtensionPath>
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -16,18 +16,38 @@ function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
|||||||
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
# platform rejects the form with "Неверный путь к данным" on load.
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
# RowPictureDataPath тоже путь к данным («Объект.Товары.РасхождениеЗаказ», «Список.DefaultPicture»),
|
||||||
|
# а не индекс картинки: эталон Конфигуратора сохраняет его с заимствованным основным реквизитом
|
||||||
|
# и выбрасывает без него — то же правило, что у остальных путей.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath')
|
||||||
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
$script:formBindingPictureTags = @('MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Пути ссылок параметров выбора, которые пришлось вырезать (для предупреждения в конце)
|
||||||
|
$script:droppedLinks = @()
|
||||||
|
|
||||||
|
# id основного реквизита в заимствованной форме — как у Конфигуратора
|
||||||
|
$script:mainAttrId = "1000001"
|
||||||
|
|
||||||
|
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
|
||||||
|
$script:childObjectKinds = @('Attribute','Dimension','Resource','AddressingAttribute')
|
||||||
|
|
||||||
|
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
|
||||||
|
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
|
||||||
|
$script:formStructuralSections = @('Events','Attributes','Commands','Parameters','CommandInterface')
|
||||||
|
# Свойства формы, значение которых — имя реквизита формы (реквизиты не заимствуются, ссылка повиснет).
|
||||||
|
$script:formAttributeRefProps = @('ReportResult','DetailsData','VariantAppearance','GroupList')
|
||||||
|
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed.
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
# $mainAttrName задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять.
|
||||||
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
# Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда.
|
||||||
function Strip-FormBindings {
|
function Strip-FormBindings {
|
||||||
param([string]$xml, [bool]$keepObjekt)
|
param([string]$xml, [string]$mainAttrName)
|
||||||
foreach ($tag in $script:formBindingDataTags) {
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
if ($keepObjekt) {
|
if ($mainAttrName) {
|
||||||
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
# Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы)
|
||||||
|
$root = [regex]::Escape($mainAttrName)
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!$root(\.|<))[^<]*</$tag>", '')
|
||||||
} else {
|
} else {
|
||||||
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
}
|
}
|
||||||
@@ -38,6 +58,119 @@ function Strip-FormBindings {
|
|||||||
return $xml
|
return $xml
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||||
|
# в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
|
||||||
|
# если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
|
||||||
|
# загрузку — «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
|
||||||
|
# них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
|
||||||
|
# Issue66Example4/5/6, JR2433, JR2976, JR49904 — совпадение на шести расширениях). Именно id
|
||||||
|
# исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
|
||||||
|
# ссылка продолжает указывать в нумерацию базовой формы.
|
||||||
|
# Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
|
||||||
|
# и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
|
||||||
|
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
|
||||||
|
# Пути вида «Items.<Элемент>.CurrentData.<Поле>» не трогаем — их кодировка отдельная.
|
||||||
|
function Rewrite-ChoiceParameterLinks {
|
||||||
|
param([string]$xml, $attrUuids, $formAttrIds, [string]$mainAttrName, [bool]$mainAttrBorrowed)
|
||||||
|
|
||||||
|
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
|
||||||
|
|
||||||
|
$mainPat = if ($mainAttrName) { [regex]::Escape($mainAttrName) } else { $null }
|
||||||
|
$mainId = if ($mainAttrName -and $formAttrIds.ContainsKey($mainAttrName)) { $formAttrIds[$mainAttrName] } else { "1" }
|
||||||
|
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
|
||||||
|
param($m)
|
||||||
|
$link = $m.Value
|
||||||
|
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>([^<]+)</xr:DataPath>')
|
||||||
|
if (-not $dp.Success) { return $link }
|
||||||
|
$path = $dp.Groups[1].Value
|
||||||
|
|
||||||
|
# Путь на основной реквизит формы
|
||||||
|
if ($mainPat -and $path -match "^${mainPat}\.(.+)$") {
|
||||||
|
$attrName = $Matches[1]
|
||||||
|
if ($mainAttrBorrowed) {
|
||||||
|
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
|
||||||
|
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
|
||||||
|
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
|
||||||
|
if ($attrUuids.ContainsKey($attrName)) { return $link }
|
||||||
|
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}`${2}")
|
||||||
|
}
|
||||||
|
if ($attrUuids.ContainsKey($attrName)) {
|
||||||
|
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}/0:$($attrUuids[$attrName])`${2}")
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Односегментный путь на реквизит формы — только по id исходной формы
|
||||||
|
if ($path -notmatch '\.' -and $formAttrIds.ContainsKey($path)) {
|
||||||
|
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}$($formAttrIds[$path])`${2}")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
|
||||||
|
if ($path -match '^\d') { return $link }
|
||||||
|
|
||||||
|
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
|
||||||
|
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
|
||||||
|
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
|
||||||
|
if ($mainAttrBorrowed) { return $link }
|
||||||
|
|
||||||
|
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
|
||||||
|
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
|
||||||
|
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
|
||||||
|
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
|
||||||
|
$script:droppedLinks += $path
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# Опустевший контейнер платформе не нужен
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
|
||||||
|
# заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
|
||||||
|
# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
|
||||||
|
# теряет то, что идёт после неё.
|
||||||
|
function Get-OwnChildObjectNames {
|
||||||
|
param([string]$objFile)
|
||||||
|
|
||||||
|
$names = @{}
|
||||||
|
if (-not (Test-Path -LiteralPath $objFile)) { return $names }
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
|
$doc.PreserveWhitespace = $false
|
||||||
|
try { $doc.Load($objFile) } catch { return $names }
|
||||||
|
$objEl = $null
|
||||||
|
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||||
|
}
|
||||||
|
if (-not $objEl) { return $names }
|
||||||
|
$childObjs = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
|
||||||
|
if (-not $childObjs) { return $names }
|
||||||
|
foreach ($child in $childObjs.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if ($nameNode) { $names[$nameNode.InnerText.Trim()] = $true }
|
||||||
|
}
|
||||||
|
return $names
|
||||||
|
}
|
||||||
|
|
||||||
|
# Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
|
||||||
|
# объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
|
||||||
|
# всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ.
|
||||||
|
function Insert-IntoOwnChildObjects {
|
||||||
|
param([string]$text, [string]$content)
|
||||||
|
|
||||||
|
$closeIdx = $text.LastIndexOf('</ChildObjects>')
|
||||||
|
if ($closeIdx -ge 0) {
|
||||||
|
return $text.Substring(0, $closeIdx) + "${content}`r`n`t`t" + $text.Substring($closeIdx)
|
||||||
|
}
|
||||||
|
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
|
||||||
|
$selfMatches = [regex]::Matches($text, '<ChildObjects\s*/>')
|
||||||
|
if ($selfMatches.Count -eq 0) { return $text }
|
||||||
|
$m = $selfMatches[$selfMatches.Count - 1]
|
||||||
|
return $text.Substring(0, $m.Index) + "<ChildObjects>${content}`r`n`t`t</ChildObjects>" + $text.Substring($m.Index + $m.Length)
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -125,7 +258,7 @@ $childTypeDirMap = @{
|
|||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 4b. Russian synonym → English type ---
|
# --- 4b. Russian synonym → English type ---
|
||||||
@@ -153,7 +286,7 @@ $synonymMap = @{
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -209,7 +342,8 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
||||||
)
|
)
|
||||||
"AccountingRegister" = @(
|
"AccountingRegister" = @(
|
||||||
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
||||||
|
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
|
||||||
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
||||||
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
||||||
@{ prefix = "AccountingRegisterList"; category = "List" }
|
@{ prefix = "AccountingRegisterList"; category = "List" }
|
||||||
@@ -223,6 +357,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "CalculationRegisterList"; category = "List" }
|
@{ prefix = "CalculationRegisterList"; category = "List" }
|
||||||
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
||||||
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
||||||
|
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
|
||||||
)
|
)
|
||||||
"ChartOfAccounts" = @(
|
"ChartOfAccounts" = @(
|
||||||
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
||||||
@@ -230,12 +365,15 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
||||||
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
|
||||||
)
|
)
|
||||||
"ChartOfCharacteristicTypes" = @(
|
"ChartOfCharacteristicTypes" = @(
|
||||||
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
||||||
|
@{ prefix = "Characteristic"; category = "Characteristic" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
||||||
)
|
)
|
||||||
"ChartOfCalculationTypes" = @(
|
"ChartOfCalculationTypes" = @(
|
||||||
@@ -245,8 +383,11 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
||||||
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
||||||
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
||||||
|
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
|
||||||
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
||||||
|
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
|
||||||
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
||||||
|
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
|
||||||
)
|
)
|
||||||
"BusinessProcess" = @(
|
"BusinessProcess" = @(
|
||||||
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
||||||
@@ -254,6 +395,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
||||||
@{ prefix = "BusinessProcessList"; category = "List" }
|
@{ prefix = "BusinessProcessList"; category = "List" }
|
||||||
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
||||||
|
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
|
||||||
)
|
)
|
||||||
"Task" = @(
|
"Task" = @(
|
||||||
@{ prefix = "TaskObject"; category = "Object" }
|
@{ prefix = "TaskObject"; category = "Object" }
|
||||||
@@ -320,6 +462,16 @@ $typesWithChildObjects = @(
|
|||||||
# CommonModule properties to copy from source
|
# CommonModule properties to copy from source
|
||||||
$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall")
|
$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall")
|
||||||
|
|
||||||
|
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
|
||||||
|
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
|
||||||
|
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра сведений
|
||||||
|
# без InformationRegisterPeriodicity не разрешается «Запись.Period».
|
||||||
|
$script:typeGateProps = @{
|
||||||
|
"InformationRegister" = @("InformationRegisterPeriodicity","WriteMode")
|
||||||
|
}
|
||||||
|
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
|
||||||
|
$script:typesWithOwners = @("Catalog","ChartOfCharacteristicTypes")
|
||||||
|
|
||||||
# Standard system fields to skip when collecting DataPath references
|
# Standard system fields to skip when collecting DataPath references
|
||||||
$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName")
|
$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName")
|
||||||
|
|
||||||
@@ -368,6 +520,14 @@ function Expand-SelfClosingElement($container, $parentIndent) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
@@ -388,6 +548,20 @@ $script:formatVersion = Detect-FormatVersion $extDir
|
|||||||
# --- 8. Namespaces declaration for object XML ---
|
# --- 8. Namespaces declaration for object XML ---
|
||||||
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- 9. Parse -Object into items ---
|
# --- 9. Parse -Object into items ---
|
||||||
$items = @()
|
$items = @()
|
||||||
foreach ($part in $Object.Split(";;")) {
|
foreach ($part in $Object.Split(";;")) {
|
||||||
@@ -418,6 +592,37 @@ if ($BorrowMainAttribute) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 10. Helper: read source object XML ---
|
# --- 10. Helper: read source object XML ---
|
||||||
|
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||||
|
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||||
|
function Get-SourceAttributeUuids {
|
||||||
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$dirName = $childTypeDirMap[$typeName]
|
||||||
|
if (-not $dirName) { return $result }
|
||||||
|
$srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml"
|
||||||
|
if (-not (Test-Path $srcFile)) { return $result }
|
||||||
|
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
|
$doc.PreserveWhitespace = $false
|
||||||
|
$doc.Load($srcFile)
|
||||||
|
$objEl = $null
|
||||||
|
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||||
|
}
|
||||||
|
if (-not $objEl) { return $result }
|
||||||
|
$childObjects = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
|
||||||
|
if (-not $childObjects) { return $result }
|
||||||
|
foreach ($child in $childObjects.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||||
|
$uuid = $child.GetAttribute("uuid")
|
||||||
|
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Read-SourceObject {
|
function Read-SourceObject {
|
||||||
param([string]$typeName, [string]$objName)
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
@@ -477,6 +682,19 @@ function Read-SourceObject {
|
|||||||
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
|
||||||
|
if ($script:typesWithOwners -ccontains $typeName) {
|
||||||
|
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $srcNs)
|
||||||
|
if ($ownersNode -and $ownersNode.HasChildNodes) {
|
||||||
|
$srcProps["__OwnersXml"] = [regex]::Replace($ownersNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Скалярные свойства, включающие стандартные поля своего типа
|
||||||
|
foreach ($gp in @($script:typeGateProps[$typeName])) {
|
||||||
|
if (-not $gp) { continue }
|
||||||
|
$gpNode = $propsNode.SelectSingleNode("md:${gp}", $srcNs)
|
||||||
|
if ($gpNode) { $srcProps[$gp] = $gpNode.InnerText.Trim() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
@@ -597,30 +815,56 @@ function Borrow-Form {
|
|||||||
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
||||||
$formVersion = $script:formatVersion
|
$formVersion = $script:formatVersion
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems.
|
||||||
|
# Секции формы отбираются по имени, а не по позиции: свойства лежат и до, и после <CommandSet>
|
||||||
|
# (корпусная проверка: у всех 794 форм документов ERP с CommandSet он стоит раньше AutoCommandBar,
|
||||||
|
# а AutoTime/UsePostingMode/RepostOnWrite — после него). Позиционная отсечка теряла весь хвост,
|
||||||
|
# и платформа молча подставляла дефолты вместо потерянных свойств.
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
$srcChildItems = $null
|
$srcChildItems = $null
|
||||||
$formProps = @()
|
$formProps = @()
|
||||||
$reachedVisual = $false
|
|
||||||
foreach ($fc in $srcFormEl.ChildNodes) {
|
foreach ($fc in $srcFormEl.ChildNodes) {
|
||||||
if ($fc.NodeType -ne 'Element') { continue }
|
if ($fc.NodeType -ne 'Element') { continue }
|
||||||
if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) {
|
if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) {
|
||||||
$reachedVisual = $true; $srcAutoCmd = $fc; continue
|
$srcAutoCmd = $fc; continue
|
||||||
}
|
}
|
||||||
if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) {
|
if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) {
|
||||||
$reachedVisual = $true; $srcChildItems = $fc; continue
|
$srcChildItems = $fc; continue
|
||||||
}
|
|
||||||
if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') {
|
|
||||||
$reachedVisual = $true; continue
|
|
||||||
}
|
|
||||||
if (-not $reachedVisual) {
|
|
||||||
$formProps += $fc.OuterXml
|
|
||||||
}
|
}
|
||||||
|
# Структурные секции: в расширении их содержимое недействительно (обработчики, команды и
|
||||||
|
# параметры базовой формы, ссылки командного интерфейса на команды базовой конфигурации).
|
||||||
|
if ($script:formStructuralSections -ccontains $fc.LocalName) { continue }
|
||||||
|
# Свойства, значение которых — имя реквизита формы. Реквизиты в заимствованную форму не
|
||||||
|
# переносятся, поэтому Конфигуратор такие свойства выбрасывает (проверено на форме отчёта:
|
||||||
|
# ReportResult и DetailsData выброшены, CustomSettingsFolder — имя элемента — сохранён).
|
||||||
|
if ($script:formAttributeRefProps -ccontains $fc.LocalName) { continue }
|
||||||
|
$formProps += $fc.OuterXml
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
||||||
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
||||||
|
|
||||||
|
# Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить
|
||||||
|
# («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра)
|
||||||
|
# Имя основного реквизита источника нужно в обоих режимах: по нему опознаётся корень путей
|
||||||
|
# в ссылках параметров выбора. А $mainAttrName управляет вырезанием привязок и потому остаётся
|
||||||
|
# пустым в скелетном режиме — там привязки снимаются все.
|
||||||
|
$srcMainInfo = Get-MainAttributeInfo $srcFormEl $nsStripPattern
|
||||||
|
$srcMainAttrName = if ($srcMainInfo) { $srcMainInfo.Name } else { "" }
|
||||||
|
$formAttrIds = Get-FormAttributeIds $srcFormEl
|
||||||
|
$mainAttrInfo = if ($BorrowMainAttr) { $srcMainInfo } else { $null }
|
||||||
|
$mainAttrName = if ($mainAttrInfo) { $mainAttrInfo.Name } else { "" }
|
||||||
|
if ($BorrowMainAttr -and -not $mainAttrInfo) {
|
||||||
|
Warn " У формы нет основного реквизита — -BorrowMainAttribute проигнорирован"
|
||||||
|
}
|
||||||
|
|
||||||
|
# uuid реквизитов объекта нужны ровно там, где основной реквизит НЕ попал в форму:
|
||||||
|
# только тогда путь «<основной>.X» переводится в непрозрачный вид
|
||||||
|
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
|
||||||
|
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
|
||||||
|
# стандартного поля (не разрешается)
|
||||||
|
$srcAttrUuids = Get-SourceAttributeUuids $typeName $objName
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
||||||
$autoCmdXml = ""
|
$autoCmdXml = ""
|
||||||
if ($srcAutoCmd) {
|
if ($srcAutoCmd) {
|
||||||
@@ -628,10 +872,13 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '')
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Вложенный CommandSet выбрасывается целиком, а не опустошается: Конфигуратор в заимствованной
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
# форме оставляет только корневой (тот идёт свойством формы, здесь его нет).
|
||||||
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
|
||||||
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<CommandSet/>', '')
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
$autoCmdXml = Strip-FormBindings $autoCmdXml $mainAttrName
|
||||||
|
$autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -643,9 +890,11 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
$childItemsXml = Strip-FormBindings $childItemsXml $mainAttrName
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
$childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
# Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<CommandSet/>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
|
||||||
# Strip element-level Events (base form handlers not in extension)
|
# Strip element-level Events (base form handlers not in extension)
|
||||||
@@ -824,11 +1073,22 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract the <Form ...> opening tag from source text (preserves namespace declarations)
|
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств имён,
|
||||||
|
# но version подставляем СВОЮ: форма обязана нести версию расширения, иначе платформа
|
||||||
|
# отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег копировался целиком,
|
||||||
|
# и версия источника молча побеждала.
|
||||||
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
$formTag = "<Form version=`"${formVersion}`">"
|
$formTag = "<Form version=`"${formVersion}`">"
|
||||||
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
||||||
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] }
|
if ($srcFormContent -match '(<Form[^>]*>)') {
|
||||||
|
$srcTag = $Matches[1]
|
||||||
|
$srcNs = $srcTag -replace '^<Form\s*', '' -replace '\s*/?>$', '' -replace '\s*version="[^"]*"', ''
|
||||||
|
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
|
||||||
|
if ((Get-FormatRank $formVersion) -ge 221 -and $srcNs -notmatch 'xmlns:pal=') {
|
||||||
|
$srcNs = $srcNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
$formTag = if ($srcNs) { "<Form $srcNs version=`"${formVersion}`">" } else { "<Form version=`"${formVersion}`">" }
|
||||||
|
}
|
||||||
|
|
||||||
# Build output Form.xml
|
# Build output Form.xml
|
||||||
$formXmlSb = New-Object System.Text.StringBuilder
|
$formXmlSb = New-Object System.Text.StringBuilder
|
||||||
@@ -851,17 +1111,9 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`r`n") | Out-Null
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
}
|
}
|
||||||
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
||||||
if ($BorrowMainAttr) {
|
if ($BorrowMainAttr -and $mainAttrInfo) {
|
||||||
$objTypePrefix = ""
|
|
||||||
$gtList = $script:generatedTypes[$typeName]
|
|
||||||
if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } }
|
|
||||||
$mainAttrType = "cfg:${objTypePrefix}.${objName}"
|
|
||||||
$formXmlSb.Append("`t<Attributes>`r`n") | Out-Null
|
$formXmlSb.Append("`t<Attributes>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null
|
$formXmlSb.Append("`t`t$($mainAttrInfo.Xml)`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
|
||||||
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
|
||||||
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
|
||||||
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
|
|
||||||
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
$formXmlSb.Append("`t<Attributes/>") | Out-Null
|
$formXmlSb.Append("`t<Attributes/>") | Out-Null
|
||||||
@@ -895,13 +1147,15 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# BaseForm Attributes: same as main section
|
# BaseForm Attributes: same as main section
|
||||||
if ($BorrowMainAttr) {
|
if ($BorrowMainAttr -and $mainAttrInfo) {
|
||||||
$formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null
|
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
|
||||||
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
$maLines = $mainAttrInfo.Xml -split "`r?`n"
|
||||||
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
for ($li = 0; $li -lt $maLines.Count; $li++) {
|
||||||
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
if ($li -eq 0) { $formXmlSb.Append("`t`t`t$($maLines[$li])") | Out-Null }
|
||||||
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
|
else { $formXmlSb.Append("`t$($maLines[$li])") | Out-Null }
|
||||||
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
|
}
|
||||||
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
$formXmlSb.Append("`t`t<Attributes/>") | Out-Null
|
$formXmlSb.Append("`t`t<Attributes/>") | Out-Null
|
||||||
@@ -917,8 +1171,21 @@ function Borrow-Form {
|
|||||||
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Здесь источник не XmlWriter, а OuterXml исходного документа — спацовывает так же.
|
||||||
|
$formXmlText = $formXmlSb.ToString()
|
||||||
|
$formXmlText = [regex]::Replace($formXmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Файл создаём мы — канон выгрузки: CRLF в разделителях строк.
|
||||||
|
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
if ($script:droppedLinks.Count -gt 0) {
|
||||||
|
$uniq = @($script:droppedLinks | Sort-Object -Unique)
|
||||||
|
Warn " Вырезано связей параметров выбора: $($uniq.Count) — путь не разрешается в расширении: $($uniq -join ', ')"
|
||||||
|
$script:droppedLinks = @()
|
||||||
|
}
|
||||||
|
|
||||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
# not clobber user code added to the form module).
|
# not clobber user code added to the form module).
|
||||||
@@ -1023,8 +1290,16 @@ function Register-FormInObject {
|
|||||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||||
Info " Registered form in: $objFile"
|
Info " Registered form in: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1071,8 +1346,46 @@ function Build-InternalInfoXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 11b. Collect DataPath references from source Form.xml ---
|
# --- 11b. Collect DataPath references from source Form.xml ---
|
||||||
|
# --- 11b1. Основной реквизит исходной формы ---
|
||||||
|
# Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы.
|
||||||
|
# У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка —
|
||||||
|
# «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData.
|
||||||
|
# Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке.
|
||||||
|
# Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2).
|
||||||
|
# Имена реквизитов ИСХОДНОЙ формы → их id. Ссылки параметров выбора адресуют реквизит формы
|
||||||
|
# именно по id базовой формы (см. Rewrite-ChoiceParameterLinks).
|
||||||
|
function Get-FormAttributeIds {
|
||||||
|
param($formEl)
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$attrs = $formEl.SelectSingleNode("*[local-name()='Attributes']")
|
||||||
|
if (-not $attrs) { return $result }
|
||||||
|
foreach ($a in $attrs.ChildNodes) {
|
||||||
|
if ($a.NodeType -ne 'Element' -or $a.LocalName -ne 'Attribute') { continue }
|
||||||
|
$nm = $a.GetAttribute("name"); $id = $a.GetAttribute("id")
|
||||||
|
if ($nm -and $id) { $result[$nm] = $id }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MainAttributeInfo {
|
||||||
|
param($formEl, [string]$nsStripPattern)
|
||||||
|
|
||||||
|
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
|
||||||
|
if (-not $mainAttr) { return $null }
|
||||||
|
$xml = [regex]::Replace($mainAttr.OuterXml, $nsStripPattern, '')
|
||||||
|
# id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои
|
||||||
|
$xml = [regex]::Replace($xml, '^(<Attribute\s[^>]*?)id="[^"]*"', "`${1}id=`"$script:mainAttrId`"")
|
||||||
|
return @{ Name = $mainAttr.GetAttribute("name"); Xml = $xml }
|
||||||
|
}
|
||||||
|
|
||||||
function Collect-FormDataPaths {
|
function Collect-FormDataPaths {
|
||||||
param([string]$formXmlPath)
|
param([string]$formXmlPath, [string]$mainAttrName)
|
||||||
|
|
||||||
|
# Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы
|
||||||
|
# списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных
|
||||||
|
# форм, и в оболочку не заимствовалось ни одного дочернего объекта.
|
||||||
|
$root = [regex]::Escape($mainAttrName)
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
$content = [System.IO.File]::ReadAllText($formXmlPath, $enc)
|
$content = [System.IO.File]::ReadAllText($formXmlPath, $enc)
|
||||||
@@ -1083,7 +1396,7 @@ function Collect-FormDataPaths {
|
|||||||
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
foreach ($tag in $script:formBindingDataTags) {
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\b$root\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
foreach ($m in $bms) {
|
foreach ($m in $bms) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
@@ -1101,7 +1414,7 @@ function Collect-FormDataPaths {
|
|||||||
|
|
||||||
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\b$root\.(\w+(?:\.\w+)*)</Field>")
|
||||||
foreach ($m in $fieldMatches) {
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
@@ -1115,6 +1428,30 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
|
||||||
|
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
|
||||||
|
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
|
||||||
|
$acMatches = [regex]::Matches($content, "<AdditionalColumns table=`"$root\.(\w+)`"")
|
||||||
|
foreach ($m in $acMatches) {
|
||||||
|
$seg0 = $m.Groups[1].Value
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Текст запроса динамического списка — такое же место ссылки на реквизиты объекта, как DataPath.
|
||||||
|
# Конфигуратор заимствует всё, что упомянуто в запросе: на эталоне Issue66Example2 это 21 из 27
|
||||||
|
# дочерних объектов, совпадение с ним точное в обе стороны. У списка без ручного запроса
|
||||||
|
# (<QueryText> нет) заимствуется только видимое на форме — эталон Issue66Example3.
|
||||||
|
# Разбирать язык запросов не нужно: имена-кандидаты отфильтрует Resolve-SourceAttributes по
|
||||||
|
# реальному составу объекта, поэтому лишние слова из запроса безвредны.
|
||||||
|
foreach ($qm in [regex]::Matches($content, '(?s)<QueryText>(.*?)</QueryText>')) {
|
||||||
|
foreach ($w in [regex]::Matches($qm.Groups[1].Value, '[\w]+')) {
|
||||||
|
$word = $w.Value
|
||||||
|
if ($script:standardFields -contains $word) { continue }
|
||||||
|
$firstLevel[$word] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
@@ -1165,7 +1502,11 @@ function Resolve-SourceAttributes {
|
|||||||
foreach ($child in $childObjs.ChildNodes) {
|
foreach ($child in $childObjs.ChildNodes) {
|
||||||
if ($child.NodeType -ne 'Element') { continue }
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
|
||||||
if ($child.LocalName -eq 'Attribute') {
|
# Реквизит объекта, измерение и ресурс регистра — один и тот же вид дочернего объекта с
|
||||||
|
# точки зрения заимствования, различается только имя элемента. Конфигуратор переносит их
|
||||||
|
# своим видом (эталон Issue66Example2: у регистра <Dimension> x3 и <Resource>), поэтому вид
|
||||||
|
# запоминается и выпускается как есть — иначе измерение уехало бы в файл как <Attribute>.
|
||||||
|
if ($script:childObjectKinds -ccontains $child.LocalName) {
|
||||||
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
|
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
|
||||||
if (-not $nameNode) { continue }
|
if (-not $nameNode) { continue }
|
||||||
$attrName = $nameNode.InnerText
|
$attrName = $nameNode.InnerText
|
||||||
@@ -1177,7 +1518,7 @@ function Resolve-SourceAttributes {
|
|||||||
# Strip namespace declarations from Type
|
# Strip namespace declarations from Type
|
||||||
$typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
$typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
|
||||||
$attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml }
|
$attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml; Kind = $child.LocalName }
|
||||||
}
|
}
|
||||||
elseif ($child.LocalName -eq 'TabularSection') {
|
elseif ($child.LocalName -eq 'TabularSection') {
|
||||||
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
|
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
|
||||||
@@ -1227,8 +1568,16 @@ function Resolve-SourceAttributes {
|
|||||||
$extraProps = [ordered]@{}
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
# NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
|
||||||
"NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity")
|
# тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
|
||||||
|
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
|
||||||
|
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
|
||||||
|
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
|
||||||
|
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
|
||||||
|
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
|
||||||
|
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
|
||||||
|
$propsToExtract = @("Hierarchical","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
|
"NumberType","NumberLength","NumberAllowedLength")
|
||||||
foreach ($pName in $propsToExtract) {
|
foreach ($pName in $propsToExtract) {
|
||||||
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
||||||
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
||||||
@@ -1240,11 +1589,11 @@ function Resolve-SourceAttributes {
|
|||||||
|
|
||||||
# --- 11d. Build adopted attribute XML ---
|
# --- 11d. Build adopted attribute XML ---
|
||||||
function Build-AdoptedAttributeXml {
|
function Build-AdoptedAttributeXml {
|
||||||
param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent)
|
param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent, [string]$kind = "Attribute")
|
||||||
|
|
||||||
$newUuid = [guid]::NewGuid().ToString()
|
$newUuid = [guid]::NewGuid().ToString()
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
$sb.AppendLine("${indent}<Attribute uuid=`"${newUuid}`">") | Out-Null
|
$sb.AppendLine("${indent}<${kind} uuid=`"${newUuid}`">") | Out-Null
|
||||||
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
|
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
|
||||||
$sb.AppendLine("${indent}`t<Properties>") | Out-Null
|
$sb.AppendLine("${indent}`t<Properties>") | Out-Null
|
||||||
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
|
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
|
||||||
@@ -1253,7 +1602,7 @@ function Build-AdoptedAttributeXml {
|
|||||||
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
|
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
|
||||||
$sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null
|
$sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null
|
||||||
$sb.AppendLine("${indent}`t</Properties>") | Out-Null
|
$sb.AppendLine("${indent}`t</Properties>") | Out-Null
|
||||||
$sb.Append("${indent}</Attribute>") | Out-Null
|
$sb.Append("${indent}</${kind}>") | Out-Null
|
||||||
return $sb.ToString()
|
return $sb.ToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1376,14 +1725,6 @@ function Merge-AttributesIntoObject {
|
|||||||
$added = 0
|
$added = 0
|
||||||
foreach ($attr in $attrsToAdd) {
|
foreach ($attr in $attrsToAdd) {
|
||||||
if ($existingNames.ContainsKey($attr.Name)) { continue }
|
if ($existingNames.ContainsKey($attr.Name)) { continue }
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
|
||||||
|
|
||||||
# Expand self-closing ChildObjects if needed
|
|
||||||
if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) {
|
|
||||||
$closeWs = $objDoc.CreateWhitespace("`r`n`t`t")
|
|
||||||
$childObjs.AppendChild($closeWs) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$added++
|
$added++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1392,7 +1733,8 @@ function Merge-AttributesIntoObject {
|
|||||||
$allAttrXml = ""
|
$allAttrXml = ""
|
||||||
foreach ($attr in $attrsToAdd) {
|
foreach ($attr in $attrsToAdd) {
|
||||||
if ($existingNames.ContainsKey($attr.Name)) { continue }
|
if ($existingNames.ContainsKey($attr.Name)) { continue }
|
||||||
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t")
|
$kind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
|
||||||
|
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save via text manipulation to avoid namespace issues with InnerXml
|
# Save via text manipulation to avoid namespace issues with InnerXml
|
||||||
@@ -1410,10 +1752,21 @@ function Merge-AttributesIntoObject {
|
|||||||
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
||||||
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
|
||||||
# Insert attributes before </ChildObjects>
|
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
||||||
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
|
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
||||||
|
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
||||||
|
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
||||||
|
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
||||||
Info " Merged $added attribute(s) into: $objFile"
|
Info " Merged $added attribute(s) into: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1435,7 +1788,16 @@ function Borrow-MainAttribute {
|
|||||||
Write-Error "Source Form.xml not found: $srcFormXmlPath"
|
Write-Error "Source Form.xml not found: $srcFormXmlPath"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$dp = Collect-FormDataPaths $srcFormXmlPath
|
# Имя основного реквизита исходной формы — корень путей, которые надо собрать
|
||||||
|
$dpDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$dpDoc.PreserveWhitespace = $true
|
||||||
|
$dpDoc.Load($srcFormXmlPath)
|
||||||
|
$dpInfo = Get-MainAttributeInfo $dpDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
|
||||||
|
if (-not $dpInfo) {
|
||||||
|
Warn " У формы нет основного реквизита — заимствовать нечего"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$dp = Collect-FormDataPaths $srcFormXmlPath $dpInfo.Name
|
||||||
$firstLevelNames = $dp.FirstLevel
|
$firstLevelNames = $dp.FirstLevel
|
||||||
$deepPaths = $dp.DeepPaths
|
$deepPaths = $dp.DeepPaths
|
||||||
Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths"
|
Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths"
|
||||||
@@ -1461,19 +1823,15 @@ function Borrow-MainAttribute {
|
|||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
$existingChildNames = @{}
|
$existingChildNames = Get-OwnChildObjectNames $objFile
|
||||||
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
|
||||||
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
|
||||||
$existingChildNames[$nm.Groups[1].Value] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $insertAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrKind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
|
||||||
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $insertTS) {
|
foreach ($ts in $insertTS) {
|
||||||
@@ -1498,17 +1856,9 @@ function Borrow-MainAttribute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
|
||||||
if ($adoptedContent) {
|
if ($adoptedContent) {
|
||||||
# Handle <ChildObjects/> (self-closing)
|
$objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}"
|
||||||
if ($objContent -match '<ChildObjects\s*/>') {
|
|
||||||
$objContent = $objContent -replace '<ChildObjects\s*/>', "<ChildObjects>`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
|
|
||||||
}
|
|
||||||
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
|
|
||||||
elseif ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
|
||||||
$existingInner = $Matches[1]
|
|
||||||
$objContent = $objContent -replace '(?s)<ChildObjects>(.*?)</ChildObjects>', "<ChildObjects>${existingInner}`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -1521,6 +1871,21 @@ function Borrow-MainAttribute {
|
|||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $srcTS) {
|
||||||
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
||||||
}
|
}
|
||||||
|
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
|
||||||
|
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
|
||||||
|
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
|
||||||
|
# формы заказа поставщику).
|
||||||
|
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||||
|
if (Test-Path $srcFormForCols) {
|
||||||
|
$colsDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$colsDoc.PreserveWhitespace = $true
|
||||||
|
$colsDoc.Load($srcFormForCols)
|
||||||
|
$colsInfo = Get-MainAttributeInfo $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
|
||||||
|
if ($colsInfo) {
|
||||||
|
foreach ($m in [regex]::Matches($colsInfo.Xml, '(?s)<Columns>.*?</Columns>')) { $allTypeXmls += $m.Value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
||||||
Info " Reference types to borrow: $($refTypes.Count)"
|
Info " Reference types to borrow: $($refTypes.Count)"
|
||||||
|
|
||||||
@@ -1694,6 +2059,16 @@ function Build-BorrowedObjectXml {
|
|||||||
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Свойства, от которых зависят стандартные поля (см. $script:typeGateProps / $script:typesWithOwners)
|
||||||
|
foreach ($gp in @($script:typeGateProps[$typeName])) {
|
||||||
|
if ($gp -and $sourceProps.ContainsKey($gp)) {
|
||||||
|
$sb.AppendLine("`t`t`t<${gp}>$($sourceProps[$gp])</${gp}>") | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($sourceProps.ContainsKey("__OwnersXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__OwnersXml'])") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
@@ -1861,6 +2236,47 @@ foreach ($item in $items) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 14b. Владельцы заимствованных справочников ---
|
||||||
|
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает при
|
||||||
|
# загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
|
||||||
|
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
|
||||||
|
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
|
||||||
|
$ownerPass = 0
|
||||||
|
while ($true) {
|
||||||
|
$ownerPass++
|
||||||
|
if ($ownerPass -gt 10) { break }
|
||||||
|
$newOwners = @()
|
||||||
|
foreach ($shell in (Get-ChildItem -Path $extDir -Filter "*.xml" -Recurse -File)) {
|
||||||
|
$shellText = [System.IO.File]::ReadAllText($shell.FullName)
|
||||||
|
if ($shellText -notmatch '<Owners>') { continue }
|
||||||
|
foreach ($om in [regex]::Matches($shellText, '<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>')) {
|
||||||
|
$oType = $om.Groups[1].Value; $oName = $om.Groups[2].Value
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($oType)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $oType $oName) { continue }
|
||||||
|
if ($newOwners | Where-Object { $_.T -eq $oType -and $_.N -eq $oName }) { continue }
|
||||||
|
$newOwners += @{ T = $oType; N = $oName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($newOwners.Count -eq 0) { break }
|
||||||
|
foreach ($ow in $newOwners) {
|
||||||
|
$owSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$ow.T]) "$($ow.N).xml"
|
||||||
|
if (-not (Test-Path $owSrcFile)) {
|
||||||
|
Warn " Владелец $($ow.T).$($ow.N) не найден в источнике — ссылка останется висячей"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$owSrc = Read-SourceObject $ow.T $ow.N
|
||||||
|
$owXml = Build-BorrowedObjectXml $ow.T $ow.N $owSrc.Uuid $owSrc.Properties
|
||||||
|
$owDir = Join-Path $extDir $childTypeDirMap[$ow.T]
|
||||||
|
if (-not (Test-Path $owDir)) { New-Item -ItemType Directory -Path $owDir -Force | Out-Null }
|
||||||
|
$owFile = Join-Path $owDir "$($ow.N).xml"
|
||||||
|
$owEnc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($owFile, $owXml, $owEnc)
|
||||||
|
Add-ToChildObjects $ow.T $ow.N
|
||||||
|
$script:borrowedFiles += $owFile
|
||||||
|
Info " Auto-borrowed owner: $($ow.T).$($ow.N)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 15. Save modified Configuration.xml ---
|
# --- 15. Save modified Configuration.xml ---
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -1877,8 +2293,16 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
||||||
Info "Saved: $extResolvedPath"
|
Info "Saved: $extResolvedPath"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
@@ -50,8 +50,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -Exte
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор — что изменено в расширении
|
# Обзор — что изменено в расширении
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
|
|
||||||
# Проверка переноса — все ли #Вставка перенесены
|
# Проверка переноса — все ли #Вставка перенесены
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -49,7 +49,9 @@ $childTypeDirMap = @{
|
|||||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"CommonAttribute"="CommonAttributes"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||||
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
|
"Bot"="Bots"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Parse extension Configuration.xml ---
|
# --- Parse extension Configuration.xml ---
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespace maps ---
|
# --- Namespace maps ---
|
||||||
|
|
||||||
MD_NSMAP = {
|
MD_NSMAP = {
|
||||||
@@ -60,6 +82,12 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"Sequence": "Sequences",
|
"Sequence": "Sequences",
|
||||||
"IntegrationService": "IntegrationServices",
|
"IntegrationService": "IntegrationServices",
|
||||||
"CommonAttribute": "CommonAttributes",
|
"CommonAttribute": "CommonAttributes",
|
||||||
|
"Style": "Styles",
|
||||||
|
"XDTOPackage": "XDTOPackages",
|
||||||
|
"WebService": "WebServices",
|
||||||
|
"HTTPService": "HTTPServices",
|
||||||
|
"WSReference": "WSReferences",
|
||||||
|
"Bot": "Bots",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -468,7 +496,7 @@ def main():
|
|||||||
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
||||||
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
||||||
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
|
|||||||
@@ -33,39 +33,39 @@ allowed-tools:
|
|||||||
| `Name` | Имя расширения (обязат.) | — |
|
| `Name` | Имя расширения (обязат.) | — |
|
||||||
| `Synonym` | Синоним | = Name |
|
| `Synonym` | Синоним | = Name |
|
||||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||||
| `OutputDir` | Каталог для создания | `src` |
|
| `OutputDir` | Каталог для создания; клади расширение в свой подкаталог, названный по имени: `src\cfe\<Name>` | `src` |
|
||||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||||
| `Version` | Версия расширения | — |
|
| `Version` | Версия расширения | — |
|
||||||
| `Vendor` | Поставщик | — |
|
| `Vendor` | Поставщик | — |
|
||||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
| `CompatibilityMode` | Режим совместимости; при заданном `ConfigPath` определяется по базовой конфигурации и этот параметр не нужен | `Version8_3_24` |
|
||||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||||
| `NoRole` | Без основной роли | false |
|
| `NoRole` | Без основной роли | false |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
... -Name Расш1 -ConfigPath src\cf -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-исправление с явным режимом совместимости
|
# Расширение-исправление с явным режимом совместимости
|
||||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-доработка с версией
|
# Расширение-доработка с версией
|
||||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src\cfe\МоёРасширение
|
||||||
|
|
||||||
# Без роли, с явным префиксом
|
# Без роли, с явным префиксом
|
||||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src\cfe\ИсправлениеБага
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <OutputDir>
|
/cfe-validate <OutputDir> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -16,6 +16,12 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Default NamePrefix ---
|
# --- Default NamePrefix ---
|
||||||
@@ -121,20 +127,23 @@ $co7 = [guid]::NewGuid().ToString()
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
$synonymXml = ""
|
$synonymXml = ""
|
||||||
if ($Synonym) {
|
if ($Synonym) {
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional properties ---
|
# --- Optional properties ---
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
$defaultRolesXml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
$defaultRolesEl = "<DefaultRoles/>"
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
$defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
@@ -144,10 +153,32 @@ if (-not $NoRole) {
|
|||||||
}
|
}
|
||||||
$childObjectsXml += "`r`n`t`t"
|
$childObjectsXml += "`r`n`t`t"
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
$f221Captions = ""
|
||||||
|
if ((Get-FormatRank $formatVersion) -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
$f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -181,21 +212,21 @@ $cfgXml = @"
|
|||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
<Synonym>$synonymXml</Synonym>
|
<Synonym>$synonymXml</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
<NamePrefix>$(Esc-XmlText ($NamePrefix))</NamePrefix>
|
||||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
</UsePurposes>
|
</UsePurposes>
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
$defaultRolesEl
|
||||||
<Vendor>$vendorXml</Vendor>
|
$vendorEl
|
||||||
<Version>$versionXml</Version>
|
$versionEl$f221Captions
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
<BriefInformation/>
|
<BriefInformation/>
|
||||||
<DetailedInformation/>
|
<DetailedInformation/>
|
||||||
@@ -212,7 +243,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -229,10 +260,10 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$(Esc-XmlText ($roleName))</Name>
|
||||||
<Synonym/>
|
<Synonym/>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
</Properties>
|
</Properties>
|
||||||
@@ -252,9 +283,18 @@ if (-not (Test-Path $langDir)) {
|
|||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
Write-XmlFile $langFile $langXml $enc
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
@@ -263,7 +303,7 @@ if (-not $NoRole) {
|
|||||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
Write-XmlFile $roleFile $roleXml $enc
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
|
|||||||
@@ -1,20 +1,62 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -29,7 +71,7 @@ def main():
|
|||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
||||||
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -126,18 +168,23 @@ def main():
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
synonym_xml = ""
|
synonym_xml = ""
|
||||||
if synonym:
|
if synonym:
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
version_xml = esc_xml(version) if version else ""
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
role_name = f"{name_prefix}ОсновнаяРоль"
|
role_name = f"{name_prefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
default_roles_xml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
default_roles_el = "<DefaultRoles/>"
|
||||||
if not args.NoRole:
|
if not args.NoRole:
|
||||||
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
|
default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
|
||||||
|
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
|
||||||
|
'\r\n\t\t\t</DefaultRoles>')
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
||||||
@@ -156,6 +203,40 @@ def main():
|
|||||||
]
|
]
|
||||||
|
|
||||||
contained_objects = ""
|
contained_objects = ""
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
f221_captions = ""
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
||||||
@@ -163,27 +244,27 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
||||||
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
|
\t\t\t<NamePrefix>{esc_xml_text(name_prefix)}</NamePrefix>
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
\t\t\t<UsePurposes>
|
\t\t\t<UsePurposes>
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
\t\t\t</UsePurposes>
|
\t\t\t</UsePurposes>
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
|
\t\t\t{default_roles_el}
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
\t\t\t{vendor_el}
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
\t\t\t{version_el}{f221_captions}
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
\t\t\t<BriefInformation/>
|
\t\t\t<BriefInformation/>
|
||||||
\t\t\t<DetailedInformation/>
|
\t\t\t<DetailedInformation/>
|
||||||
@@ -198,7 +279,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -213,10 +294,10 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml_text(role_name)}</Name>
|
||||||
\t\t\t<Synonym/>
|
\t\t\t<Synonym/>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t</Properties>
|
\t\t</Properties>
|
||||||
@@ -229,9 +310,9 @@ def main():
|
|||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_xml_file(lang_file, lang_xml)
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
role_file = None
|
role_file = None
|
||||||
@@ -239,7 +320,7 @@ def main():
|
|||||||
role_dir = os.path.join(output_dir, "Roles")
|
role_dir = os.path.join(output_dir, "Roles")
|
||||||
os.makedirs(role_dir, exist_ok=True)
|
os.makedirs(role_dir, exist_ok=True)
|
||||||
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
||||||
write_utf8_bom(role_file, role_xml)
|
write_xml_file(role_file, role_xml)
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
print(f"[OK] Создано расширение: {name}")
|
print(f"[OK] Создано расширение: {name}")
|
||||||
|
|||||||
@@ -110,36 +110,36 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
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.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Код перед записью
|
# Код перед записью
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
|
||||||
# Перехват После на форме
|
# Перехват После на форме
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||||
|
|
||||||
# Замена функции (ПродолжитьВызов)
|
# Замена функции (ПродолжитьВызов)
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
# ИзменениеИКонтроль — копия тела для правки маркерами
|
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||||
|
|
||||||
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
... -ExtensionPath src\cfe\extname -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
# Проверить все контролируемые методы расширения на дрейф
|
# Проверить все контролируемые методы расширения на дрейф
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Check
|
||||||
|
|
||||||
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Actualize
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <ExtensionPath>
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE)
|
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE)
|
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
TYPE_DIR_MAP = {
|
TYPE_DIR_MAP = {
|
||||||
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
|
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
|
||||||
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
|
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
|
||||||
@@ -19,6 +41,24 @@ TYPE_DIR_MAP = {
|
|||||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
|
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
|
||||||
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||||||
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
|
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
|
||||||
|
# Прощающий ввод: имя каталога принимается наравне с именем типа (Catalogs.X ≡ Catalog.X) —
|
||||||
|
# PS1-порт так умел с самого начала, PY отставал.
|
||||||
|
"Catalogs": "Catalogs",
|
||||||
|
"Documents": "Documents",
|
||||||
|
"Enums": "Enums",
|
||||||
|
"CommonModules": "CommonModules",
|
||||||
|
"Reports": "Reports",
|
||||||
|
"DataProcessors": "DataProcessors",
|
||||||
|
"ExchangePlans": "ExchangePlans",
|
||||||
|
"ChartsOfAccounts": "ChartsOfAccounts",
|
||||||
|
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||||
|
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||||
|
"BusinessProcesses": "BusinessProcesses",
|
||||||
|
"Tasks": "Tasks",
|
||||||
|
"InformationRegisters": "InformationRegisters",
|
||||||
|
"AccumulationRegisters": "AccumulationRegisters",
|
||||||
|
"AccountingRegisters": "AccountingRegisters",
|
||||||
|
"CalculationRegisters": "CalculationRegisters",
|
||||||
}
|
}
|
||||||
# accept plural forms too
|
# accept plural forms too
|
||||||
for _v in list(TYPE_DIR_MAP.values()):
|
for _v in list(TYPE_DIR_MAP.values()):
|
||||||
@@ -539,7 +579,7 @@ def main():
|
|||||||
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
||||||
parser.add_argument("-Check", action="store_true")
|
parser.add_argument("-Check", action="store_true")
|
||||||
parser.add_argument("-Actualize", action="store_true")
|
parser.add_argument("-Actualize", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cfe-validate
|
name: cfe-validate
|
||||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -17,13 +17,24 @@ allowed-tools:
|
|||||||
| Параметр | Обяз. | Умолч. | Описание |
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|---------------|:-----:|---------|-------------------------------------------------|
|
|---------------|:-----:|---------|-------------------------------------------------|
|
||||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||||
|
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
|
||||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
| OutFile | нет | — | Записать результат в файл |
|
||||||
|
|
||||||
|
### ConfigPath
|
||||||
|
|
||||||
|
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
|
||||||
|
|
||||||
|
Если пользователь не указал путь — определи сам:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default`)
|
||||||
|
3. Возьми её поле `configSrc`
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-validate v1.5 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -9,7 +9,11 @@ param(
|
|||||||
|
|
||||||
[int]$MaxErrors = 30,
|
[int]$MaxErrors = 30,
|
||||||
|
|
||||||
[string]$OutFile
|
[string]$OutFile,
|
||||||
|
|
||||||
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
[string]$ConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -89,6 +93,19 @@ $finalize = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||||
@@ -108,7 +125,7 @@ $validClassIds = @(
|
|||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -122,7 +139,7 @@ $childObjectTypes = @(
|
|||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
$childTypeDirMap = @{
|
$childTypeDirMap = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
@@ -144,6 +161,46 @@ $childTypeDirMap = @{
|
|||||||
"IntegrationService"="IntegrationServices"
|
"IntegrationService"="IntegrationServices"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
$generatedTypeCategories = @{
|
||||||
|
"Catalog" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Document" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Enum" = @("Ref","Manager","List")
|
||||||
|
"Constant" = @("Manager","ValueManager","ValueKey")
|
||||||
|
"Report" = @("Object","Manager")
|
||||||
|
"DataProcessor" = @("Object","Manager")
|
||||||
|
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Task" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
|
||||||
|
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
|
||||||
|
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
|
||||||
|
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
|
||||||
|
"InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager")
|
||||||
|
"AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey")
|
||||||
|
"AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions")
|
||||||
|
"CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs")
|
||||||
|
"DocumentJournal" = @("Selection","List","Manager")
|
||||||
|
"Sequence" = @("Record","Manager","RecordSet")
|
||||||
|
"FilterCriterion" = @("Manager","List")
|
||||||
|
"SettingsStorage" = @("Manager")
|
||||||
|
"IntegrationService" = @("Manager")
|
||||||
|
"WSReference" = @("Manager")
|
||||||
|
"DefinedType" = @("DefinedType")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
$script:standardObjectFields = @(
|
||||||
|
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
|
||||||
|
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
|
||||||
|
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
|
||||||
|
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
|
||||||
|
"Движения","ВерсияДанных","КоличествоСтрок"
|
||||||
|
)
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
$validEnumValues = @{
|
$validEnumValues = @{
|
||||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
||||||
@@ -195,11 +252,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
|
$versionRank = Get-FormatRank $version
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
} elseif ($versionRank -eq 0) {
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
Report-Error "1. Malformed version '$version' (expected N.N)"
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -537,6 +598,7 @@ if ($script:stopped) { & $finalize; exit 1 }
|
|||||||
|
|
||||||
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
||||||
$script:enumValuesIndex = @{}
|
$script:enumValuesIndex = @{}
|
||||||
|
$script:borrowedTSIndex = @{}
|
||||||
$script:formList = @()
|
$script:formList = @()
|
||||||
|
|
||||||
# Helper: check if sub-item has explicit borrowed metadata
|
# Helper: check if sub-item has explicit borrowed metadata
|
||||||
@@ -640,6 +702,25 @@ if ($childObjNode) {
|
|||||||
} else {
|
} else {
|
||||||
$borrowedOk++
|
$borrowedOk++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
$expectedCats = $generatedTypeCategories[$typeName]
|
||||||
|
if ($expectedCats) {
|
||||||
|
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
|
$foundCats = @{}
|
||||||
|
if ($objInfo) {
|
||||||
|
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
|
||||||
|
$cat = $gt.GetAttribute("category")
|
||||||
|
if ($cat) { $foundCats[$cat] = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
|
||||||
|
if ($missingCats.Count -gt 0) {
|
||||||
|
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
|
||||||
|
$check9Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
@@ -667,6 +748,12 @@ if ($childObjNode) {
|
|||||||
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
||||||
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ($tsName) {
|
||||||
|
$tsKey = "${typeName}.${childName}"
|
||||||
|
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
|
||||||
|
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
|
||||||
|
}
|
||||||
if (-not $tsInfo) {
|
if (-not $tsInfo) {
|
||||||
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
||||||
$check10Ok = $false
|
$check10Ok = $false
|
||||||
@@ -896,6 +983,38 @@ foreach ($bf in $script:borrowedFormsWithTree) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
|
||||||
|
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки ниже на
|
||||||
|
# таких формах молча не срабатывали. Ищем сначала в <Attributes> самой формы, потом в <BaseForm>.
|
||||||
|
$rootName = ""
|
||||||
|
$rootMatch = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
|
||||||
|
if ($rootMatch.Success) { $rootName = $rootMatch.Groups[1].Value }
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
$acTables = @{}
|
||||||
|
if ($rootName) {
|
||||||
|
$rootPat = [regex]::Escape($rootName)
|
||||||
|
foreach ($m in [regex]::Matches($raw, "<AdditionalColumns table=`"${rootPat}\.(\w+)`"")) {
|
||||||
|
$acTables[$m.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
if ($acTables.Count -gt 0) {
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerTS = $script:borrowedTSIndex[$ownerKey]
|
||||||
|
foreach ($tblName in $acTables.Keys) {
|
||||||
|
$depCheckCount++
|
||||||
|
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
|
||||||
|
Report-Error "12. ${ctx}: <AdditionalColumns table=`"${rootName}.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
|
||||||
|
$check12Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($mi in $missingItems) {
|
foreach ($mi in $missingItems) {
|
||||||
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
||||||
$check12Ok = $false
|
$check12Ok = $false
|
||||||
@@ -931,6 +1050,127 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
|
|||||||
Report-OK "13. TypeLink: clean"
|
Report-OK "13. TypeLink: clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
|
||||||
|
if (-not $ConfigPath) {
|
||||||
|
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
|
||||||
|
} else {
|
||||||
|
$cfgRoot = $ConfigPath
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
|
||||||
|
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
|
||||||
|
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
|
||||||
|
} else {
|
||||||
|
$check14Ok = $true
|
||||||
|
$pathCheckCount = 0
|
||||||
|
|
||||||
|
foreach ($bf in $script:borrowedFormsWithTree) {
|
||||||
|
$raw = $bf.RawText
|
||||||
|
$ctx = $bf.Context
|
||||||
|
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
|
||||||
|
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
|
||||||
|
$rootMatch14 = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
|
||||||
|
if (-not $rootMatch14.Success) { continue }
|
||||||
|
$rootName = $rootMatch14.Groups[1].Value
|
||||||
|
# У динамического списка набор полей — результат его запроса, а не состав объекта:
|
||||||
|
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
|
||||||
|
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
|
||||||
|
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
|
||||||
|
if ($rootMatch14.Value -match '>cfg:DynamicList<') { continue }
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerParts = $ownerKey -split '\.', 2
|
||||||
|
if ($ownerParts.Count -lt 2) { continue }
|
||||||
|
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
|
||||||
|
$ownerDir = $childTypeDirMap[$ownerType]
|
||||||
|
if (-not $ownerDir) { continue }
|
||||||
|
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
|
||||||
|
if (-not (Test-Path $srcObjFile)) {
|
||||||
|
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
$srcNames = @{}
|
||||||
|
$srcTSColumns = @{}
|
||||||
|
$srcDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$srcDoc.PreserveWhitespace = $false
|
||||||
|
$srcDoc.Load($srcObjFile)
|
||||||
|
$srcObjEl = $null
|
||||||
|
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
|
||||||
|
}
|
||||||
|
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
|
||||||
|
if ($srcChildObjects) {
|
||||||
|
foreach ($sub in $srcChildObjects.ChildNodes) {
|
||||||
|
if ($sub.NodeType -ne 'Element') { continue }
|
||||||
|
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них замена
|
||||||
|
# корня превратила бы тихий пропуск в ложные ошибки на форме записи.
|
||||||
|
if ($sub.LocalName -notin @('Attribute','Dimension','Resource','TabularSection')) { continue }
|
||||||
|
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if (-not $nameNode) { continue }
|
||||||
|
$subName = $nameNode.InnerText.Trim()
|
||||||
|
$srcNames[$subName] = $true
|
||||||
|
if ($sub.LocalName -ne 'TabularSection') { continue }
|
||||||
|
$cols = @{}
|
||||||
|
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
|
||||||
|
$cols[$colName.InnerText.Trim()] = $true
|
||||||
|
}
|
||||||
|
$srcTSColumns[$subName] = $cols
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
$rootPat14 = [regex]::Escape($rootName)
|
||||||
|
foreach ($acm in [regex]::Matches($raw, "(?s)<AdditionalColumns table=`"${rootPat14}\.(\w+)`">(.*?)</AdditionalColumns>")) {
|
||||||
|
$tbl = $acm.Groups[1].Value
|
||||||
|
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
|
||||||
|
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
|
||||||
|
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$badPaths = @{}
|
||||||
|
foreach ($m in [regex]::Matches($raw, "<(?:\w+:)?\w*DataPath[^>]*>${rootPat14}\.([^<]+)</(?:\w+:)?\w*DataPath>")) {
|
||||||
|
$segments = $m.Groups[1].Value -split '\.'
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
$pathCheckCount++
|
||||||
|
if ($script:standardObjectFields -contains $seg0) { continue }
|
||||||
|
if (-not $srcNames.ContainsKey($seg0)) {
|
||||||
|
$badPaths["${rootName}.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardObjectFields -contains $seg1) { continue }
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
|
||||||
|
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
|
||||||
|
$badPaths["${rootName}.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
|
||||||
|
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
|
||||||
|
$check14Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($check14Ok) {
|
||||||
|
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:stopped) { & $finalize; exit 1 }
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
$extRootDir = Split-Path $resolvedPath -Parent
|
$extRootDir = Split-Path $resolvedPath -Parent
|
||||||
$ctrlCount = 0
|
$ctrlCount = 0
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.5 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -37,7 +59,7 @@ VALID_CLASS_IDS = [
|
|||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -73,6 +96,50 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'IntegrationService': 'IntegrationServices',
|
'IntegrationService': 'IntegrationServices',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
GENERATED_TYPE_CATEGORIES = {
|
||||||
|
'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Enum': ['Ref', 'Manager', 'List'],
|
||||||
|
'Constant': ['Manager', 'ValueManager', 'ValueKey'],
|
||||||
|
'Report': ['Object', 'Manager'],
|
||||||
|
'DataProcessor': ['Object', 'Manager'],
|
||||||
|
'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'],
|
||||||
|
'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'],
|
||||||
|
'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'],
|
||||||
|
'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'],
|
||||||
|
'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'],
|
||||||
|
'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'],
|
||||||
|
'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'],
|
||||||
|
'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'],
|
||||||
|
'DocumentJournal': ['Selection', 'List', 'Manager'],
|
||||||
|
'Sequence': ['Record', 'Manager', 'RecordSet'],
|
||||||
|
'FilterCriterion': ['Manager', 'List'],
|
||||||
|
'SettingsStorage': ['Manager'],
|
||||||
|
'IntegrationService': ['Manager'],
|
||||||
|
'WSReference': ['Manager'],
|
||||||
|
'DefinedType': ['DefinedType'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
# Основной реквизит формы: <Attribute name="X"> с <MainAttribute>true</MainAttribute> внутри
|
||||||
|
MAIN_ATTR_RE = re.compile(
|
||||||
|
r'<Attribute name=\"([^\"]+)\"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>', re.DOTALL)
|
||||||
|
|
||||||
|
STANDARD_OBJECT_FIELDS = {
|
||||||
|
'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber',
|
||||||
|
'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount',
|
||||||
|
'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный',
|
||||||
|
'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных',
|
||||||
|
'Движения', 'ВерсияДанных', 'КоличествоСтрок',
|
||||||
|
}
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
VALID_ENUM_VALUES = {
|
VALID_ENUM_VALUES = {
|
||||||
'ConfigurationExtensionCompatibilityMode': [
|
'ConfigurationExtensionCompatibilityMode': [
|
||||||
@@ -94,6 +161,20 @@ VALID_ENUM_VALUES = {
|
|||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
class Reporter:
|
class Reporter:
|
||||||
def __init__(self, max_errors, detailed=False):
|
def __init__(self, max_errors, detailed=False):
|
||||||
@@ -154,11 +235,15 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default='')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
out_file = args.OutFile
|
out_file = args.OutFile
|
||||||
|
config_path_arg = args.ConfigPath
|
||||||
|
|
||||||
# --- Resolve path ---
|
# --- Resolve path ---
|
||||||
if not os.path.isabs(extension_path):
|
if not os.path.isabs(extension_path):
|
||||||
@@ -214,11 +299,17 @@ def main():
|
|||||||
check1_ok = False
|
check1_ok = False
|
||||||
|
|
||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
elif version_rank == 0:
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
r.error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
r.warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
r.warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -537,6 +628,7 @@ def main():
|
|||||||
MD = NS['md']
|
MD = NS['md']
|
||||||
XR = NS['xr']
|
XR = NS['xr']
|
||||||
enum_values_index = {}
|
enum_values_index = {}
|
||||||
|
borrowed_ts_index = {}
|
||||||
form_list = []
|
form_list = []
|
||||||
|
|
||||||
def is_borrowed_sub_item(sub_item):
|
def is_borrowed_sub_item(sub_item):
|
||||||
@@ -636,6 +728,23 @@ def main():
|
|||||||
else:
|
else:
|
||||||
borrowed_ok_count += 1
|
borrowed_ok_count += 1
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name)
|
||||||
|
if expected_cats:
|
||||||
|
obj_info = obj_el.find(f'{{{MD}}}InternalInfo')
|
||||||
|
found_cats = set()
|
||||||
|
if obj_info is not None:
|
||||||
|
for gt in obj_info.findall(f'{{{XR}}}GeneratedType'):
|
||||||
|
cat = gt.get('category')
|
||||||
|
if cat:
|
||||||
|
found_cats.add(cat)
|
||||||
|
missing_cats = [c for c in expected_cats if c not in found_cats]
|
||||||
|
if missing_cats:
|
||||||
|
word = 'category' if len(missing_cats) == 1 else 'categories'
|
||||||
|
r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}")
|
||||||
|
check9_ok = False
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
||||||
if obj_child_objects is not None:
|
if obj_child_objects is not None:
|
||||||
@@ -663,6 +772,9 @@ def main():
|
|||||||
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
||||||
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ts_name_el is not None and ts_name_el.text:
|
||||||
|
borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True
|
||||||
if ts_info is None:
|
if ts_info is None:
|
||||||
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
||||||
check10_ok = False
|
check10_ok = False
|
||||||
@@ -855,6 +967,29 @@ def main():
|
|||||||
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
||||||
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
|
||||||
|
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки на
|
||||||
|
# таких формах молча не срабатывали. Ищем сначала в <Attributes> формы, потом в <BaseForm>.
|
||||||
|
root_match = MAIN_ATTR_RE.search(raw)
|
||||||
|
root_name = root_match.group(1) if root_match else ""
|
||||||
|
ac_tables = set()
|
||||||
|
if root_name:
|
||||||
|
ac_tables = set(re.findall(r'<AdditionalColumns table="' + re.escape(root_name) + r'\.(\w+)"', raw))
|
||||||
|
if ac_tables:
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_ts = borrowed_ts_index.get(owner_key, {})
|
||||||
|
for tbl_name in sorted(ac_tables):
|
||||||
|
dep_check_count += 1
|
||||||
|
if tbl_name not in owner_ts:
|
||||||
|
r.error(f'12. {ctx}: <AdditionalColumns table="{root_name}.{tbl_name}"> — TabularSection.{tbl_name} not borrowed in extension')
|
||||||
|
check12_ok = False
|
||||||
|
|
||||||
for mi in missing_items:
|
for mi in missing_items:
|
||||||
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
||||||
check12_ok = False
|
check12_ok = False
|
||||||
@@ -886,6 +1021,130 @@ def main():
|
|||||||
elif check13_ok:
|
elif check13_ok:
|
||||||
r.ok('13. TypeLink: clean')
|
r.ok('13. TypeLink: clean')
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if not r.stopped and borrowed_forms_with_tree:
|
||||||
|
if not config_path_arg:
|
||||||
|
r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath')
|
||||||
|
else:
|
||||||
|
cfg_root = config_path_arg
|
||||||
|
if not os.path.isabs(cfg_root):
|
||||||
|
cfg_root = os.path.join(os.getcwd(), cfg_root)
|
||||||
|
if os.path.exists(cfg_root) and not os.path.isdir(cfg_root):
|
||||||
|
cfg_root = os.path.dirname(cfg_root)
|
||||||
|
|
||||||
|
if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')):
|
||||||
|
r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена")
|
||||||
|
else:
|
||||||
|
check14_ok = True
|
||||||
|
path_check_count = 0
|
||||||
|
|
||||||
|
for bf in borrowed_forms_with_tree:
|
||||||
|
raw = bf['RawText']
|
||||||
|
ctx = bf['Context']
|
||||||
|
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
|
||||||
|
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
|
||||||
|
root_match14 = MAIN_ATTR_RE.search(raw)
|
||||||
|
if root_match14 is None:
|
||||||
|
continue
|
||||||
|
root_name14 = root_match14.group(1)
|
||||||
|
# У динамического списка набор полей — результат его запроса, а не состав объекта:
|
||||||
|
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
|
||||||
|
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
|
||||||
|
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
|
||||||
|
if '>cfg:DynamicList<' in root_match14.group(0):
|
||||||
|
continue
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_parts = owner_key.split('.', 1)
|
||||||
|
if len(owner_parts) < 2:
|
||||||
|
continue
|
||||||
|
owner_type, owner_name = owner_parts
|
||||||
|
owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type)
|
||||||
|
if not owner_dir:
|
||||||
|
continue
|
||||||
|
src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml')
|
||||||
|
if not os.path.isfile(src_obj_file):
|
||||||
|
r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
src_names = set()
|
||||||
|
src_ts_columns = {}
|
||||||
|
src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True))
|
||||||
|
src_obj_el = None
|
||||||
|
for c in src_tree.getroot():
|
||||||
|
if isinstance(c.tag, str):
|
||||||
|
src_obj_el = c
|
||||||
|
break
|
||||||
|
src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None
|
||||||
|
if src_child_objects is not None:
|
||||||
|
for sub in src_child_objects:
|
||||||
|
if not isinstance(sub.tag, str):
|
||||||
|
continue
|
||||||
|
sub_ln = etree.QName(sub.tag).localname
|
||||||
|
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них
|
||||||
|
# замена корня превратила бы тихий пропуск в ложные ошибки на форме записи.
|
||||||
|
if sub_ln not in ('Attribute', 'Dimension', 'Resource', 'TabularSection'):
|
||||||
|
continue
|
||||||
|
name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
|
if name_el is None or not name_el.text:
|
||||||
|
continue
|
||||||
|
sub_name = name_el.text.strip()
|
||||||
|
src_names.add(sub_name)
|
||||||
|
if sub_ln != 'TabularSection':
|
||||||
|
continue
|
||||||
|
cols = set()
|
||||||
|
for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'):
|
||||||
|
if col_name.text:
|
||||||
|
cols.add(col_name.text.strip())
|
||||||
|
src_ts_columns[sub_name] = cols
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
root_pat14 = re.escape(root_name14)
|
||||||
|
for acm in re.finditer(r'<AdditionalColumns table="' + root_pat14 + r'\.(\w+)">(.*?)</AdditionalColumns>', raw, re.DOTALL):
|
||||||
|
tbl = acm.group(1)
|
||||||
|
cols = src_ts_columns.setdefault(tbl, set())
|
||||||
|
for cm in re.finditer(r'<Column name="(\w+)"', acm.group(2)):
|
||||||
|
cols.add(cm.group(1))
|
||||||
|
|
||||||
|
bad_paths = {}
|
||||||
|
for m in re.finditer(r'<(?:\w+:)?\w*DataPath[^>]*>' + root_pat14 + r'\.([^<]+)</(?:\w+:)?\w*DataPath>', raw):
|
||||||
|
segments = m.group(1).split('.')
|
||||||
|
seg0 = segments[0]
|
||||||
|
path_check_count += 1
|
||||||
|
if seg0 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
if seg0 not in src_names:
|
||||||
|
bad_paths[f'{root_name14}.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части'
|
||||||
|
continue
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if len(segments) < 2 or seg0 not in src_ts_columns:
|
||||||
|
continue
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]:
|
||||||
|
continue
|
||||||
|
if seg1 not in src_ts_columns[seg0]:
|
||||||
|
bad_paths[f'{root_name14}.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и <Columns> формы её не объявляет'
|
||||||
|
|
||||||
|
for bad in sorted(bad_paths):
|
||||||
|
r.error(f"14. {ctx}: путь '{bad}' — {bad_paths[bad]}")
|
||||||
|
check14_ok = False
|
||||||
|
|
||||||
|
if check14_ok:
|
||||||
|
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
||||||
|
|
||||||
|
if r.stopped:
|
||||||
|
r.finalize(out_file)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
ctrl_count = 0
|
ctrl_count = 0
|
||||||
for dp, _dn, files in os.walk(config_dir):
|
for dp, _dn, files in os.walk(config_dir):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-create v1.10 — Create 1C information base
|
# db-create v1.11 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.10 — Create 1C information base
|
# db-create v1.11 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -355,7 +377,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-cf v1.12 — Dump 1C configuration to CF file
|
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.12 — Dump 1C configuration to CF file
|
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -375,7 +397,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-dt v1.11 — Dump 1C information base to DT file
|
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-dt v1.11 — Dump 1C information base to DT file
|
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -373,7 +395,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-xml v1.14 — Dump 1C configuration to XML files
|
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.14 — Dump 1C configuration to XML files
|
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -388,7 +410,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-cf v1.13 — Load 1C configuration from CF file
|
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.13 — Load 1C configuration from CF file
|
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -393,7 +415,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-dt v1.12 — Load 1C information base from DT file
|
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-dt v1.12 — Load 1C information base from DT file
|
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -393,7 +415,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-git v1.18 — Load Git changes into 1C database
|
# db-load-git v1.20 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -110,6 +110,12 @@ param(
|
|||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$UpdateDB,
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||||
|
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||||
|
# но в логе есть отбраковка.
|
||||||
|
[switch]$StrictLog,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -394,6 +400,41 @@ function Write-PlatformOutput {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
|
||||||
|
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
|
||||||
|
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Find-SilentRejections {
|
||||||
|
param([string]$LogText)
|
||||||
|
$patterns = @(
|
||||||
|
'Неверное свойство объекта метаданных',
|
||||||
|
'не входит в состав объекта метаданных',
|
||||||
|
'Неизвестное имя типа',
|
||||||
|
'Неизвестный объект метаданных',
|
||||||
|
'Ни один из документов не является регистратором для регистра',
|
||||||
|
'Неверное значение перечисления',
|
||||||
|
'не может быть приведен к типу',
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
|
||||||
|
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
'Для работы с конфигурацией необходима версия платформы не меньше'
|
||||||
|
)
|
||||||
|
$found = @()
|
||||||
|
if ($LogText) {
|
||||||
|
foreach ($line in ($LogText -split "`r?`n")) {
|
||||||
|
foreach ($pat in $patterns) {
|
||||||
|
if ($line -match [regex]::Escape($pat)) {
|
||||||
|
$found += $line.Trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
|
||||||
|
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
|
||||||
|
return $found
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
@@ -667,6 +708,7 @@ try {
|
|||||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$logContent = $null
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
if ($logContent) {
|
if ($logContent) {
|
||||||
@@ -677,6 +719,16 @@ try {
|
|||||||
}
|
}
|
||||||
Write-PlatformOutput $__v8.Output
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
|
||||||
|
$silentFailures = @(Find-SilentRejections $logContent)
|
||||||
|
if ($silentFailures.Count -gt 0) {
|
||||||
|
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
|
||||||
|
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
||||||
|
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||||
|
}
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.18 — Load Git changes into 1C database
|
# db-load-git v1.20 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -322,6 +344,38 @@ def print_platform_output(result):
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def find_silent_rejections(log_text):
|
||||||
|
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
|
||||||
|
|
||||||
|
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
|
||||||
|
Возвращает подошедшие строки.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
|
||||||
|
автономны). Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет
|
||||||
|
весь смысл.
|
||||||
|
"""
|
||||||
|
patterns = [
|
||||||
|
"Неверное свойство объекта метаданных",
|
||||||
|
"не входит в состав объекта метаданных",
|
||||||
|
"Неизвестное имя типа",
|
||||||
|
"Неизвестный объект метаданных",
|
||||||
|
"Ни один из документов не является регистратором для регистра",
|
||||||
|
"Неверное значение перечисления",
|
||||||
|
"не может быть приведен к типу",
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
|
||||||
|
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
"Для работы с конфигурацией необходима версия платформы не меньше",
|
||||||
|
]
|
||||||
|
found = []
|
||||||
|
if log_text:
|
||||||
|
for line in log_text.splitlines():
|
||||||
|
for pat in patterns:
|
||||||
|
if pat in line:
|
||||||
|
found.append(line.strip())
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -424,13 +478,17 @@ def main():
|
|||||||
)
|
)
|
||||||
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
||||||
|
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||||
|
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||||
|
# но в логе есть отбраковка.
|
||||||
|
parser.add_argument("-StrictLog", action="store_true")
|
||||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
@@ -683,6 +741,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
|
log_content = ""
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -695,6 +754,21 @@ def main():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
|
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
|
||||||
|
silent_failures = find_silent_rejections(log_content)
|
||||||
|
if silent_failures:
|
||||||
|
print(
|
||||||
|
f"[warning] platform reported success, but the log contains "
|
||||||
|
f"{len(silent_failures)} problem(s):"
|
||||||
|
)
|
||||||
|
for line in silent_failures:
|
||||||
|
print(f" {line}")
|
||||||
|
if args.StrictLog and exit_code == 0:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-xml v1.19 — Load 1C configuration from XML files
|
# db-load-xml v1.21 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -416,6 +416,41 @@ function Write-PlatformOutput {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
|
||||||
|
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
|
||||||
|
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Find-SilentRejections {
|
||||||
|
param([string]$LogText)
|
||||||
|
$patterns = @(
|
||||||
|
'Неверное свойство объекта метаданных',
|
||||||
|
'не входит в состав объекта метаданных',
|
||||||
|
'Неизвестное имя типа',
|
||||||
|
'Неизвестный объект метаданных',
|
||||||
|
'Ни один из документов не является регистратором для регистра',
|
||||||
|
'Неверное значение перечисления',
|
||||||
|
'не может быть приведен к типу',
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
|
||||||
|
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
'Для работы с конфигурацией необходима версия платформы не меньше'
|
||||||
|
)
|
||||||
|
$found = @()
|
||||||
|
if ($LogText) {
|
||||||
|
foreach ($line in ($LogText -split "`r?`n")) {
|
||||||
|
foreach ($pat in $patterns) {
|
||||||
|
if ($line -match [regex]::Escape($pat)) {
|
||||||
|
$found += $line.Trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
|
||||||
|
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
|
||||||
|
return $found
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
@@ -607,28 +642,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
# --- Scan log for silent rejections ---
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
$silentFailures = @(Find-SilentRejections $logContent)
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
$fatalLogPatterns = @(
|
|
||||||
'Неверное свойство объекта метаданных',
|
|
||||||
'не входит в состав объекта метаданных',
|
|
||||||
'Неизвестное имя типа',
|
|
||||||
'Неизвестный объект метаданных',
|
|
||||||
'Ни один из документов не является регистратором для регистра',
|
|
||||||
'Неверное значение перечисления',
|
|
||||||
'не может быть приведен к типу'
|
|
||||||
)
|
|
||||||
$silentFailures = @()
|
|
||||||
if ($logContent) {
|
|
||||||
foreach ($line in ($logContent -split "`r?`n")) {
|
|
||||||
foreach ($pat in $fatalLogPatterns) {
|
|
||||||
if ($line -match [regex]::Escape($pat)) {
|
|
||||||
$silentFailures += $line.Trim()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
# Default: mirror platform's verdict via exit code. Log content (including any
|
||||||
@@ -647,10 +661,11 @@ try {
|
|||||||
}
|
}
|
||||||
Write-PlatformOutput $__v8.Output
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
||||||
if ($silentFailures.Count -gt 0) {
|
if ($silentFailures.Count -gt 0) {
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
|
||||||
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
|
|
||||||
Write-Host $msg -ForegroundColor Yellow
|
|
||||||
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
||||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.19 — Load 1C configuration from XML files
|
# db-load-xml v1.21 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -322,6 +344,38 @@ def print_platform_output(result):
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def find_silent_rejections(log_text):
|
||||||
|
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
|
||||||
|
|
||||||
|
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
|
||||||
|
Возвращает подошедшие строки.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
|
||||||
|
автономны). Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет
|
||||||
|
весь смысл.
|
||||||
|
"""
|
||||||
|
patterns = [
|
||||||
|
"Неверное свойство объекта метаданных",
|
||||||
|
"не входит в состав объекта метаданных",
|
||||||
|
"Неизвестное имя типа",
|
||||||
|
"Неизвестный объект метаданных",
|
||||||
|
"Ни один из документов не является регистратором для регистра",
|
||||||
|
"Неверное значение перечисления",
|
||||||
|
"не может быть приведен к типу",
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
|
||||||
|
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
"Для работы с конфигурацией необходима версия платформы не меньше",
|
||||||
|
]
|
||||||
|
found = []
|
||||||
|
if log_text:
|
||||||
|
for line in log_text.splitlines():
|
||||||
|
for pat in patterns:
|
||||||
|
if pat in line:
|
||||||
|
found.append(line.strip())
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -413,7 +467,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
@@ -614,22 +668,7 @@ def main():
|
|||||||
# --- Scan log for silent rejections ---
|
# --- Scan log for silent rejections ---
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
# Platform often writes load-time rejections into /Out but exits with code 0.
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
# These patterns flag cases where metadata was dropped or rejected silently.
|
||||||
fatal_log_patterns = [
|
silent_failures = find_silent_rejections(log_content)
|
||||||
"Неверное свойство объекта метаданных",
|
|
||||||
"не входит в состав объекта метаданных",
|
|
||||||
"Неизвестное имя типа",
|
|
||||||
"Неизвестный объект метаданных",
|
|
||||||
"Ни один из документов не является регистратором для регистра",
|
|
||||||
"Неверное значение перечисления",
|
|
||||||
"не может быть приведен к типу",
|
|
||||||
]
|
|
||||||
silent_failures = []
|
|
||||||
if log_content:
|
|
||||||
for line in log_content.splitlines():
|
|
||||||
for pat in fatal_log_patterns:
|
|
||||||
if pat in line:
|
|
||||||
silent_failures.append(line.strip())
|
|
||||||
break
|
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
# Default: mirror platform's verdict via exit code. Log content (including any
|
||||||
@@ -646,15 +685,19 @@ def main():
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
||||||
|
# Поток — stdout, как у PS1-порта: предупреждение относится к содержимому загрузки, а не к
|
||||||
|
# отказу навыка, и при code 0 остаётся предупреждением. Раньше py писал его в stderr —
|
||||||
|
# наблюдаемое поведение портов расходилось, и один кейс не мог проверить оба.
|
||||||
if silent_failures:
|
if silent_failures:
|
||||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
|
||||||
print(
|
print(
|
||||||
f"[warning] log contains {len(silent_failures)} rejection(s) — "
|
f"[warning] platform reported success, but the log contains "
|
||||||
f"platform loaded config but dropped properties/refs{suffix}",
|
f"{len(silent_failures)} problem(s):"
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
for f in silent_failures:
|
for f in silent_failures:
|
||||||
print(f" {f}", file=sys.stderr)
|
print(f" {f}")
|
||||||
if args.StrictLog and exit_code == 0:
|
if args.StrictLog and exit_code == 0:
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-run v1.7 — Launch 1C:Enterprise
|
# db-run v1.8 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.7 — Launch 1C:Enterprise
|
# db-run v1.8 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,6 +11,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -281,7 +303,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-update v1.13 — Update 1C database configuration
|
# db-update v1.15 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -91,6 +91,12 @@ param(
|
|||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$WarningsAsErrors,
|
[switch]$WarningsAsErrors,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||||
|
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||||
|
# но в логе есть отбраковка.
|
||||||
|
[switch]$StrictLog,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -395,6 +401,41 @@ function Write-PlatformOutput {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
|
||||||
|
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
|
||||||
|
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Find-SilentRejections {
|
||||||
|
param([string]$LogText)
|
||||||
|
$patterns = @(
|
||||||
|
'Неверное свойство объекта метаданных',
|
||||||
|
'не входит в состав объекта метаданных',
|
||||||
|
'Неизвестное имя типа',
|
||||||
|
'Неизвестный объект метаданных',
|
||||||
|
'Ни один из документов не является регистратором для регистра',
|
||||||
|
'Неверное значение перечисления',
|
||||||
|
'не может быть приведен к типу',
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
|
||||||
|
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
'Для работы с конфигурацией необходима версия платформы не меньше'
|
||||||
|
)
|
||||||
|
$found = @()
|
||||||
|
if ($LogText) {
|
||||||
|
foreach ($line in ($LogText -split "`r?`n")) {
|
||||||
|
foreach ($pat in $patterns) {
|
||||||
|
if ($line -match [regex]::Escape($pat)) {
|
||||||
|
$found += $line.Trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
|
||||||
|
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
|
||||||
|
return $found
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
@@ -496,6 +537,7 @@ try {
|
|||||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$logContent = $null
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
if ($logContent) {
|
if ($logContent) {
|
||||||
@@ -506,6 +548,16 @@ try {
|
|||||||
}
|
}
|
||||||
Write-PlatformOutput $__v8.Output
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
|
||||||
|
$silentFailures = @(Find-SilentRejections $logContent)
|
||||||
|
if ($silentFailures.Count -gt 0) {
|
||||||
|
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
|
||||||
|
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
||||||
|
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||||
|
}
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.13 — Update 1C database configuration
|
# db-update v1.15 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -322,6 +344,38 @@ def print_platform_output(result):
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def find_silent_rejections(log_text):
|
||||||
|
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
|
||||||
|
|
||||||
|
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
|
||||||
|
Возвращает подошедшие строки.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
|
||||||
|
автономны). Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет
|
||||||
|
весь смысл.
|
||||||
|
"""
|
||||||
|
patterns = [
|
||||||
|
"Неверное свойство объекта метаданных",
|
||||||
|
"не входит в состав объекта метаданных",
|
||||||
|
"Неизвестное имя типа",
|
||||||
|
"Неизвестный объект метаданных",
|
||||||
|
"Ни один из документов не является регистратором для регистра",
|
||||||
|
"Неверное значение перечисления",
|
||||||
|
"не может быть приведен к типу",
|
||||||
|
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
|
||||||
|
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
|
||||||
|
"Для работы с конфигурацией необходима версия платформы не меньше",
|
||||||
|
]
|
||||||
|
found = []
|
||||||
|
if log_text:
|
||||||
|
for line in log_text.splitlines():
|
||||||
|
for pat in patterns:
|
||||||
|
if pat in line:
|
||||||
|
found.append(line.strip())
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -389,13 +443,17 @@ def main():
|
|||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||||
parser.add_argument("-Server", action="store_true")
|
parser.add_argument("-Server", action="store_true")
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||||
|
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||||
|
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||||
|
# но в логе есть отбраковка.
|
||||||
|
parser.add_argument("-StrictLog", action="store_true")
|
||||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
@@ -505,6 +563,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
|
log_content = ""
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -517,6 +576,21 @@ def main():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
|
|
||||||
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
|
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
|
||||||
|
silent_failures = find_silent_rejections(log_content)
|
||||||
|
if silent_failures:
|
||||||
|
print(
|
||||||
|
f"[warning] platform reported success, but the log contains "
|
||||||
|
f"{len(silent_failures)} problem(s):"
|
||||||
|
)
|
||||||
|
for line in silent_failures:
|
||||||
|
print(f" {line}")
|
||||||
|
if args.StrictLog and exit_code == 0:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -374,7 +396,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -163,14 +163,35 @@ function Format-ArgsForDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Scan XML files for reference types ---
|
# --- 1. Scan XML files for reference types ---
|
||||||
|
|
||||||
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
||||||
|
|
||||||
|
# Версия формата заглушечной конфигурации. Платформа грузит формат не новее себя, поэтому зашитая
|
||||||
|
# версия ломала бы сборку исходников более старого формата на соответствующей ей платформе. Берём
|
||||||
|
# версию из корня собираемого объекта (ExternalDataProcessor/ExternalReport); вложенные файлы —
|
||||||
|
# запасной вариант, если корень почему-то не попался.
|
||||||
|
$srcRootVersion = ""
|
||||||
|
$srcAnyVersion = ""
|
||||||
|
|
||||||
$xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File
|
$xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File
|
||||||
foreach ($f in $xmlFiles) {
|
foreach ($f in $xmlFiles) {
|
||||||
$content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
|
$content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
|
||||||
|
|
||||||
|
if ($content -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') {
|
||||||
|
$ver = $Matches[1]
|
||||||
|
if (-not $srcAnyVersion) { $srcAnyVersion = $ver }
|
||||||
|
if (-not $srcRootVersion -and $content -match '<(ExternalDataProcessor|ExternalReport)[ >]') {
|
||||||
|
$srcRootVersion = $ver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.)
|
# Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.)
|
||||||
$refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)'
|
$refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)'
|
||||||
foreach ($m in [regex]::Matches($content, $refPattern)) {
|
foreach ($m in [regex]::Matches($content, $refPattern)) {
|
||||||
@@ -337,7 +358,24 @@ if ($hasRefTypes) {
|
|||||||
$cfgDir = Join-Path $TempBasePath "cfg"
|
$cfgDir = Join-Path $TempBasePath "cfg"
|
||||||
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
|
||||||
|
|
||||||
$ns = '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"'
|
# Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
|
||||||
|
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
|
||||||
|
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
|
||||||
|
# конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
|
||||||
|
# формата 2.17 загружаемого файла», замерено на 8.3.20).
|
||||||
|
#
|
||||||
|
$srcVersion = if ($srcRootVersion) { $srcRootVersion } elseif ($srcAnyVersion) { $srcAnyVersion } else { "2.17" }
|
||||||
|
$srcRank = Get-FormatRank $srcVersion
|
||||||
|
$stubFormatVersion = if ($srcRank -gt 0 -and $srcRank -lt (Get-FormatRank "2.17")) { $srcVersion } else { "2.17" }
|
||||||
|
# Режим совместимости заглушки — по той же логике. Платформа отказывается работать с
|
||||||
|
# конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
|
||||||
|
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
|
||||||
|
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий
|
||||||
|
# формата из docs/1c-configuration-spec.md.
|
||||||
|
$compatByFormat = @{ "2.13" = "Version8_3_20"; "2.14" = "Version8_3_21"; "2.15" = "Version8_3_22"; "2.16" = "Version8_3_23" }
|
||||||
|
$stubCompatMode = if ($compatByFormat.ContainsKey($stubFormatVersion)) { $compatByFormat[$stubFormatVersion] } else { "Version8_3_24" }
|
||||||
|
|
||||||
|
$ns = '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="' + $stubFormatVersion + '"'
|
||||||
|
|
||||||
# GeneratedType definitions per metadata type
|
# GeneratedType definitions per metadata type
|
||||||
$gtDefs = @{
|
$gtDefs = @{
|
||||||
@@ -521,7 +559,7 @@ if ($hasRefTypes) {
|
|||||||
<Synonym/>
|
<Synonym/>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<NamePrefix/>
|
<NamePrefix/>
|
||||||
<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode>
|
<ConfigurationExtensionCompatibilityMode>$stubCompatMode</ConfigurationExtensionCompatibilityMode>
|
||||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
@@ -572,7 +610,7 @@ if ($hasRefTypes) {
|
|||||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
<CompatibilityMode>Version8_3_24</CompatibilityMode>
|
<CompatibilityMode>$stubCompatMode</CompatibilityMode>
|
||||||
<DefaultConstantsForm/>
|
<DefaultConstantsForm/>
|
||||||
</Properties>
|
</Properties>
|
||||||
<ChildObjects>$childXml
|
<ChildObjects>$childXml
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -339,6 +339,66 @@ def scan_ref_types(source_dir):
|
|||||||
return type_map
|
return type_map
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
def detect_stub_format_version(source_dir):
|
||||||
|
"""Версия формата заглушечной конфигурации.
|
||||||
|
|
||||||
|
Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
|
||||||
|
одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
|
||||||
|
заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
|
||||||
|
конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
|
||||||
|
формата 2.17 загружаемого файла», замерено на 8.3.20).
|
||||||
|
|
||||||
|
Версию исходников берём из корня собираемого объекта (ExternalDataProcessor/ExternalReport);
|
||||||
|
вложенные файлы — запасной вариант, если корень почему-то не попался.
|
||||||
|
"""
|
||||||
|
root_version = ""
|
||||||
|
any_version = ""
|
||||||
|
ver_pattern = re.compile(r'<MetaDataObject[^>]+version="(\d+\.\d+)"')
|
||||||
|
root_pattern = re.compile(r'<(ExternalDataProcessor|ExternalReport)[ >]')
|
||||||
|
for dirpath, _, filenames in os.walk(source_dir):
|
||||||
|
for fn in filenames:
|
||||||
|
if not fn.endswith('.xml'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(os.path.join(dirpath, fn), 'r', encoding='utf-8-sig') as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
m = ver_pattern.search(content)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
if not any_version:
|
||||||
|
any_version = m.group(1)
|
||||||
|
if not root_version and root_pattern.search(content):
|
||||||
|
root_version = m.group(1)
|
||||||
|
src_version = root_version or any_version or "2.17"
|
||||||
|
src_rank = format_rank(src_version)
|
||||||
|
return src_version if 0 < src_rank < format_rank("2.17") else "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
# Режим совместимости заглушки — по той же логике, что и версия формата. Платформа отказывается
|
||||||
|
# работать с конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
|
||||||
|
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
|
||||||
|
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий формата
|
||||||
|
# из docs/1c-configuration-spec.md.
|
||||||
|
COMPAT_BY_FORMAT = {
|
||||||
|
"2.13": "Version8_3_20",
|
||||||
|
"2.14": "Version8_3_21",
|
||||||
|
"2.15": "Version8_3_22",
|
||||||
|
"2.16": "Version8_3_23",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def stub_compatibility_mode(format_version):
|
||||||
|
return COMPAT_BY_FORMAT.get(format_version, "Version8_3_24")
|
||||||
|
|
||||||
|
|
||||||
def scan_register_columns(source_dir):
|
def scan_register_columns(source_dir):
|
||||||
"""Scan Form.xml for register record set columns referenced via DataPath.
|
"""Scan Form.xml for register record set columns referenced via DataPath.
|
||||||
Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}."""
|
Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}."""
|
||||||
@@ -417,7 +477,7 @@ NS = (
|
|||||||
'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
|
'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
|
||||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"'
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
)
|
)
|
||||||
|
|
||||||
CLASS_IDS = [
|
CLASS_IDS = [
|
||||||
@@ -1046,6 +1106,9 @@ def main():
|
|||||||
type_map = scan_ref_types(args.SourceDir)
|
type_map = scan_ref_types(args.SourceDir)
|
||||||
register_columns = scan_register_columns(args.SourceDir)
|
register_columns = scan_register_columns(args.SourceDir)
|
||||||
has_ref_types = len(type_map) > 0
|
has_ref_types = len(type_map) > 0
|
||||||
|
stub_format_version = detect_stub_format_version(args.SourceDir)
|
||||||
|
stub_compat = stub_compatibility_mode(stub_format_version)
|
||||||
|
ns_decl = f'{NS} version="{stub_format_version}"'
|
||||||
|
|
||||||
temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}')
|
temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}')
|
||||||
|
|
||||||
@@ -1077,7 +1140,7 @@ def main():
|
|||||||
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
|
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
|
||||||
|
|
||||||
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject {NS}>
|
<MetaDataObject {ns_decl}>
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>{co_xml}
|
\t\t<InternalInfo>{co_xml}
|
||||||
\t\t</InternalInfo>
|
\t\t</InternalInfo>
|
||||||
@@ -1086,7 +1149,7 @@ def main():
|
|||||||
\t\t\t<Synonym/>
|
\t\t\t<Synonym/>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<NamePrefix/>
|
\t\t\t<NamePrefix/>
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode>
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{stub_compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
\t\t\t<UsePurposes>
|
\t\t\t<UsePurposes>
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
@@ -1137,7 +1200,7 @@ def main():
|
|||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
\t\t\t<CompatibilityMode>Version8_3_24</CompatibilityMode>
|
\t\t\t<CompatibilityMode>{stub_compat}</CompatibilityMode>
|
||||||
\t\t\t<DefaultConstantsForm/>
|
\t\t\t<DefaultConstantsForm/>
|
||||||
\t\t</Properties>
|
\t\t</Properties>
|
||||||
\t\t<ChildObjects>{child_xml}
|
\t\t<ChildObjects>{child_xml}
|
||||||
@@ -1151,7 +1214,7 @@ def main():
|
|||||||
lang_dir = os.path.join(cfg_dir, 'Languages')
|
lang_dir = os.path.join(cfg_dir, 'Languages')
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject {NS}>
|
<MetaDataObject {ns_decl}>
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name>
|
\t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name>
|
||||||
@@ -1280,7 +1343,7 @@ def main():
|
|||||||
child_obj_xml = '\n\t\t<ChildObjects/>'
|
child_obj_xml = '\n\t\t<ChildObjects/>'
|
||||||
|
|
||||||
obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject {NS}>
|
<MetaDataObject {ns_decl}>
|
||||||
\t<{tag} uuid="{obj_uuid}">{internal_xml}
|
\t<{tag} uuid="{obj_uuid}">{internal_xml}
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
{props_xml}
|
{props_xml}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -380,7 +402,7 @@ def main():
|
|||||||
help="Extra ibcmd arguments in --key=value form")
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
args = parser.parse_args(argv)
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
|||||||
@@ -18,19 +18,26 @@ allowed-tools:
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
/epf-init <Name> [Synonym] [SrcDir]
|
/epf-init <Name> [Synonym] [SrcDir] [FormatVersion]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|-----------|:------------:|--------------|-------------------------------------|
|
|---------------|:------------:|--------------|------------------------------------------------|
|
||||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
| Name | да | — | Имя обработки (латиница/кириллица) |
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||||
|
| FormatVersion | нет | `2.17` | Версия формата выгрузки — см. ниже |
|
||||||
|
|
||||||
|
`FormatVersion` — **не выше** версии формата платформы, на которой объект будут собирать и открывать:
|
||||||
|
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
|
||||||
|
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
|
||||||
|
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
|
||||||
|
но на них навыки не проверялись — такое значение принимается с предупреждением.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.8 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -6,21 +6,65 @@ param(
|
|||||||
|
|
||||||
[string]$Synonym = $Name,
|
[string]$Synonym = $Name,
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
[string]$SrcDir = "src",
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
|
||||||
|
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
|
||||||
|
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
|
||||||
|
# на нечисловое значение: это опечатка, а не версия.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$formatRank = Get-FormatRank $FormatVersion
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
|
||||||
|
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
|
||||||
|
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
|
||||||
|
if ($formatRank -eq 0) {
|
||||||
|
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
|
||||||
|
}
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
$uuid3 = [guid]::NewGuid().ToString()
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
$uuid4 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
|
||||||
|
# платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if ($formatRank -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
$xml = @"
|
$xml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<ExternalDataProcessor uuid="$uuid1">
|
<ExternalDataProcessor uuid="$uuid1">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -33,11 +77,11 @@ $xml = @"
|
|||||||
</xr:GeneratedType>
|
</xr:GeneratedType>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$Name</Name>
|
<Name>$(Esc-XmlText $Name)</Name>
|
||||||
<Synonym>
|
<Synonym>
|
||||||
<v8:item>
|
<v8:item>
|
||||||
<v8:lang>ru</v8:lang>
|
<v8:lang>ru</v8:lang>
|
||||||
<v8:content>$Synonym</v8:content>
|
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
|
||||||
</v8:item>
|
</v8:item>
|
||||||
</Synonym>
|
</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
@@ -64,7 +108,16 @@ $extDir = Join-Path $processorDir "Ext"
|
|||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
|
|
||||||
@@ -83,6 +136,11 @@ $moduleBsl = @"
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||||
|
|
||||||
Write-Host "[OK] Создана обработка: $rootFile"
|
Write-Host "[OK] Создана обработка: $rootFile"
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.8 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external data processor."""
|
"""Generates minimal XML source files for a 1C external data processor."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -21,7 +67,24 @@ def main():
|
|||||||
parser.add_argument('-Name', dest='Name', required=True)
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
||||||
args = parser.parse_args()
|
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
|
||||||
|
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
|
||||||
|
format_rank_value = format_rank(args.FormatVersion)
|
||||||
|
if format_rank_value == 0:
|
||||||
|
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
|
||||||
|
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
|
||||||
|
f"but was not verified on that platform", file=sys.stderr)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -32,8 +95,36 @@ def main():
|
|||||||
uuid3 = new_uuid()
|
uuid3 = new_uuid()
|
||||||
uuid4 = new_uuid()
|
uuid4 = new_uuid()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
format_version = args.FormatVersion
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
|
||||||
|
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<ExternalDataProcessor uuid="{uuid1}">
|
\t<ExternalDataProcessor uuid="{uuid1}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
\t\t\t<xr:ContainedObject>
|
\t\t\t<xr:ContainedObject>
|
||||||
@@ -46,11 +137,11 @@ def main():
|
|||||||
\t\t\t</xr:GeneratedType>
|
\t\t\t</xr:GeneratedType>
|
||||||
\t\t</InternalInfo>
|
\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>
|
\t\t\t<Synonym>
|
||||||
\t\t\t\t<v8:item>
|
\t\t\t\t<v8:item>
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
|
||||||
\t\t\t\t</v8:item>
|
\t\t\t\t</v8:item>
|
||||||
\t\t\t</Synonym>
|
\t\t\t</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
@@ -72,7 +163,7 @@ def main():
|
|||||||
ext_dir = os.path.join(processor_dir, "Ext")
|
ext_dir = os.path.join(processor_dir, "Ext")
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
module_bsl = """\
|
module_bsl = """\
|
||||||
@@ -89,7 +180,10 @@ def main():
|
|||||||
#КонецОбласти"""
|
#КонецОбласти"""
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
||||||
write_utf8_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
print(f"[OK] Создана обработка: {root_file}")
|
print(f"[OK] Создана обработка: {root_file}")
|
||||||
print(f" Каталог: {processor_dir}")
|
print(f" Каталог: {processor_dir}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-validate v1.3 — Validate 1C external data processor / report structure
|
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
param(
|
param(
|
||||||
@@ -111,6 +111,19 @@ $finalize = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
|
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||||
@@ -183,11 +196,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
|
$versionRank = Get-FormatRank $version
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
} elseif ($versionRank -eq 0) {
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
Report-Error "1. Malformed version '$version' (expected N.N)"
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type: ExternalDataProcessor or ExternalReport
|
# Detect type: ExternalDataProcessor or ExternalReport
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.3 — Validate 1C external data processor / report structure
|
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
|
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
@@ -38,6 +60,21 @@ CHILD_TYPE_ORDER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
@@ -50,7 +87,7 @@ def main():
|
|||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
parser.add_argument("-OutFile", default=None)
|
parser.add_argument("-OutFile", default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
|
|
||||||
@@ -163,11 +200,17 @@ def main():
|
|||||||
check1_ok = False
|
check1_ok = False
|
||||||
|
|
||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
report_warn("1. Missing version attribute on MetaDataObject")
|
report_warn("1. Missing version attribute on MetaDataObject")
|
||||||
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
|
elif version_rank == 0:
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
report_error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
report_warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
report_warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
child_elements = []
|
child_elements = []
|
||||||
|
|||||||
@@ -18,20 +18,27 @@ allowed-tools:
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
/erf-init <Name> [Synonym] [SrcDir] [FormatVersion] [--with-skd]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|-----------|:------------:|--------------|---------------------------------------|
|
|---------------|:------------:|--------------|---------------------------------------|
|
||||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
| FormatVersion | нет | `2.17` | Версия формата выгрузки — см. ниже |
|
||||||
|
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
||||||
|
|
||||||
|
`FormatVersion` — **не выше** версии формата платформы, на которой объект будут собирать и открывать:
|
||||||
|
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
|
||||||
|
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
|
||||||
|
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
|
||||||
|
но на них навыки не проверялись — такое значение принимается с предупреждением.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.8 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -8,18 +8,62 @@ param(
|
|||||||
|
|
||||||
[string]$SrcDir = "src",
|
[string]$SrcDir = "src",
|
||||||
|
|
||||||
[switch]$WithSKD
|
[switch]$WithSKD,
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
|
||||||
|
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
|
||||||
|
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
|
||||||
|
# на нечисловое значение: это опечатка, а не версия.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$formatRank = Get-FormatRank $FormatVersion
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
|
||||||
|
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
|
||||||
|
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
|
||||||
|
if ($formatRank -eq 0) {
|
||||||
|
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
|
||||||
|
}
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
$uuid3 = [guid]::NewGuid().ToString()
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
$uuid4 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
|
||||||
|
# платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if ($formatRank -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- Формируем Properties ---
|
# --- Формируем Properties ---
|
||||||
|
|
||||||
$mainDCSValue = ""
|
$mainDCSValue = ""
|
||||||
@@ -48,7 +92,7 @@ $childObjectsXml = if ($childObjectsContent) {
|
|||||||
|
|
||||||
$xml = @"
|
$xml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<ExternalReport uuid="$uuid1">
|
<ExternalReport uuid="$uuid1">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -61,11 +105,11 @@ $xml = @"
|
|||||||
</xr:GeneratedType>
|
</xr:GeneratedType>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$Name</Name>
|
<Name>$(Esc-XmlText $Name)</Name>
|
||||||
<Synonym>
|
<Synonym>
|
||||||
<v8:item>
|
<v8:item>
|
||||||
<v8:lang>ru</v8:lang>
|
<v8:lang>ru</v8:lang>
|
||||||
<v8:content>$Synonym</v8:content>
|
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
|
||||||
</v8:item>
|
</v8:item>
|
||||||
</Synonym>
|
</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
@@ -98,7 +142,16 @@ $extDir = Join-Path $reportDir "Ext"
|
|||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
|
|
||||||
@@ -117,6 +170,11 @@ $moduleBsl = @"
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||||
|
|
||||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
Write-Host "[OK] Создан отчёт: $rootFile"
|
||||||
@@ -136,7 +194,7 @@ if ($WithSKD) {
|
|||||||
|
|
||||||
$skdMetaXml = @"
|
$skdMetaXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<Template uuid="$skdUuid">
|
<Template uuid="$skdUuid">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$skdName</Name>
|
<Name>$skdName</Name>
|
||||||
@@ -153,7 +211,7 @@ if ($WithSKD) {
|
|||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
Write-XmlFile $skdMetaPath $skdMetaXml $enc
|
||||||
|
|
||||||
$skdContent = @"
|
$skdContent = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -173,7 +231,7 @@ if ($WithSKD) {
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
||||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
Write-XmlFile $skdFilePath $skdContent $enc
|
||||||
|
|
||||||
Write-Host " СКД: $skdMetaPath"
|
Write-Host " СКД: $skdMetaPath"
|
||||||
Write-Host " Тело: $skdFilePath"
|
Write-Host " Тело: $skdFilePath"
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.8 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external report."""
|
"""Generates minimal XML source files for a 1C external report."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -21,8 +67,25 @@ def main():
|
|||||||
parser.add_argument('-Name', dest='Name', required=True)
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
|
||||||
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
|
||||||
|
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
|
||||||
|
format_rank_value = format_rank(args.FormatVersion)
|
||||||
|
if format_rank_value == 0:
|
||||||
|
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
|
||||||
|
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
|
||||||
|
f"but was not verified on that platform", file=sys.stderr)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -33,6 +96,34 @@ def main():
|
|||||||
uuid3 = new_uuid()
|
uuid3 = new_uuid()
|
||||||
uuid4 = new_uuid()
|
uuid4 = new_uuid()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
format_version = args.FormatVersion
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
|
||||||
|
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
|
||||||
# --- Properties ---
|
# --- Properties ---
|
||||||
main_dcs_value = ""
|
main_dcs_value = ""
|
||||||
child_objects_content = ""
|
child_objects_content = ""
|
||||||
@@ -45,7 +136,7 @@ def main():
|
|||||||
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<ExternalReport uuid="{uuid1}">
|
\t<ExternalReport uuid="{uuid1}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
\t\t\t<xr:ContainedObject>
|
\t\t\t<xr:ContainedObject>
|
||||||
@@ -58,11 +149,11 @@ def main():
|
|||||||
\t\t\t</xr:GeneratedType>
|
\t\t\t</xr:GeneratedType>
|
||||||
\t\t</InternalInfo>
|
\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>
|
\t\t\t<Synonym>
|
||||||
\t\t\t\t<v8:item>
|
\t\t\t\t<v8:item>
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
|
||||||
\t\t\t\t</v8:item>
|
\t\t\t\t</v8:item>
|
||||||
\t\t\t</Synonym>
|
\t\t\t</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
@@ -90,7 +181,7 @@ def main():
|
|||||||
ext_dir = os.path.join(report_dir, "Ext")
|
ext_dir = os.path.join(report_dir, "Ext")
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
module_bsl = """\
|
module_bsl = """\
|
||||||
@@ -107,7 +198,10 @@ def main():
|
|||||||
#КонецОбласти"""
|
#КонецОбласти"""
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
||||||
write_utf8_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
print(f"[OK] Создан отчёт: {root_file}")
|
print(f"[OK] Создан отчёт: {root_file}")
|
||||||
print(f" Каталог: {report_dir}")
|
print(f" Каталог: {report_dir}")
|
||||||
@@ -124,7 +218,7 @@ def main():
|
|||||||
skd_uuid = new_uuid()
|
skd_uuid = new_uuid()
|
||||||
|
|
||||||
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Template uuid="{skd_uuid}">
|
\t<Template uuid="{skd_uuid}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{skd_name}</Name>
|
\t\t\t<Name>{skd_name}</Name>
|
||||||
@@ -140,7 +234,7 @@ def main():
|
|||||||
\t</Template>
|
\t</Template>
|
||||||
</MetaDataObject>'''
|
</MetaDataObject>'''
|
||||||
|
|
||||||
write_utf8_bom(skd_meta_path, skd_meta_xml)
|
write_xml_file(skd_meta_path, skd_meta_xml)
|
||||||
|
|
||||||
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||||
@@ -158,7 +252,7 @@ def main():
|
|||||||
</DataCompositionSchema>'''
|
</DataCompositionSchema>'''
|
||||||
|
|
||||||
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
||||||
write_utf8_bom(skd_file_path, skd_content)
|
write_xml_file(skd_file_path, skd_content)
|
||||||
|
|
||||||
print(f" СКД: {skd_meta_path}")
|
print(f" СКД: {skd_meta_path}")
|
||||||
print(f" Тело: {skd_file_path}")
|
print(f" Тело: {skd_file_path}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.12 — Add managed form to 1C config object
|
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -154,6 +154,14 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
@@ -169,6 +177,13 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "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
|
||||||
|
}
|
||||||
|
|
||||||
# --- Фаза 1: Определение типа объекта ---
|
# --- Фаза 1: Определение типа объекта ---
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
# Resolve ObjectPath (directory → .xml)
|
||||||
@@ -190,7 +205,26 @@ if (-not (Test-Path $ObjectPath)) {
|
|||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
$objectXmlFull = Resolve-Path $ObjectPath
|
||||||
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
||||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
|
||||||
|
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
|
||||||
|
$script:formatVersion = $null
|
||||||
|
$objHead = [System.IO.File]::ReadAllText($objectXmlFull.Path, [System.Text.Encoding]::UTF8)
|
||||||
|
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
|
||||||
|
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $script:formatVersion = $Matches[1] }
|
||||||
|
if (-not $script:formatVersion) { $script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) }
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
|
||||||
|
# интерполируют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
$xmlDoc.PreserveWhitespace = $true
|
||||||
@@ -313,9 +347,16 @@ if ($objectType -in $processorLikeTypes) {
|
|||||||
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
|
||||||
|
$useInIfcLine = ""
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$useInIfcLine = "`n`t`t`t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>"
|
||||||
|
}
|
||||||
|
|
||||||
$formMetaXml = @"
|
$formMetaXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
<MetaDataObject $($script:xmlnsDecl) version="$($script:formatVersion)">
|
||||||
<Form uuid="$formUuid">
|
<Form uuid="$formUuid">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$FormName</Name>
|
<Name>$FormName</Name>
|
||||||
@@ -331,20 +372,29 @@ $formMetaXml = @"
|
|||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
||||||
</UsePurposes>$extPresentationLine
|
</UsePurposes>$useInIfcLine$extPresentationLine
|
||||||
</Properties>
|
</Properties>
|
||||||
</Form>
|
</Form>
|
||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
#
|
||||||
|
# Модуль .bsl сюда НЕ идёт — он пишется отдельно.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $formMetaPath $formMetaXml $encBom
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
# --- 3b. Form.xml ---
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||||
|
|
||||||
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
|
|
||||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
||||||
# Динамический список
|
# Динамический список
|
||||||
# MainTable: тип.имя
|
# MainTable: тип.имя
|
||||||
@@ -352,7 +402,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -377,7 +427,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -424,7 +474,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -444,7 +494,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
if (Test-Path $formXmlPath) {
|
if (Test-Path $formXmlPath) {
|
||||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
||||||
} else {
|
} else {
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
Write-XmlFile $formXmlPath $formXml $encBom
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
# --- 3c. Module.bsl ---
|
||||||
@@ -476,6 +526,11 @@ $moduleBsl = @"
|
|||||||
if (Test-Path $modulePath) {
|
if (Test-Path $modulePath) {
|
||||||
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
||||||
} else {
|
} else {
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,12 +640,27 @@ if ($SetDefault -or $isFirstFormForPurpose) {
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objectXmlFull.Path) -and ([System.IO.File]::ReadAllText($objectXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($objectXmlFull.Path, $xmlText, $encBom)
|
||||||
|
|
||||||
# --- Фаза 5: Вывод ---
|
# --- Фаза 5: Вывод ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.12 — Add managed form to 1C config object
|
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,6 +11,28 @@ import uuid
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -196,6 +218,16 @@ NSMAP = {
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -210,6 +242,12 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def _detect_xml_style(path):
|
def _detect_xml_style(path):
|
||||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
@@ -227,21 +265,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -257,10 +296,24 @@ def save_xml_with_bom(tree, path):
|
|||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
def write_utf8_bom(path, content):
|
||||||
"""Write text to file with UTF-8 BOM."""
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
f.write(text)
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
|
||||||
|
Модуль .bsl сюда НЕ идёт — он пишется отдельно.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -272,7 +325,7 @@ def main():
|
|||||||
parser.add_argument("-Synonym", default=None)
|
parser.add_argument("-Synonym", default=None)
|
||||||
parser.add_argument("-Purpose", default="Object")
|
parser.add_argument("-Purpose", default="Object")
|
||||||
parser.add_argument("-SetDefault", action="store_true")
|
parser.add_argument("-SetDefault", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
form_name = args.FormName
|
form_name = args.FormName
|
||||||
@@ -299,7 +352,64 @@ def main():
|
|||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
object_xml_full = os.path.abspath(object_path)
|
||||||
assert_edit_allowed(object_xml_full, "editable")
|
assert_edit_allowed(object_xml_full, "editable")
|
||||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
|
||||||
|
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
|
||||||
|
format_version = None
|
||||||
|
with open(object_xml_full, "r", encoding="utf-8-sig") as f:
|
||||||
|
obj_head = f.read(2000)
|
||||||
|
m_ver = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', obj_head)
|
||||||
|
if m_ver:
|
||||||
|
format_version = m_ver.group(1)
|
||||||
|
if not format_version:
|
||||||
|
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
|
||||||
|
# подставляют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
form_ns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
||||||
|
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
pal = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
xmlns_decl = xmlns_decl.replace(' xmlns:style=', pal)
|
||||||
|
form_ns_decl = form_ns_decl.replace(' xmlns:style=', pal)
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(object_xml_full, parser_xml)
|
tree = etree.parse(object_xml_full, parser_xml)
|
||||||
@@ -388,24 +498,7 @@ def main():
|
|||||||
|
|
||||||
form_meta_xml = (
|
form_meta_xml = (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
f' version="{format_version}">\n'
|
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
f'\t<Form uuid="{form_uuid}">\n'
|
||||||
'\t\t<Properties>\n'
|
'\t\t<Properties>\n'
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||||
@@ -422,37 +515,22 @@ def main():
|
|||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
||||||
'\t\t\t</UsePurposes>\n'
|
'\t\t\t</UsePurposes>\n'
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
|
||||||
|
+ ('\t\t\t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>\n'
|
||||||
|
if format_rank(format_version) >= 221 else '')
|
||||||
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
||||||
+ '\t\t</Properties>\n'
|
+ '\t\t</Properties>\n'
|
||||||
'\t</Form>\n'
|
'\t</Form>\n'
|
||||||
'</MetaDataObject>'
|
'</MetaDataObject>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
write_xml_file(form_meta_path, form_meta_xml)
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
# --- 3b. Form.xml ---
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||||
|
|
||||||
form_ns_decl = (
|
|
||||||
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
||||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if purpose in ("List", "Choice"):
|
if purpose in ("List", "Choice"):
|
||||||
# Dynamic list
|
# Dynamic list
|
||||||
main_table = f"{object_type}.{object_name}"
|
main_table = f"{object_type}.{object_name}"
|
||||||
@@ -551,7 +629,7 @@ def main():
|
|||||||
if os.path.exists(form_xml_path):
|
if os.path.exists(form_xml_path):
|
||||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
||||||
else:
|
else:
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
write_xml_file(form_xml_path, form_xml)
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
# --- 3c. Module.bsl ---
|
||||||
|
|
||||||
@@ -582,7 +660,10 @@ def main():
|
|||||||
if os.path.exists(module_path):
|
if os.path.exists(module_path):
|
||||||
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
||||||
else:
|
else:
|
||||||
write_text_with_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
# --- Phase 4: Register in parent object ---
|
# --- Phase 4: Register in parent object ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$JsonPath,
|
[string]$JsonPath,
|
||||||
@@ -1335,6 +1335,14 @@ function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
@@ -1350,6 +1358,13 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "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
|
||||||
|
}
|
||||||
|
|
||||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
@@ -1485,6 +1500,17 @@ $script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $Ou
|
|||||||
Assert-EditAllowed $script:outPathResolved 'editable'
|
Assert-EditAllowed $script:outPathResolved 'editable'
|
||||||
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
|
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: место эмиссии её только интерполирует.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- 0. Path normalization and mode dispatch ---
|
# --- 0. Path normalization and mode dispatch ---
|
||||||
|
|
||||||
# Form name → purpose mapping
|
# Form name → purpose mapping
|
||||||
@@ -1679,6 +1705,12 @@ function X {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Esc-Xml {
|
function Esc-Xml {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
|
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
|
||||||
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
|
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
|
||||||
# " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
|
# " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
|
||||||
@@ -1693,14 +1725,14 @@ function Emit-MLItems {
|
|||||||
param($val, [string]$indent)
|
param($val, [string]$indent)
|
||||||
if ($val -is [System.Collections.IDictionary]) {
|
if ($val -is [System.Collections.IDictionary]) {
|
||||||
foreach ($k in $val.Keys) {
|
foreach ($k in $val.Keys) {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
|
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
|
||||||
foreach ($p in $val.PSObject.Properties) {
|
foreach ($p in $val.PSObject.Properties) {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1719,7 +1751,7 @@ function Emit-USPresentation {
|
|||||||
param($val, [string]$tag, [string]$indent)
|
param($val, [string]$tag, [string]$indent)
|
||||||
if ($null -eq $val) { return }
|
if ($null -eq $val) { return }
|
||||||
if ($val -is [string]) {
|
if ($val -is [string]) {
|
||||||
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>"
|
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-XmlText $val)</$tag>"
|
||||||
} else {
|
} else {
|
||||||
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
|
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
|
||||||
}
|
}
|
||||||
@@ -1848,10 +1880,10 @@ function Emit-FilterItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
||||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($item.userSettingID) {
|
if ($item.userSettingID) {
|
||||||
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $guid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
@@ -1859,10 +1891,10 @@ function Emit-FilterItem {
|
|||||||
}
|
}
|
||||||
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
|
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
|
||||||
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
|
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
|
||||||
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>"
|
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-XmlText "$($item.field)")</dcsset:left>"
|
||||||
$compType = $script:comparisonTypes["$($item.op)"]
|
$compType = $script:comparisonTypes["$($item.op)"]
|
||||||
if (-not $compType) { $compType = "$($item.op)" }
|
if (-not $compType) { $compType = "$($item.op)" }
|
||||||
X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>"
|
X "$indent`t<dcsset:comparisonType>$(Esc-XmlText $compType)</dcsset:comparisonType>"
|
||||||
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
|
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
|
||||||
if ($valIsArray) {
|
if ($valIsArray) {
|
||||||
if (@($item.value).Count -eq 0) {
|
if (@($item.value).Count -eq 0) {
|
||||||
@@ -1881,7 +1913,7 @@ function Emit-FilterItem {
|
|||||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
|
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
|
||||||
else { $vt = 'xs:string' }
|
else { $vt = 'xs:string' }
|
||||||
}
|
}
|
||||||
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" }
|
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-XmlText "$v" }
|
||||||
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
|
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
|
||||||
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||||
}
|
}
|
||||||
@@ -1907,8 +1939,8 @@ function Emit-FilterItem {
|
|||||||
$variant = "$sv"; $hasDate = $false; $dateV = $null
|
$variant = "$sv"; $hasDate = $false; $dateV = $null
|
||||||
}
|
}
|
||||||
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
|
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
|
||||||
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>"
|
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-XmlText $variant)</v8:variant>"
|
||||||
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" }
|
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-XmlText $dateV)</v8:date>" }
|
||||||
X "$indent`t</dcsset:right>"
|
X "$indent`t</dcsset:right>"
|
||||||
} elseif ("$($item.value)" -eq '_') {
|
} elseif ("$($item.value)" -eq '_') {
|
||||||
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
|
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
|
||||||
@@ -1926,15 +1958,15 @@ function Emit-FilterItem {
|
|||||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
|
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
|
||||||
else { $vt = "xs:string" }
|
else { $vt = "xs:string" }
|
||||||
}
|
}
|
||||||
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
|
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-XmlText "$($item.value)" }
|
||||||
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
|
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
|
||||||
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||||
}
|
}
|
||||||
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
||||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($item.userSettingID) {
|
if ($item.userSettingID) {
|
||||||
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
@@ -1958,10 +1990,10 @@ function Emit-Filter {
|
|||||||
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
|
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
|
||||||
} else { Emit-FilterItem -item $item -indent "$indent`t" }
|
} else { Emit-FilterItem -item $item -indent "$indent`t" }
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:filter>"
|
X "$indent</dcsset:filter>"
|
||||||
@@ -1983,7 +2015,7 @@ function Emit-Order {
|
|||||||
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
|
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
|
||||||
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||||
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
X "$indent`t`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
|
||||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
@@ -1993,16 +2025,16 @@ function Emit-Order {
|
|||||||
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||||
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
||||||
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
|
X "$indent`t`t<dcsset:field>$(Esc-XmlText "$($item.field)")</dcsset:field>"
|
||||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||||
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:order>"
|
X "$indent</dcsset:order>"
|
||||||
@@ -2034,7 +2066,7 @@ function Emit-AppearanceValue {
|
|||||||
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
||||||
}
|
}
|
||||||
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
|
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
|
X "$indent`t<dcscor:parameter>$(Esc-XmlText $key)</dcscor:parameter>"
|
||||||
$isFontDict = $false
|
$isFontDict = $false
|
||||||
if ($innerVal -is [PSCustomObject]) {
|
if ($innerVal -is [PSCustomObject]) {
|
||||||
$tProp = $innerVal.PSObject.Properties['@type']
|
$tProp = $innerVal.PSObject.Properties['@type']
|
||||||
@@ -2050,7 +2082,7 @@ function Emit-AppearanceValue {
|
|||||||
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
|
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
|
||||||
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
|
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
|
||||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
|
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
|
||||||
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>"
|
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-XmlText $ls)</v8ui:style>"
|
||||||
X "$indent`t</dcscor:value>"
|
X "$indent`t</dcscor:value>"
|
||||||
} elseif ($isFontDict) {
|
} elseif ($isFontDict) {
|
||||||
$attrParts = @()
|
$attrParts = @()
|
||||||
@@ -2063,7 +2095,7 @@ function Emit-AppearanceValue {
|
|||||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
|
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
|
||||||
} elseif ($isDict -and (_HasKey $innerVal 'field')) {
|
} elseif ($isDict -and (_HasKey $innerVal 'field')) {
|
||||||
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
|
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
|
||||||
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>"
|
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-XmlText "$(_Get $innerVal 'field')")</dcscor:value>"
|
||||||
} elseif ($isDict) {
|
} elseif ($isDict) {
|
||||||
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
|
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
|
||||||
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
|
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
|
||||||
@@ -2078,19 +2110,19 @@ function Emit-AppearanceValue {
|
|||||||
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
|
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
|
||||||
}
|
}
|
||||||
$keyType = $keyTypeMap[$key]
|
$keyType = $keyTypeMap[$key]
|
||||||
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
|
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
|
||||||
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
|
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
|
||||||
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
|
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
|
||||||
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
|
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
|
||||||
# Пустая строка → самозакрывающийся тег (как у платформы).
|
# Пустая строка → самозакрывающийся тег (как у платформы).
|
||||||
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
|
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
|
||||||
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
|
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
|
||||||
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
if ($nestedItems) {
|
if ($nestedItems) {
|
||||||
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
|
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
|
||||||
@@ -2113,7 +2145,7 @@ function Emit-ConditionalAppearance {
|
|||||||
X "$indent`t`t<dcsset:selection>"
|
X "$indent`t`t<dcsset:selection>"
|
||||||
foreach ($sel in $ca.selection) {
|
foreach ($sel in $ca.selection) {
|
||||||
X "$indent`t`t`t<dcsset:item>"
|
X "$indent`t`t`t<dcsset:item>"
|
||||||
X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>"
|
X "$indent`t`t`t`t<dcsset:field>$(Esc-XmlText "$sel")</dcsset:field>"
|
||||||
X "$indent`t`t`t</dcsset:item>"
|
X "$indent`t`t`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
X "$indent`t`t</dcsset:selection>"
|
X "$indent`t`t</dcsset:selection>"
|
||||||
@@ -2132,12 +2164,12 @@ function Emit-ConditionalAppearance {
|
|||||||
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
|
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
|
||||||
X "$indent`t`t</dcsset:presentation>"
|
X "$indent`t`t</dcsset:presentation>"
|
||||||
}
|
}
|
||||||
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" }
|
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-XmlText "$($ca.presentation)")</dcsset:presentation>" }
|
||||||
}
|
}
|
||||||
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" }
|
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($ca.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($ca.userSettingID) {
|
if ($ca.userSettingID) {
|
||||||
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
|
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
|
||||||
X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
||||||
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
|
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
|
||||||
@@ -2153,10 +2185,10 @@ function Emit-ConditionalAppearance {
|
|||||||
}
|
}
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</$wrapTag>"
|
X "$indent</$wrapTag>"
|
||||||
@@ -2195,14 +2227,14 @@ function Emit-GroupItemField {
|
|||||||
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
|
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
|
||||||
}
|
}
|
||||||
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
|
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
|
||||||
X "$indent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
X "$indent`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
|
||||||
X "$indent`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>"
|
X "$indent`t<dcsset:groupType>$(Esc-XmlText $gt)</dcsset:groupType>"
|
||||||
X "$indent`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>"
|
X "$indent`t<dcsset:periodAdditionType>$(Esc-XmlText $pat)</dcsset:periodAdditionType>"
|
||||||
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
|
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
|
||||||
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
||||||
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
||||||
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>"
|
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-XmlText $pab)</dcsset:periodAdditionBegin>"
|
||||||
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>"
|
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-XmlText $pae)</dcsset:periodAdditionEnd>"
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2271,22 +2303,22 @@ function Emit-CalcFields {
|
|||||||
}
|
}
|
||||||
$ci = "$indent`t"
|
$ci = "$indent`t"
|
||||||
X "$indent<CalculatedField>"
|
X "$indent<CalculatedField>"
|
||||||
X "$ci<dcssch:dataPath>$(Esc-Xml $dataPath)</dcssch:dataPath>"
|
X "$ci<dcssch:dataPath>$(Esc-XmlText $dataPath)</dcssch:dataPath>"
|
||||||
X "$ci<dcssch:expression>$(Esc-Xml $expression)</dcssch:expression>"
|
X "$ci<dcssch:expression>$(Esc-XmlText $expression)</dcssch:expression>"
|
||||||
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
|
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
|
||||||
if ($restrict.Count -gt 0) {
|
if ($restrict.Count -gt 0) {
|
||||||
X "$ci<dcssch:useRestriction>"
|
X "$ci<dcssch:useRestriction>"
|
||||||
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
|
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
|
||||||
X "$ci</dcssch:useRestriction>"
|
X "$ci</dcssch:useRestriction>"
|
||||||
}
|
}
|
||||||
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-Xml "$pres")</dcssch:presentationExpression>" }
|
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-XmlText "$pres")</dcssch:presentationExpression>" }
|
||||||
if ($orderExpr) {
|
if ($orderExpr) {
|
||||||
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
|
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
|
||||||
foreach ($oe in $oeList) {
|
foreach ($oe in $oeList) {
|
||||||
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
|
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
|
||||||
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
|
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
|
||||||
X "$ci<dcssch:orderExpression>"
|
X "$ci<dcssch:orderExpression>"
|
||||||
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-Xml $exprV)</expression>"
|
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-XmlText $exprV)</expression>"
|
||||||
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
|
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
|
||||||
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
|
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
|
||||||
X "$ci</dcssch:orderExpression>"
|
X "$ci</dcssch:orderExpression>"
|
||||||
@@ -2371,26 +2403,49 @@ $script:knownInvalidTypes = @{
|
|||||||
"FormTable" = "UI element type, not a data type"
|
"FormTable" = "UI element type, not a data type"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
$script:typeSynonyms = $script:formTypeSynonyms
|
||||||
|
|
||||||
function Resolve-TypeStr {
|
function Resolve-TypeStr {
|
||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
|
|
||||||
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$base = $Matches[1].Trim(); $params = $Matches[2]
|
$baseName = $Matches[1].Trim()
|
||||||
$r = $script:formTypeSynonyms[$base.ToLower()]
|
$params = $Matches[2]
|
||||||
if ($r) { return "$r($params)" }
|
$resolved = $script:typeSynonyms[$baseName.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved($params)" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$i = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
$r = $script:formTypeSynonyms[$prefix.ToLower()]
|
$suffix = $typeStr.Substring($dotIdx) # includes the dot
|
||||||
if ($r) { return "$r$suffix" }
|
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved$suffix" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
|
|
||||||
if ($r) { return $r }
|
# Простое имя
|
||||||
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
|
if ($resolved) { return $resolved }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3210,7 +3265,7 @@ function Emit-CommonElementProps {
|
|||||||
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
|
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
|
||||||
}
|
}
|
||||||
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
|
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
|
||||||
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-Xml "$($el.headerDataPath)")</HeaderDataPath>" }
|
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-XmlText "$($el.headerDataPath)")</HeaderDataPath>" }
|
||||||
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
|
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
|
||||||
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
|
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
|
||||||
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
|
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
|
||||||
@@ -3230,8 +3285,8 @@ function Emit-PictureRef {
|
|||||||
if (-not $src) { return }
|
if (-not $src) { return }
|
||||||
$srcStr = "$src"
|
$srcStr = "$src"
|
||||||
X "$indent<$picTag>"
|
X "$indent<$picTag>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
|
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
|
||||||
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
||||||
X "$indent</$picTag>"
|
X "$indent</$picTag>"
|
||||||
@@ -3260,8 +3315,8 @@ function Emit-CommandPicture {
|
|||||||
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
|
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
|
||||||
$srcStr = "$src"
|
$srcStr = "$src"
|
||||||
X "$indent<Picture>"
|
X "$indent<Picture>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
|
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
|
||||||
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
||||||
X "$indent</Picture>"
|
X "$indent</Picture>"
|
||||||
@@ -3423,7 +3478,7 @@ function Emit-GenericScalars {
|
|||||||
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
|
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
|
||||||
} else {
|
} else {
|
||||||
$v = "$($p.Value)"; if ($v -eq '') { continue }
|
$v = "$($p.Value)"; if ($v -eq '') { continue }
|
||||||
X "$indent<$($s.Tag)>$(Esc-Xml $v)</$($s.Tag)>"
|
X "$indent<$($s.Tag)>$(Esc-XmlText $v)</$($s.Tag)>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3470,7 +3525,7 @@ function Emit-BorderTag {
|
|||||||
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
|
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
|
||||||
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
|
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
|
||||||
X "$indent<Border width=`"$width`">"
|
X "$indent<Border width=`"$width`">"
|
||||||
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml $style)</v8ui:style>" }
|
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText $style)</v8ui:style>" }
|
||||||
X "$indent</Border>"
|
X "$indent</Border>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3497,13 +3552,13 @@ function PL-Bool {
|
|||||||
}
|
}
|
||||||
function Emit-PlannerColor {
|
function Emit-PlannerColor {
|
||||||
param([string]$tag, $o, [string]$key, [string]$ind)
|
param([string]$tag, $o, [string]$key, [string]$ind)
|
||||||
X "$ind<pl:$tag>$(Esc-Xml "$(PL-Get $o $key 'auto')")</pl:$tag>"
|
X "$ind<pl:$tag>$(Esc-XmlText "$(PL-Get $o $key 'auto')")</pl:$tag>"
|
||||||
}
|
}
|
||||||
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
|
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
|
||||||
function Emit-PlannerText {
|
function Emit-PlannerText {
|
||||||
param([string]$tag, $v, [string]$ind)
|
param([string]$tag, $v, [string]$ind)
|
||||||
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
|
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
|
||||||
else { X "$ind<pl:$tag>$(Esc-Xml "$v")</pl:$tag>" }
|
else { X "$ind<pl:$tag>$(Esc-XmlText "$v")</pl:$tag>" }
|
||||||
}
|
}
|
||||||
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
|
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
|
||||||
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
|
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
|
||||||
@@ -3518,7 +3573,7 @@ function Emit-PlannerValue {
|
|||||||
param($v, [string]$ind)
|
param($v, [string]$ind)
|
||||||
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
|
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
|
||||||
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
|
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
|
||||||
X "$ind<pl:value xsi:type=`"$t`">$(Esc-Xml "$v")</pl:value>"
|
X "$ind<pl:value xsi:type=`"$t`">$(Esc-XmlText "$v")</pl:value>"
|
||||||
}
|
}
|
||||||
function Emit-PlannerFont {
|
function Emit-PlannerFont {
|
||||||
param($o, [string]$ind)
|
param($o, [string]$ind)
|
||||||
@@ -3532,14 +3587,14 @@ function Emit-PlannerBorder {
|
|||||||
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
|
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
|
||||||
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
|
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
|
||||||
X "$ind<pl:border width=`"$bw`">"
|
X "$ind<pl:border width=`"$bw`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$bs")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$bs")</v8ui:style>"
|
||||||
X "$ind</pl:border>"
|
X "$ind</pl:border>"
|
||||||
}
|
}
|
||||||
function Emit-PlannerLevel {
|
function Emit-PlannerLevel {
|
||||||
param($lv, [string]$cns, [string]$ind)
|
param($lv, [string]$cns, [string]$ind)
|
||||||
$li = "$ind`t"
|
$li = "$ind`t"
|
||||||
X "$ind<level xmlns=`"$cns`">"
|
X "$ind<level xmlns=`"$cns`">"
|
||||||
X "$li<measure>$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")</measure>"
|
X "$li<measure>$(Esc-XmlText "$(PL-Get $lv 'measure' 'Hour')")</measure>"
|
||||||
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
|
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
|
||||||
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
|
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
|
||||||
$line = PL-Get $lv 'line' $null
|
$line = PL-Get $lv 'line' $null
|
||||||
@@ -3547,10 +3602,10 @@ function Emit-PlannerLevel {
|
|||||||
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
|
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
|
||||||
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
|
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
|
||||||
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
|
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
|
||||||
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$lst")</v8ui:style>"
|
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$lst")</v8ui:style>"
|
||||||
X "$li</line>"
|
X "$li</line>"
|
||||||
X "$li<scaleColor>$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
|
X "$li<scaleColor>$(Esc-XmlText "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
|
||||||
X "$li<dayFormatRule>$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
|
X "$li<dayFormatRule>$(Esc-XmlText "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
|
||||||
$fmt = PL-Get $lv 'format' $null
|
$fmt = PL-Get $lv 'format' $null
|
||||||
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
|
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
|
||||||
X "$li<format>"
|
X "$li<format>"
|
||||||
@@ -3561,8 +3616,8 @@ function Emit-PlannerLevel {
|
|||||||
X "$li<labels>"
|
X "$li<labels>"
|
||||||
X "$li`t<ticks>$ticks</ticks>"
|
X "$li`t<ticks>$ticks</ticks>"
|
||||||
X "$li</labels>"
|
X "$li</labels>"
|
||||||
X "$li<backColor>$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
|
X "$li<backColor>$(Esc-XmlText "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
|
||||||
X "$li<textColor>$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
|
X "$li<textColor>$(Esc-XmlText "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
|
||||||
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
|
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
|
||||||
X "$ind</level>"
|
X "$ind</level>"
|
||||||
}
|
}
|
||||||
@@ -3571,14 +3626,14 @@ function Emit-PlannerTimeScale {
|
|||||||
$cns = $script:CHART_NS
|
$cns = $script:CHART_NS
|
||||||
$ci = "$ind`t"
|
$ci = "$ind`t"
|
||||||
X "$ind<pl:timeScale>"
|
X "$ind<pl:timeScale>"
|
||||||
X "$ci<placement xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
|
X "$ci<placement xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
|
||||||
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
|
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
|
||||||
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
|
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
|
||||||
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
|
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
|
||||||
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
|
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
|
||||||
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
|
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
|
||||||
X "$ci<backColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
|
X "$ci<backColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
|
||||||
X "$ci<textColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
|
X "$ci<textColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
|
||||||
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
|
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
|
||||||
X "$ind</pl:timeScale>"
|
X "$ind</pl:timeScale>"
|
||||||
}
|
}
|
||||||
@@ -3603,7 +3658,7 @@ function Emit-PlannerItem {
|
|||||||
X "$ii<pl:id>$id</pl:id>"
|
X "$ii<pl:id>$id</pl:id>"
|
||||||
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
|
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
|
||||||
Emit-PlannerBorder $it $ii 'border'
|
Emit-PlannerBorder $it $ii 'border'
|
||||||
X "$ii<pl:editMode>$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
|
X "$ii<pl:editMode>$(Esc-XmlText "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
|
||||||
X "$ind</pl:item>"
|
X "$ind</pl:item>"
|
||||||
}
|
}
|
||||||
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
|
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
|
||||||
@@ -3658,7 +3713,7 @@ function Emit-PlannerSettings {
|
|||||||
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
|
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
|
||||||
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
|
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
|
||||||
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
|
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
|
||||||
X "$si<pl:periodicVariantUnit>$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
|
X "$si<pl:periodicVariantUnit>$(Esc-XmlText "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
|
||||||
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
|
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
|
||||||
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
|
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
|
||||||
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
|
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
|
||||||
@@ -3671,16 +3726,16 @@ function Emit-PlannerSettings {
|
|||||||
X "$si</pl:period>"
|
X "$si</pl:period>"
|
||||||
}
|
}
|
||||||
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
|
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
|
||||||
X "$si<pl:itemsTimeRepresentation>$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
|
X "$si<pl:itemsTimeRepresentation>$(Esc-XmlText "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
|
||||||
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
|
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-XmlText "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
|
||||||
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
|
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
|
||||||
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
|
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
|
||||||
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
|
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
|
||||||
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
|
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
|
||||||
X "$si<pl:fixDimensionsHeader>$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
|
X "$si<pl:fixDimensionsHeader>$(Esc-XmlText "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
|
||||||
X "$si<pl:fixTimeScaleHeader>$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
|
X "$si<pl:fixTimeScaleHeader>$(Esc-XmlText "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
|
||||||
Emit-PlannerBorder $pl $si 'border'
|
Emit-PlannerBorder $pl $si 'border'
|
||||||
X "$si<pl:newItemsTextType>$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
|
X "$si<pl:newItemsTextType>$(Esc-XmlText "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
|
||||||
X "$ind</Settings>"
|
X "$ind</Settings>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3713,13 +3768,13 @@ function Emit-ChartNode {
|
|||||||
if ($keys -contains 'gap') {
|
if ($keys -contains 'gap') {
|
||||||
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
|
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
|
||||||
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
|
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$st")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$st")</v8ui:style>"
|
||||||
X "$ind</d4p1:$name>"; return
|
X "$ind</d4p1:$name>"; return
|
||||||
}
|
}
|
||||||
if (($keys -contains 'style') -and ($keys -contains 'width')) {
|
if (($keys -contains 'style') -and ($keys -contains 'width')) {
|
||||||
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
|
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
|
||||||
X "$ind<d4p1:$name width=`"$w`">"
|
X "$ind<d4p1:$name width=`"$w`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$st")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$st")</v8ui:style>"
|
||||||
X "$ind</d4p1:$name>"; return
|
X "$ind</d4p1:$name>"; return
|
||||||
}
|
}
|
||||||
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
|
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
|
||||||
@@ -3735,7 +3790,7 @@ function Emit-ChartNode {
|
|||||||
}
|
}
|
||||||
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
|
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
|
||||||
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
|
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
|
||||||
X "$ind<d4p1:$name>$(Esc-Xml "$val")</d4p1:$name>"
|
X "$ind<d4p1:$name>$(Esc-XmlText "$val")</d4p1:$name>"
|
||||||
}
|
}
|
||||||
function Emit-ChartSettings {
|
function Emit-ChartSettings {
|
||||||
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
|
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
|
||||||
@@ -3757,7 +3812,7 @@ function Emit-Appearance {
|
|||||||
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
|
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
|
||||||
$spec = $script:appearanceSpec[$key]
|
$spec = $script:appearanceSpec[$key]
|
||||||
switch ($spec.kind) {
|
switch ($spec.kind) {
|
||||||
'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")</$($spec.tag)>" }
|
'color' { X "$indent<$($spec.tag)>$(Esc-XmlText "$val")</$($spec.tag)>" }
|
||||||
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
|
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
|
||||||
'border' { Emit-BorderTag -val $val -indent $indent }
|
'border' { Emit-BorderTag -val $val -indent $indent }
|
||||||
}
|
}
|
||||||
@@ -4042,13 +4097,13 @@ function Emit-Input {
|
|||||||
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
|
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
|
||||||
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
|
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
|
||||||
)) {
|
)) {
|
||||||
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")</$($p[1])>" }
|
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>" }
|
||||||
}
|
}
|
||||||
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
|
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
|
||||||
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
|
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
|
||||||
if ($null -ne $el.($p[0])) {
|
if ($null -ne $el.($p[0])) {
|
||||||
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
|
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
|
||||||
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")</$($p[1])>"
|
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
|
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
|
||||||
@@ -4111,7 +4166,7 @@ function Emit-Check {
|
|||||||
|
|
||||||
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
|
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
|
||||||
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
||||||
|
|
||||||
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
|
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
|
||||||
@@ -4268,7 +4323,7 @@ function Emit-ChoicePresentation {
|
|||||||
foreach ($pair in $pairs) {
|
foreach ($pair in $pairs) {
|
||||||
X "$indent`t<v8:item>"
|
X "$indent`t<v8:item>"
|
||||||
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
|
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
|
||||||
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
|
X "$indent`t`t<v8:content>$(Esc-XmlText $pair[1])</v8:content>"
|
||||||
X "$indent`t</v8:item>"
|
X "$indent`t</v8:item>"
|
||||||
}
|
}
|
||||||
X "$indent</Presentation>"
|
X "$indent</Presentation>"
|
||||||
@@ -4278,7 +4333,7 @@ function Emit-ChoicePresentation {
|
|||||||
function Get-ChoiceValueTag {
|
function Get-ChoiceValueTag {
|
||||||
param($norm)
|
param($norm)
|
||||||
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
|
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
|
||||||
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
|
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-XmlText $norm.Text)</Value>"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
|
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
|
||||||
@@ -4491,8 +4546,8 @@ function Emit-ChoiceParameterLinks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
X "$indent`t<xr:Link>"
|
X "$indent`t<xr:Link>"
|
||||||
X "$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>"
|
X "$indent`t`t<xr:Name>$(Esc-XmlText "$name")</xr:Name>"
|
||||||
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>"
|
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-XmlText "$dp")</xr:DataPath>"
|
||||||
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
|
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
|
||||||
X "$indent`t</xr:Link>"
|
X "$indent`t</xr:Link>"
|
||||||
}
|
}
|
||||||
@@ -4509,7 +4564,7 @@ function Emit-TypeLink {
|
|||||||
$li = Get-ElProp $tl @('linkItem','элементСвязи')
|
$li = Get-ElProp $tl @('linkItem','элементСвязи')
|
||||||
if ($null -eq $li) { $li = 0 }
|
if ($null -eq $li) { $li = 0 }
|
||||||
X "$indent<TypeLink>"
|
X "$indent<TypeLink>"
|
||||||
X "$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>"
|
X "$indent`t<xr:DataPath>$(Esc-XmlText "$dp")</xr:DataPath>"
|
||||||
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
|
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
|
||||||
X "$indent</TypeLink>"
|
X "$indent</TypeLink>"
|
||||||
}
|
}
|
||||||
@@ -4616,7 +4671,7 @@ function Emit-LabelField {
|
|||||||
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
|
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
|
||||||
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
|
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
|
||||||
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
|
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
|
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
|
||||||
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
|
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
|
||||||
Emit-ColumnPics -el $el -indent $inner
|
Emit-ColumnPics -el $el -indent $inner
|
||||||
@@ -4939,7 +4994,7 @@ function Emit-Button {
|
|||||||
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
|
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
|
||||||
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
|
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
|
||||||
} else {
|
} else {
|
||||||
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$btnParam")</Parameter>"
|
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText "$btnParam")</Parameter>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
|
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
|
||||||
@@ -4994,8 +5049,8 @@ function Emit-PictureDecoration {
|
|||||||
$srcStr = "$($el.src)"
|
$srcStr = "$($el.src)"
|
||||||
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
|
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
|
||||||
X "$inner<Picture>"
|
X "$inner<Picture>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$inner`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$inner`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
|
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
|
||||||
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
|
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
|
||||||
X "$inner</Picture>"
|
X "$inner</Picture>"
|
||||||
@@ -5039,7 +5094,7 @@ function Emit-PictureField {
|
|||||||
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
|
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
|
||||||
|
|
||||||
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
||||||
|
|
||||||
# ValuesPicture — picture (collection) used to render the field's value.
|
# ValuesPicture — picture (collection) used to render the field's value.
|
||||||
@@ -5395,18 +5450,18 @@ function Emit-DLValue {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
|
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
|
||||||
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
|
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
|
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-XmlText $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
|
||||||
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
else {
|
else {
|
||||||
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
|
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
|
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5440,7 +5495,7 @@ function Emit-DLInputParameters {
|
|||||||
foreach ($item in $items) {
|
foreach ($item in $items) {
|
||||||
X "$indent`t<dcscor:item>"
|
X "$indent`t<dcscor:item>"
|
||||||
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>"
|
X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($item.parameter)")</dcscor:parameter>"
|
||||||
if (Has-DLProp $item 'choiceParameters') {
|
if (Has-DLProp $item 'choiceParameters') {
|
||||||
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
|
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
|
||||||
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
|
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
|
||||||
@@ -5448,11 +5503,11 @@ function Emit-DLInputParameters {
|
|||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
|
||||||
foreach ($cpItem in $cpItems) {
|
foreach ($cpItem in $cpItems) {
|
||||||
X "$indent`t`t`t<dcscor:item>"
|
X "$indent`t`t`t<dcscor:item>"
|
||||||
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>"
|
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cpItem.name)")</dcscor:choiceParameter>"
|
||||||
foreach ($v in @($cpItem.values)) {
|
foreach ($v in @($cpItem.values)) {
|
||||||
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
|
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
|
||||||
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
|
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
|
||||||
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" }
|
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$v")</dcscor:value>" }
|
||||||
}
|
}
|
||||||
X "$indent`t`t`t</dcscor:item>"
|
X "$indent`t`t`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
@@ -5465,8 +5520,8 @@ function Emit-DLInputParameters {
|
|||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
|
||||||
foreach ($cplItem in $cplItems) {
|
foreach ($cplItem in $cplItems) {
|
||||||
X "$indent`t`t`t<dcscor:item>"
|
X "$indent`t`t`t<dcscor:item>"
|
||||||
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>"
|
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cplItem.name)")</dcscor:choiceParameter>"
|
||||||
X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>"
|
X "$indent`t`t`t`t<dcscor:value>$(Esc-XmlText "$($cplItem.value)")</dcscor:value>"
|
||||||
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
|
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
|
||||||
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
|
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
|
||||||
X "$indent`t`t`t</dcscor:item>"
|
X "$indent`t`t`t</dcscor:item>"
|
||||||
@@ -5477,15 +5532,15 @@ function Emit-DLInputParameters {
|
|||||||
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
|
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
|
||||||
$tl = $item.typeLink
|
$tl = $item.typeLink
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
|
||||||
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" }
|
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-XmlText "$tlf")</dcscor:field>" }
|
||||||
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" }
|
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-XmlText "$tli")</dcscor:linkItem>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
} elseif (Has-DLProp $item 'value') {
|
} elseif (Has-DLProp $item 'value') {
|
||||||
$val = $item.value
|
$val = $item.value
|
||||||
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
|
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
|
||||||
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
|
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
|
||||||
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
|
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
|
||||||
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" }
|
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$val")</dcscor:value>" }
|
||||||
}
|
}
|
||||||
X "$indent`t</dcscor:item>"
|
X "$indent`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
@@ -5560,16 +5615,16 @@ function Emit-DataParameters {
|
|||||||
}
|
}
|
||||||
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
|
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
|
||||||
if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>"
|
X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($dp.parameter)")</dcscor:parameter>"
|
||||||
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
|
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
|
||||||
if ($dpValIsArr) {
|
if ($dpValIsArr) {
|
||||||
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
|
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
|
||||||
$avtype = "$($dp.valueType)"
|
$avtype = "$($dp.valueType)"
|
||||||
foreach ($v in @($dp.value)) {
|
foreach ($v in @($dp.value)) {
|
||||||
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
|
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
|
||||||
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" }
|
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" }
|
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" }
|
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
} elseif ($dp.nilValue -eq $true) {
|
} elseif ($dp.nilValue -eq $true) {
|
||||||
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
|
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
|
||||||
@@ -5592,41 +5647,41 @@ function Emit-DataParameters {
|
|||||||
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
|
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
|
||||||
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
|
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
|
||||||
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
|
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
|
||||||
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" }
|
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-XmlText $_d)</v8:date>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
} else {
|
} else {
|
||||||
$_sd = $null; $_ed = $null
|
$_sd = $null; $_ed = $null
|
||||||
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
|
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
|
||||||
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
|
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
||||||
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
|
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
|
||||||
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" }
|
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-XmlText $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-XmlText $_ed)</v8:endDate>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
}
|
}
|
||||||
} elseif ($vtype -match '^[a-zA-Z]+:') {
|
} elseif ($vtype -match '^[a-zA-Z]+:') {
|
||||||
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
|
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-XmlText $vStr)</dcscor:value>"
|
||||||
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
|
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-XmlText ("$($dp.value)".ToLower()))</dcscor:value>"
|
||||||
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ($vtype -match '^decimal') {
|
} elseif ($vtype -match '^decimal') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ($vtype -match '^string') {
|
} elseif ($vtype -match '^string') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
|
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} else {
|
} else {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" }
|
if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($dp.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" }
|
if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>" }
|
||||||
if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
||||||
X "$indent`t</dcscor:item>"
|
X "$indent`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
X "$indent</dcsset:dataParameters>"
|
X "$indent</dcsset:dataParameters>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5634,7 +5689,7 @@ function Emit-DLParameter {
|
|||||||
param($p, $parsed, [string]$indent)
|
param($p, $parsed, [string]$indent)
|
||||||
X "$indent<Parameter>"
|
X "$indent<Parameter>"
|
||||||
$ci = "$indent`t"
|
$ci = "$indent`t"
|
||||||
X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>"
|
X "$ci<dcssch:name>$(Esc-XmlText $parsed.name)</dcssch:name>"
|
||||||
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
|
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
|
||||||
$title = $null
|
$title = $null
|
||||||
if ($parsed.title) { $title = $parsed.title }
|
if ($parsed.title) { $title = $parsed.title }
|
||||||
@@ -5668,7 +5723,7 @@ function Emit-DLParameter {
|
|||||||
# expression
|
# expression
|
||||||
$expr = $null
|
$expr = $null
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
|
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
|
||||||
if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" }
|
if ($expr) { X "$ci<dcssch:expression>$(Esc-XmlText $expr)</dcssch:expression>" }
|
||||||
# availableValues
|
# availableValues
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
|
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
|
||||||
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
|
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
|
||||||
@@ -5687,7 +5742,7 @@ function Emit-DLParameter {
|
|||||||
# use
|
# use
|
||||||
$useVal = $null
|
$useVal = $null
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
|
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
|
||||||
if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" }
|
if ($useVal) { X "$ci<dcssch:use>$(Esc-XmlText $useVal)</dcssch:use>" }
|
||||||
X "$indent</Parameter>"
|
X "$indent</Parameter>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5807,7 +5862,7 @@ function Emit-Attributes {
|
|||||||
}
|
}
|
||||||
if ($saveFields.Count -gt 0) {
|
if ($saveFields.Count -gt 0) {
|
||||||
X "$inner<Save>"
|
X "$inner<Save>"
|
||||||
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" }
|
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-XmlText $f)</Field>" }
|
||||||
X "$inner</Save>"
|
X "$inner</Save>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5871,9 +5926,17 @@ function Emit-Attributes {
|
|||||||
}
|
}
|
||||||
if ($hasAddCols) {
|
if ($hasAddCols) {
|
||||||
foreach ($ac in @($attr.additionalColumns)) {
|
foreach ($ac in @($attr.additionalColumns)) {
|
||||||
|
# Пустой список колонок задаётся ЯВНО (`"columns": []`) — это законная форма,
|
||||||
|
# платформа так пишет таблицу, у которой доп. колонок нет. А вот отсутствие ключа
|
||||||
|
# — недосказанность автора: «доп. колонки есть», а какие, не указано. Раньше на
|
||||||
|
# этом PS падал с «Не удается индексировать в массив NULL» (@($null).Count = 1).
|
||||||
|
if ($null -eq $ac.PSObject.Properties['columns'] -or $null -eq $ac.columns) {
|
||||||
|
Write-Error "additionalColumns group for table '$($ac.table)': key 'columns' is missing — list the columns, or pass an empty array for a table without extra columns"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
$acCols = @($ac.columns)
|
$acCols = @($ac.columns)
|
||||||
if ($acCols.Count -eq 0) {
|
if ($acCols.Count -eq 0) {
|
||||||
# Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа)
|
# Явно пустая группа → self-closing (как платформа)
|
||||||
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
|
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -5908,7 +5971,7 @@ function Emit-Attributes {
|
|||||||
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
|
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
|
||||||
if ($hasQuery) {
|
if ($hasQuery) {
|
||||||
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
|
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
|
||||||
X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>"
|
X "$si<QueryText>$(Esc-XmlText $qtext)</QueryText>"
|
||||||
}
|
}
|
||||||
# Явные поля набора (редко): override title/dataPath
|
# Явные поля набора (редко): override title/dataPath
|
||||||
if ($st.fields) {
|
if ($st.fields) {
|
||||||
@@ -5923,8 +5986,8 @@ function Emit-Attributes {
|
|||||||
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
|
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
|
||||||
elseif ($isFolder) { $dp = "" }
|
elseif ($isFolder) { $dp = "" }
|
||||||
else { $dp = "$($fld.field)" }
|
else { $dp = "$($fld.field)" }
|
||||||
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" }
|
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-XmlText "$dp")</dcssch:dataPath>" }
|
||||||
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" }
|
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-XmlText "$($fld.field)")</dcssch:field>" }
|
||||||
if ($fld.title) {
|
if ($fld.title) {
|
||||||
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
|
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
|
||||||
Emit-MLItems -val $fld.title -indent "$si`t`t"
|
Emit-MLItems -val $fld.title -indent "$si`t`t"
|
||||||
@@ -5934,7 +5997,7 @@ function Emit-Attributes {
|
|||||||
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
|
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
|
||||||
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
|
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
|
||||||
# presentationExpression поля — перед valueType (порядок исходника)
|
# presentationExpression поля — перед valueType (порядок исходника)
|
||||||
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
|
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-XmlText "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
|
||||||
# valueType поля набора (тип значения; вычисляемые/кастомные поля)
|
# valueType поля набора (тип значения; вычисляемые/кастомные поля)
|
||||||
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
|
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
|
||||||
# appearance поля (формат/оформление) — после valueType (порядок исходника)
|
# appearance поля (формат/оформление) — после valueType (порядок исходника)
|
||||||
@@ -5954,8 +6017,8 @@ function Emit-Attributes {
|
|||||||
Emit-DLParameters -params $st.parameters -indent $si
|
Emit-DLParameters -params $st.parameters -indent $si
|
||||||
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
|
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
|
||||||
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
|
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
|
||||||
if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" }
|
if ($st.keyType) { X "$si<KeyType>$(Esc-XmlText "$($st.keyType)")</KeyType>" }
|
||||||
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } }
|
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-XmlText "$kf")</KeyField>" } }
|
||||||
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
|
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
|
||||||
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
|
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
|
||||||
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
|
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
|
||||||
@@ -6092,7 +6155,7 @@ function Emit-Commands {
|
|||||||
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
|
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
|
||||||
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
|
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
|
||||||
if ($cmdTable) {
|
if ($cmdTable) {
|
||||||
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>"
|
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-XmlText "$cmdTable")</AssociatedTableElementId>"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cmd.shortcut) {
|
if ($cmd.shortcut) {
|
||||||
@@ -6184,10 +6247,10 @@ function Emit-CommandInterface {
|
|||||||
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
|
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
|
||||||
if ($treeGroup) { $grp = $treeGroup }
|
if ($treeGroup) { $grp = $treeGroup }
|
||||||
X "$inner`t<Item>"
|
X "$inner`t<Item>"
|
||||||
X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>"
|
X "$inner`t`t<Command>$(Esc-XmlText "$cmd")</Command>"
|
||||||
X "$inner`t`t<Type>$type</Type>"
|
X "$inner`t`t<Type>$type</Type>"
|
||||||
if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" }
|
if ($attr) { X "$inner`t`t<Attribute>$(Esc-XmlText "$attr")</Attribute>" }
|
||||||
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" }
|
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-XmlText "$grp")</CommandGroup>" }
|
||||||
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
|
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
|
||||||
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
|
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
|
||||||
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
|
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
|
||||||
@@ -6463,25 +6526,14 @@ function Compute-MainAcbAutofill {
|
|||||||
|
|
||||||
# --- 12. Main compilation ---
|
# --- 12. Main compilation ---
|
||||||
|
|
||||||
# Title
|
# Буфер и счётчики — с чистого листа: до этой точки они могли быть тронуты режимом from-object.
|
||||||
if ($def.title) {
|
|
||||||
Emit-MLText -tag "Title" -text $def.title -indent "`t"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Header
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
|
|
||||||
|
|
||||||
# Oops — Title was emitted before header. Need to fix the order.
|
|
||||||
# Actually, let me restructure: build the body into a separate buffer, then assemble
|
|
||||||
|
|
||||||
# Reset and rebuild properly
|
|
||||||
$script:xml = New-Object System.Text.StringBuilder 8192
|
$script:xml = New-Object System.Text.StringBuilder 8192
|
||||||
$script:nextId = 1
|
$script:nextId = 1
|
||||||
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
|
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
|
||||||
|
|
||||||
|
# Header
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
|
X "<Form $($script:formNsDecl) version=`"$($script:formatVersion)`">"
|
||||||
|
|
||||||
# 12a. Title (from def.title or properties.title — must be multilingual XML)
|
# 12a. Title (from def.title or properties.title — must be multilingual XML)
|
||||||
$formTitle = $def.title
|
$formTitle = $def.title
|
||||||
@@ -6535,7 +6587,7 @@ if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).
|
|||||||
X "`t`t`t<xr:CheckState>0</xr:CheckState>"
|
X "`t`t`t<xr:CheckState>0</xr:CheckState>"
|
||||||
# пустое значение → самозакрывающийся тег (зеркало платформы)
|
# пустое значение → самозакрывающийся тег (зеркало платформы)
|
||||||
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
|
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
|
||||||
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" }
|
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-XmlText "$nm")</xr:Value>" }
|
||||||
X "`t`t</xr:Item>"
|
X "`t`t</xr:Item>"
|
||||||
}
|
}
|
||||||
X "`t</MobileDeviceCommandBarContent>"
|
X "`t</MobileDeviceCommandBarContent>"
|
||||||
@@ -6622,7 +6674,7 @@ if (-not (Test-Path $outDir)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc)
|
[System.IO.File]::WriteAllText($outPath, $xml.ToString().TrimEnd("`r", "`n"), $enc)
|
||||||
|
|
||||||
# --- 13b. Auto-register form in parent object XML ---
|
# --- 13b. Auto-register form in parent object XML ---
|
||||||
|
|
||||||
@@ -6677,11 +6729,26 @@ if ($formsLeaf -eq 'Forms') {
|
|||||||
$regSettings = New-Object System.Xml.XmlWriterSettings
|
$regSettings = New-Object System.Xml.XmlWriterSettings
|
||||||
$regSettings.Encoding = $regEnc
|
$regSettings.Encoding = $regEnc
|
||||||
$regSettings.Indent = $false
|
$regSettings.Indent = $false
|
||||||
$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create)
|
$regSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
|
$regMem = New-Object System.IO.MemoryStream
|
||||||
|
$regWriter = [System.Xml.XmlWriter]::Create($regMem, $regSettings)
|
||||||
$objDoc.Save($regWriter)
|
$objDoc.Save($regWriter)
|
||||||
$regWriter.Close()
|
$regWriter.Flush(); $regWriter.Close()
|
||||||
$regStream.Close()
|
|
||||||
|
$regText = [System.Text.Encoding]::UTF8.GetString($regMem.ToArray())
|
||||||
|
$regMem.Close()
|
||||||
|
if ($regText.Length -gt 0 -and $regText[0] -eq [char]0xFEFF) { $regText = $regText.Substring(1) }
|
||||||
|
$regText = $regText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$regText = [regex]::Replace($regText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objectXmlPath) -and ([System.IO.File]::ReadAllText($objectXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$regText = ($regText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($objectXmlPath, $regText, $regEnc)
|
||||||
|
|
||||||
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
|
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||||
param(
|
param(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||||
#
|
#
|
||||||
@@ -13,6 +13,28 @@ import xml.etree.ElementTree as ET
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- 1. Namespaces ---
|
# --- 1. Namespaces ---
|
||||||
NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
|
NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
|
||||||
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||||
@@ -81,29 +103,29 @@ def _attr(node, name, ns_uri=None):
|
|||||||
def convert_string_to_json_literal(s):
|
def convert_string_to_json_literal(s):
|
||||||
if s is None:
|
if s is None:
|
||||||
return 'null'
|
return 'null'
|
||||||
sb = ['"']
|
out = ['"']
|
||||||
for ch in s:
|
for ch in s:
|
||||||
code = ord(ch)
|
code = ord(ch)
|
||||||
if code == 0x22:
|
if code == 0x22:
|
||||||
sb.append('\\"')
|
out.append('\\"')
|
||||||
elif code == 0x5C:
|
elif code == 0x5C:
|
||||||
sb.append('\\\\')
|
out.append('\\\\')
|
||||||
elif code == 0x08:
|
elif code == 0x08:
|
||||||
sb.append('\\b')
|
out.append('\\b')
|
||||||
elif code == 0x09:
|
elif code == 0x09:
|
||||||
sb.append('\\t')
|
out.append('\\t')
|
||||||
elif code == 0x0A:
|
elif code == 0x0A:
|
||||||
sb.append('\\n')
|
out.append('\\n')
|
||||||
elif code == 0x0C:
|
elif code == 0x0C:
|
||||||
sb.append('\\f')
|
out.append('\\f')
|
||||||
elif code == 0x0D:
|
elif code == 0x0D:
|
||||||
sb.append('\\r')
|
out.append('\\r')
|
||||||
elif code < 0x20:
|
elif code < 0x20:
|
||||||
sb.append('\\u%04x' % code)
|
out.append('\\u%04x' % code)
|
||||||
else:
|
else:
|
||||||
sb.append(ch)
|
out.append(ch)
|
||||||
sb.append('"')
|
out.append('"')
|
||||||
return ''.join(sb)
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
def _num_to_str(obj):
|
def _num_to_str(obj):
|
||||||
@@ -3115,7 +3137,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
|
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
|
||||||
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
|
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
|
||||||
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
output_path = args.OutputPath
|
output_path = args.OutputPath
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.6 — Edit 1C managed form elements
|
# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -270,6 +270,12 @@ function X {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Esc-Xml {
|
function Esc-Xml {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
|
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
|
||||||
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной "). " платформа
|
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной "). " платформа
|
||||||
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
|
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
|
||||||
@@ -282,7 +288,7 @@ function Emit-MLText {
|
|||||||
X "$indent<$tag>"
|
X "$indent<$tag>"
|
||||||
X "$indent`t<v8:item>"
|
X "$indent`t<v8:item>"
|
||||||
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
||||||
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
|
X "$indent`t`t<v8:content>$(Esc-XmlText $text)</v8:content>"
|
||||||
X "$indent`t</v8:item>"
|
X "$indent`t</v8:item>"
|
||||||
X "$indent</$tag>"
|
X "$indent</$tag>"
|
||||||
}
|
}
|
||||||
@@ -311,24 +317,49 @@ $script:formTypeSynonyms["бизнеспроцессссылка"] = "
|
|||||||
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
|
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
|
||||||
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
|
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
$script:typeSynonyms = $script:formTypeSynonyms
|
||||||
|
|
||||||
function Resolve-TypeStr {
|
function Resolve-TypeStr {
|
||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
|
|
||||||
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$base = $Matches[1].Trim(); $params = $Matches[2]
|
$baseName = $Matches[1].Trim()
|
||||||
$r = $script:formTypeSynonyms[$base.ToLower()]
|
$params = $Matches[2]
|
||||||
if ($r) { return "$r($params)" }
|
$resolved = $script:typeSynonyms[$baseName.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved($params)" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$i = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
$r = $script:formTypeSynonyms[$prefix.ToLower()]
|
$suffix = $typeStr.Substring($dotIdx) # includes the dot
|
||||||
if ($r) { return "$r$suffix" }
|
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved$suffix" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
|
|
||||||
if ($r) { return $r }
|
# Простое имя
|
||||||
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
|
if ($resolved) { return $resolved }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +623,7 @@ function Emit-Label {
|
|||||||
X "$inner<Title formatted=`"$formatted`">"
|
X "$inner<Title formatted=`"$formatted`">"
|
||||||
X "$inner`t<v8:item>"
|
X "$inner`t<v8:item>"
|
||||||
X "$inner`t`t<v8:lang>ru</v8:lang>"
|
X "$inner`t`t<v8:lang>ru</v8:lang>"
|
||||||
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>"
|
X "$inner`t`t<v8:content>$(Esc-XmlText "$($el.title)")</v8:content>"
|
||||||
X "$inner`t</v8:item>"
|
X "$inner`t</v8:item>"
|
||||||
X "$inner</Title>"
|
X "$inner</Title>"
|
||||||
}
|
}
|
||||||
@@ -1387,8 +1418,16 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
|
|||||||
$content = $xmlDoc.OuterXml
|
$content = $xmlDoc.OuterXml
|
||||||
# Ensure encoding declaration is uppercase UTF-8
|
# Ensure encoding declaration is uppercase UTF-8
|
||||||
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
|
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$content = [regex]::Replace($content, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedFormPath) -and ([System.IO.File]::ReadAllText($resolvedFormPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$content = ($content -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
|
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
|
||||||
|
|
||||||
# === 14. Summary ===
|
# === 14. Summary ===
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.6 — Edit 1C managed form elements (Python port)
|
# form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -11,6 +11,65 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
@@ -192,7 +251,7 @@ def assert_edit_allowed(target_path, require):
|
|||||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||||
parser.add_argument("-FormPath", "-Path", required=True)
|
parser.add_argument("-FormPath", "-Path", required=True)
|
||||||
parser.add_argument("-JsonPath", required=True)
|
parser.add_argument("-JsonPath", required=True)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
json_path = args.JsonPath
|
json_path = args.JsonPath
|
||||||
@@ -226,6 +285,11 @@ def local_name(node):
|
|||||||
# ── helpers ──────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def esc_xml(s):
|
def esc_xml(s):
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
||||||
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
@@ -254,7 +318,7 @@ root = tree.getroot()
|
|||||||
# ── 2. Load JSON ────────────────────────────────────────────
|
# ── 2. Load JSON ────────────────────────────────────────────
|
||||||
|
|
||||||
with open(json_path, "r", encoding="utf-8-sig") as f:
|
with open(json_path, "r", encoding="utf-8-sig") as f:
|
||||||
defn = json.load(f)
|
defn = ci_json(json.load(f))
|
||||||
|
|
||||||
# ── 3. Form name + header ───────────────────────────────────
|
# ── 3. Form name + header ───────────────────────────────────
|
||||||
|
|
||||||
@@ -386,23 +450,48 @@ _FORM_TYPE_SYNONYMS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
TYPE_SYNONYMS = _FORM_TYPE_SYNONYMS
|
||||||
|
|
||||||
|
|
||||||
def resolve_type_str(type_str):
|
def resolve_type_str(type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return type_str
|
return type_str
|
||||||
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if type_str.startswith('cfg:'):
|
||||||
|
type_str = type_str[4:]
|
||||||
|
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
|
||||||
|
type_str = type_str[type_str.index(':') + 1:]
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
||||||
if m:
|
if m:
|
||||||
base, params = m.group(1).strip(), m.group(2)
|
base_name = m.group(1).strip()
|
||||||
r = _FORM_TYPE_SYNONYMS.get(base.lower())
|
params = m.group(2)
|
||||||
return f"{r}({params})" if r else type_str
|
resolved = TYPE_SYNONYMS.get(base_name.lower())
|
||||||
|
if resolved:
|
||||||
|
return f'{resolved}({params})'
|
||||||
|
return type_str
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
|
||||||
if '.' in type_str:
|
if '.' in type_str:
|
||||||
i = type_str.index('.')
|
dot_idx = type_str.index('.')
|
||||||
prefix, suffix = type_str[:i], type_str[i:]
|
prefix = type_str[:dot_idx]
|
||||||
r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
|
suffix = type_str[dot_idx:] # includes the dot
|
||||||
return f"{r}{suffix}" if r else type_str
|
resolved = TYPE_SYNONYMS.get(prefix.lower())
|
||||||
r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
|
if resolved:
|
||||||
return r if r else type_str
|
return f'{resolved}{suffix}'
|
||||||
|
return type_str
|
||||||
|
# Простое имя
|
||||||
|
resolved = TYPE_SYNONYMS.get(type_str.lower())
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
return type_str
|
||||||
def emit_type(type_str, indent):
|
def emit_type(type_str, indent):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
X(f"{indent}<Type/>")
|
X(f"{indent}<Type/>")
|
||||||
@@ -496,7 +585,7 @@ def emit_mltext(tag, text, indent):
|
|||||||
X(f"{indent}<{tag}>")
|
X(f"{indent}<{tag}>")
|
||||||
X(f"{indent}\t<v8:item>")
|
X(f"{indent}\t<v8:item>")
|
||||||
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
||||||
X(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
X(f"{indent}\t\t<v8:content>{esc_xml_text(text)}</v8:content>")
|
||||||
X(f"{indent}\t</v8:item>")
|
X(f"{indent}\t</v8:item>")
|
||||||
X(f"{indent}</{tag}>")
|
X(f"{indent}</{tag}>")
|
||||||
|
|
||||||
@@ -724,7 +813,7 @@ def emit_label(el, name, _id, indent):
|
|||||||
X(f'{inner}<Title formatted="{formatted}">')
|
X(f'{inner}<Title formatted="{formatted}">')
|
||||||
X(f"{inner}\t<v8:item>")
|
X(f"{inner}\t<v8:item>")
|
||||||
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
|
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
|
||||||
X(f"{inner}\t\t<v8:content>{esc_xml(str(el['title']))}</v8:content>")
|
X(f"{inner}\t\t<v8:content>{esc_xml_text(str(el['title']))}</v8:content>")
|
||||||
X(f"{inner}\t</v8:item>")
|
X(f"{inner}\t</v8:item>")
|
||||||
X(f"{inner}</Title>")
|
X(f"{inner}</Title>")
|
||||||
emit_common_flags(el, inner)
|
emit_common_flags(el, inner)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-info v1.5 — Analyze 1C managed form structure
|
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-info v1.5 — Analyze 1C managed form structure
|
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespace map ---
|
# --- Namespace map ---
|
||||||
|
|
||||||
NSMAP = {
|
NSMAP = {
|
||||||
@@ -353,7 +375,7 @@ def get_support_status_for_path(target_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
def is_external_root(xml_path):
|
def _sg_is_external_root(xml_path):
|
||||||
if not os.path.isfile(xml_path):
|
if not os.path.isfile(xml_path):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -367,14 +389,14 @@ def get_support_status_for_path(target_path):
|
|||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
elem_uuid = root_uuid(rp)
|
elem_uuid = root_uuid(rp)
|
||||||
if is_external_root(rp):
|
if _sg_is_external_root(rp):
|
||||||
return None
|
return None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
d = os.path.dirname(rp)
|
d = os.path.dirname(rp)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
if is_external_root(d + ".xml"):
|
if _sg_is_external_root(d + ".xml"):
|
||||||
return None
|
return None
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = root_uuid(d + ".xml")
|
elem_uuid = root_uuid(d + ".xml")
|
||||||
@@ -433,7 +455,7 @@ def main():
|
|||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
|
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
|
||||||
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
|
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
limit = args.Limit
|
limit = args.Limit
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-remove v1.4 — Remove form from 1C object
|
# form-remove v1.9 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -64,6 +64,10 @@ foreach ($node in $formNodes) {
|
|||||||
$parent.RemoveChild($prev) | Out-Null
|
$parent.RemoveChild($prev) | Out-Null
|
||||||
}
|
}
|
||||||
$parent.RemoveChild($node) | Out-Null
|
$parent.RemoveChild($node) | Out-Null
|
||||||
|
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
|
||||||
|
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
|
||||||
|
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
|
||||||
|
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +78,9 @@ foreach ($node in $formNodes) {
|
|||||||
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
||||||
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
||||||
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
||||||
$node.InnerText = ""
|
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
|
||||||
|
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||||
|
$node.IsEmpty = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +89,26 @@ $encBom = New-Object System.Text.UTF8Encoding($true)
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
|
||||||
|
|
||||||
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# remove-form v1.4 — Remove form from 1C object
|
# form-remove v1.9 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,21 +52,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -67,7 +90,7 @@ def main():
|
|||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
||||||
parser.add_argument("-FormName", required=True)
|
parser.add_argument("-FormName", required=True)
|
||||||
parser.add_argument("-SrcDir", default="src")
|
parser.add_argument("-SrcDir", default="src")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_name = args.ObjectName
|
object_name = args.ObjectName
|
||||||
form_name = args.FormName
|
form_name = args.FormName
|
||||||
@@ -119,6 +142,10 @@ def main():
|
|||||||
if parent.text and parent.text.strip() == "":
|
if parent.text and parent.text.strip() == "":
|
||||||
parent.text = ""
|
parent.text = ""
|
||||||
parent.remove(node)
|
parent.remove(node)
|
||||||
|
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
|
||||||
|
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
|
||||||
|
if len(parent) == 0 and not (parent.text or "").strip():
|
||||||
|
parent.text = None
|
||||||
break
|
break
|
||||||
|
|
||||||
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
||||||
@@ -129,7 +156,9 @@ def main():
|
|||||||
if not isinstance(el.tag, str):
|
if not isinstance(el.tag, str):
|
||||||
continue
|
continue
|
||||||
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
||||||
el.text = ""
|
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
|
||||||
|
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||||
|
el.text = None
|
||||||
|
|
||||||
# Save with BOM
|
# Save with BOM
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
save_xml_with_bom(tree, root_xml_full)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-validate v1.9 — Validate 1C managed form
|
# form-validate v1.17 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -56,9 +56,23 @@ try {
|
|||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||||
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||||
|
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
|
||||||
$root = $xmlDoc.DocumentElement
|
$root = $xmlDoc.DocumentElement
|
||||||
|
|
||||||
|
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
|
||||||
|
# support-guard: is_external_root, авторитет — cf-edit).
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
# --- Detect context: config vs EPF/ERF ---
|
# --- Detect context: config vs EPF/ERF ---
|
||||||
# Walk up from FormPath looking for Configuration.xml → config context
|
# Walk up from FormPath looking for Configuration.xml → config context
|
||||||
# No Configuration.xml → external data processor / report (EPF/ERF)
|
# No Configuration.xml → external data processor / report (EPF/ERF)
|
||||||
@@ -66,13 +80,56 @@ $script:isConfigContext = $false
|
|||||||
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
|
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
|
||||||
for ($i = 0; $i -lt 15; $i++) {
|
for ($i = 0; $i -lt 15; $i++) {
|
||||||
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
|
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
|
||||||
|
# Порядок проверок тот же, что у Detect-FormatVersion: сначала корень автономной обработки,
|
||||||
|
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
|
||||||
|
# версию конфигурации.
|
||||||
|
$extRoot = "$walkDir.xml"
|
||||||
|
if (-not $script:versionAnchor) {
|
||||||
|
if (Test-ExternalObjectRoot $extRoot) {
|
||||||
|
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
|
||||||
|
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
|
||||||
|
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
|
||||||
|
$script:versionAnchor = $extRoot
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
|
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
|
||||||
$script:isConfigContext = $true
|
$script:isConfigContext = $true
|
||||||
|
$script:configXmlPath = Join-Path $walkDir "Configuration.xml"
|
||||||
|
if (-not $script:versionAnchor) { $script:versionAnchor = $script:configXmlPath }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
$walkDir = Split-Path $walkDir
|
$walkDir = Split-Path $walkDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
|
||||||
|
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $d -Parent
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return "2.17"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Counters ---
|
# --- Counters ---
|
||||||
|
|
||||||
$errors = 0
|
$errors = 0
|
||||||
@@ -101,6 +158,19 @@ function Report-Warn {
|
|||||||
Write-Host "[WARN] $msg"
|
Write-Host "[WARN] $msg"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Form name from path ---
|
# --- Form name from path ---
|
||||||
|
|
||||||
$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
|
$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
|
||||||
@@ -127,13 +197,17 @@ if ($root.LocalName -ne "Form") {
|
|||||||
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
||||||
} else {
|
} else {
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
$versionRank = Get-FormatRank $version
|
||||||
if ($version -in @("2.17", "2.18", "2.19", "2.20")) {
|
if (-not $version) {
|
||||||
Report-OK "Root element: Form version=$version"
|
|
||||||
} elseif ($version) {
|
|
||||||
Report-Warn "Form version='$version' (expected 2.17-2.20)"
|
|
||||||
} else {
|
|
||||||
Report-Warn "Form version attribute missing"
|
Report-Warn "Form version attribute missing"
|
||||||
|
} elseif ($versionRank -eq 0) {
|
||||||
|
Report-Error "Malformed version '$version' (expected N.N)"
|
||||||
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} else {
|
||||||
|
Report-OK "Root element: Form version=$version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +218,15 @@ if (-not $stopped) {
|
|||||||
if ($acb) {
|
if ($acb) {
|
||||||
$acbName = $acb.GetAttribute("name")
|
$acbName = $acb.GetAttribute("name")
|
||||||
$acbId = $acb.GetAttribute("id")
|
$acbId = $acb.GetAttribute("id")
|
||||||
|
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
|
||||||
|
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
|
||||||
|
# предупреждение; ошибка — только если id вовсе не число.
|
||||||
if ($acbId -eq "-1") {
|
if ($acbId -eq "-1") {
|
||||||
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
|
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
|
||||||
|
} elseif ($acbId -match '^-?\d+$') {
|
||||||
|
Report-Warn "AutoCommandBar id='$acbId', usually '-1'"
|
||||||
} else {
|
} else {
|
||||||
Report-Error "AutoCommandBar id='$acbId', expected '-1'"
|
Report-Error "AutoCommandBar id='$acbId' is not a number"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Report-Error "AutoCommandBar element missing"
|
Report-Error "AutoCommandBar element missing"
|
||||||
@@ -427,11 +506,19 @@ if (-not $stopped) {
|
|||||||
$segments = $cleanPath -split '\.'
|
$segments = $cleanPath -split '\.'
|
||||||
$rootAttr = $segments[0]
|
$rootAttr = $segments[0]
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
|
||||||
if ($rootAttr -eq 'Items') {
|
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
|
||||||
|
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
|
||||||
|
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
|
||||||
|
$itemsHops = 0
|
||||||
|
$itemsBroken = $false
|
||||||
|
while ($rootAttr -eq 'Items') {
|
||||||
|
$itemsHops++
|
||||||
|
if ($itemsHops -gt 10) { $itemsBroken = $true; break } # страховка от кольца ссылок
|
||||||
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
||||||
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableName = $segments[1]
|
$tableName = $segments[1]
|
||||||
$tableEl = $null
|
$tableEl = $null
|
||||||
@@ -444,17 +531,21 @@ if (-not $stopped) {
|
|||||||
if (-not $tableEl) {
|
if (-not $tableEl) {
|
||||||
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
|
||||||
$pathErrors++
|
$pathErrors++
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
||||||
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
||||||
# Table without DataPath — can't resolve further, accept silently
|
# Table without DataPath — can't resolve further, accept silently
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
||||||
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
||||||
$rootAttr = ($tableDp -split '\.')[0]
|
$segments = $tableDp -split '\.'
|
||||||
|
$rootAttr = $segments[0]
|
||||||
}
|
}
|
||||||
|
if ($itemsBroken) { continue }
|
||||||
|
|
||||||
if (-not $attrMap.ContainsKey($rootAttr)) {
|
if (-not $attrMap.ContainsKey($rootAttr)) {
|
||||||
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
|
||||||
@@ -569,13 +660,18 @@ if (-not $stopped) {
|
|||||||
$actionErrors = 0
|
$actionErrors = 0
|
||||||
$actionChecked = 0
|
$actionChecked = 0
|
||||||
|
|
||||||
|
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
|
||||||
|
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
|
||||||
|
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
|
||||||
|
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
|
||||||
|
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
|
||||||
foreach ($cmd in $cmdNodes) {
|
foreach ($cmd in $cmdNodes) {
|
||||||
if ($stopped) { break }
|
if ($stopped) { break }
|
||||||
$cmdName = $cmd.GetAttribute("name")
|
$cmdName = $cmd.GetAttribute("name")
|
||||||
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
|
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
|
||||||
$actionChecked++
|
$actionChecked++
|
||||||
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
|
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
|
||||||
Report-Error "Command '$cmdName': missing or empty Action"
|
Report-Warn "Command '$cmdName': no Action — handler must be assigned at runtime, otherwise the command does nothing"
|
||||||
$actionErrors++
|
$actionErrors++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,6 +838,41 @@ if (-not $stopped -and $isExtension) {
|
|||||||
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
|
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 11d. Пути на основной реквизит, которого форма не объявляет.
|
||||||
|
# Check 5 такое пропускает: у заимствованной формы он не проверяет базовые элементы (id < 1000000),
|
||||||
|
# а привязки в <xr:Link> вообще вне его списка тегов. Между тем это ровно тот случай, на котором
|
||||||
|
# платформа отвергает загрузку: «Неверный путь к полю - Объект.X». Правило: если основной реквизит
|
||||||
|
# не объявлен в <Attributes> формы, любой путь с его корнем не разрешится.
|
||||||
|
# Корень берётся из основного реквизита BaseForm: «Объект» он только у формы объекта, у формы
|
||||||
|
# списка это «Список», у формы записи регистра «Запись». С зашитым «Объект» проверка на таких
|
||||||
|
# формах молча не срабатывала — валидатор рапортовал «чисто» на форме, которую платформа не примет.
|
||||||
|
$mainAttrDeclared = $false
|
||||||
|
foreach ($attr in $attrNodes) {
|
||||||
|
$maNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr)
|
||||||
|
if ($maNode -and $maNode.InnerText.Trim() -eq "true") { $mainAttrDeclared = $true; break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $mainAttrDeclared) {
|
||||||
|
# Значения привязок ищем текстом: интересуют и обычные теги, и <xr:DataPath> внутри
|
||||||
|
# <ChoiceParameterLinks>, а те живут в чужом пространстве имён.
|
||||||
|
$rawForm = [System.IO.File]::ReadAllText($FormPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$mainBase = $baseFormNode.SelectSingleNode("f:Attributes/f:Attribute[f:MainAttribute='true']", $bfNs)
|
||||||
|
$rootName = if ($mainBase -and $mainBase.GetAttribute("name")) { $mainBase.GetAttribute("name") } else { "Объект" }
|
||||||
|
$rootPat = [regex]::Escape($rootName)
|
||||||
|
$danglingPaths = @{}
|
||||||
|
foreach ($m in [regex]::Matches($rawForm, "<(?:\w+:)?\w*DataPath[^>]*>(${rootPat}\.[^<]+)</(?:\w+:)?\w*DataPath>")) {
|
||||||
|
$danglingPaths[$m.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
if ($danglingPaths.Count -gt 0) {
|
||||||
|
$shown = @($danglingPaths.Keys | Sort-Object)
|
||||||
|
$sample = ($shown | Select-Object -First 3) -join ", "
|
||||||
|
$suffix = if ($shown.Count -gt 3) { " (и ещё $($shown.Count - 3))" } else { "" }
|
||||||
|
Report-Error "Path(s) rooted at '${rootName}' but the form declares no MainAttribute: $sample$suffix"
|
||||||
|
} elseif ($mainBase) {
|
||||||
|
Report-OK "Object paths: none dangling (MainAttribute not declared)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check callType without BaseForm (structural warning)
|
# Check callType without BaseForm (structural warning)
|
||||||
@@ -844,6 +975,71 @@ if (-not $stopped) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 13: префиксы в значениях объявлены в самом файле ---
|
||||||
|
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
|
||||||
|
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
|
||||||
|
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
|
||||||
|
# считаем по узлу (GetNamespaceOfPrefix), а не по корню: локальная xmlns на элементе законна.
|
||||||
|
|
||||||
|
if (-not $stopped) {
|
||||||
|
$prefixErrors = 0
|
||||||
|
$prefixChecked = 0
|
||||||
|
|
||||||
|
$prefixPattern = '^([A-Za-z_][A-Za-z0-9_.-]*):.+$'
|
||||||
|
# Значения, где префикс обязан резолвиться: тип реквизита/колонки и xsi:type
|
||||||
|
# Только листовые узлы: под local-name()='Type' подходит и обёртка <Type>, и вложенный <v8:Type>,
|
||||||
|
# а InnerText обёртки — то же значение, иначе одна ошибка сообщалась бы дважды.
|
||||||
|
foreach ($node in $xmlDoc.SelectNodes("//*[local-name()='Type' or local-name()='TypeSet']", $nsMgr)) {
|
||||||
|
if ($node.SelectSingleNode("*")) { continue }
|
||||||
|
$val = $node.InnerText.Trim()
|
||||||
|
if (-not $val) { continue }
|
||||||
|
$m = [regex]::Match($val, $prefixPattern)
|
||||||
|
if (-not $m.Success) { continue }
|
||||||
|
$prefixChecked++
|
||||||
|
$pfx = $m.Groups[1].Value
|
||||||
|
if (-not $node.GetNamespaceOfPrefix($pfx)) {
|
||||||
|
Report-Error "13. Type '$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
|
||||||
|
$prefixErrors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($node in $xmlDoc.SelectNodes("//*[@xsi:type]", $nsMgr)) {
|
||||||
|
$val = $node.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
$m = [regex]::Match($val, $prefixPattern)
|
||||||
|
if (-not $m.Success) { continue }
|
||||||
|
$prefixChecked++
|
||||||
|
$pfx = $m.Groups[1].Value
|
||||||
|
if (-not $node.GetNamespaceOfPrefix($pfx)) {
|
||||||
|
Report-Error "13. xsi:type='$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
|
||||||
|
$prefixErrors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prefixChecked -eq 0) {
|
||||||
|
Report-OK "13. Namespace prefixes: nothing to check"
|
||||||
|
} elseif ($prefixErrors -eq 0) {
|
||||||
|
Report-OK "13. Namespace prefixes: $prefixChecked values, all declared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
|
||||||
|
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
|
||||||
|
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
|
||||||
|
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
|
||||||
|
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
|
||||||
|
|
||||||
|
if (-not $stopped -and $script:versionAnchor) {
|
||||||
|
$formVer = $root.GetAttribute("version")
|
||||||
|
$dumpVer = Detect-FormatVersion (Split-Path (Resolve-Path $FormPath) -Parent)
|
||||||
|
|
||||||
|
if (-not $formVer) {
|
||||||
|
Report-OK "14. Format version: not comparable"
|
||||||
|
} elseif ($formVer -ne $dumpVer) {
|
||||||
|
Report-Error "14. Format version $formVer differs from the dump ($dumpVer) — a dump carries one version, the platform refuses a file it cannot read"
|
||||||
|
} else {
|
||||||
|
Report-OK "14. Format version: $formVer, matches the dump"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Summary ---
|
# --- Summary ---
|
||||||
|
|
||||||
$checks = $script:okCount + $errors + $warnings
|
$checks = $script:okCount + $errors + $warnings
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-validate v1.9 — Validate 1C managed form
|
# form-validate v1.17 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
|
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
@@ -49,6 +71,64 @@ VALID_CFG_PREFIXES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
|
||||||
|
# support-guard: is_external_root, авторитет — cf-edit).
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
|
||||||
|
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
|
if os.path.isfile(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
head = f.read(2000)
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
@@ -60,7 +140,7 @@ def main():
|
|||||||
parser.add_argument("-FormPath", "-Path", required=True)
|
parser.add_argument("-FormPath", "-Path", required=True)
|
||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
detailed = args.Detailed
|
detailed = args.Detailed
|
||||||
@@ -106,13 +186,29 @@ def main():
|
|||||||
|
|
||||||
# Detect context: config vs EPF/ERF
|
# Detect context: config vs EPF/ERF
|
||||||
is_config_context = False
|
is_config_context = False
|
||||||
|
config_xml_path = ''
|
||||||
|
version_anchor = ''
|
||||||
walk_dir = os.path.dirname(os.path.abspath(form_path))
|
walk_dir = os.path.dirname(os.path.abspath(form_path))
|
||||||
for _ in range(15):
|
for _ in range(15):
|
||||||
parent = os.path.dirname(walk_dir)
|
parent = os.path.dirname(walk_dir)
|
||||||
if parent == walk_dir:
|
if parent == walk_dir:
|
||||||
break
|
break
|
||||||
|
# Порядок проверок тот же, что у detect_format_version: сначала корень автономной обработки,
|
||||||
|
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
|
||||||
|
# версию конфигурации.
|
||||||
|
ext_root = walk_dir + '.xml'
|
||||||
|
if not version_anchor:
|
||||||
|
if _sg_is_external_root(ext_root):
|
||||||
|
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
|
||||||
|
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
|
||||||
|
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
|
||||||
|
version_anchor = ext_root
|
||||||
|
break
|
||||||
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
|
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
|
||||||
is_config_context = True
|
is_config_context = True
|
||||||
|
config_xml_path = os.path.join(walk_dir, 'Configuration.xml')
|
||||||
|
if not version_anchor:
|
||||||
|
version_anchor = config_xml_path
|
||||||
break
|
break
|
||||||
walk_dir = parent
|
walk_dir = parent
|
||||||
|
|
||||||
@@ -161,13 +257,19 @@ def main():
|
|||||||
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
||||||
else:
|
else:
|
||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
version_rank = format_rank(version)
|
||||||
if version in ("2.17", "2.18", "2.19", "2.20"):
|
if not version:
|
||||||
report_ok(f"Root element: Form version={version}")
|
|
||||||
elif version:
|
|
||||||
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
|
|
||||||
else:
|
|
||||||
report_warn("Form version attribute missing")
|
report_warn("Form version attribute missing")
|
||||||
|
elif version_rank == 0:
|
||||||
|
report_error(f"Malformed version '{version}' (expected N.N)")
|
||||||
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
report_warn(f"Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
report_warn(f"Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
else:
|
||||||
|
report_ok(f"Root element: Form version={version}")
|
||||||
|
|
||||||
# --- Check 2: AutoCommandBar ---
|
# --- Check 2: AutoCommandBar ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -175,10 +277,15 @@ def main():
|
|||||||
if acb is not None:
|
if acb is not None:
|
||||||
acb_name = acb.get("name", "")
|
acb_name = acb.get("name", "")
|
||||||
acb_id = acb.get("id", "")
|
acb_id = acb.get("id", "")
|
||||||
|
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
|
||||||
|
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
|
||||||
|
# предупреждение; ошибка — только если id вовсе не число.
|
||||||
if acb_id == "-1":
|
if acb_id == "-1":
|
||||||
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
|
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
|
||||||
|
elif re.match(r'^-?\d+$', acb_id):
|
||||||
|
report_warn(f"AutoCommandBar id='{acb_id}', usually '-1'")
|
||||||
else:
|
else:
|
||||||
report_error(f"AutoCommandBar id='{acb_id}', expected '-1'")
|
report_error(f"AutoCommandBar id='{acb_id}' is not a number")
|
||||||
else:
|
else:
|
||||||
report_error("AutoCommandBar element missing")
|
report_error("AutoCommandBar element missing")
|
||||||
|
|
||||||
@@ -430,11 +537,21 @@ def main():
|
|||||||
segments = clean_path.split(".")
|
segments = clean_path.split(".")
|
||||||
root_attr = segments[0]
|
root_attr = segments[0]
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
|
||||||
if root_attr == 'Items':
|
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
|
||||||
|
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
|
||||||
|
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
|
||||||
|
items_hops = 0
|
||||||
|
items_broken = False
|
||||||
|
while root_attr == 'Items':
|
||||||
|
items_hops += 1
|
||||||
|
if items_hops > 10: # страховка от кольца ссылок
|
||||||
|
items_broken = True
|
||||||
|
break
|
||||||
if len(segments) < 3 or segments[2] != 'CurrentData':
|
if len(segments) < 3 or segments[2] != 'CurrentData':
|
||||||
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_name = segments[1]
|
table_name = segments[1]
|
||||||
table_el = None
|
table_el = None
|
||||||
for candidate in all_elements:
|
for candidate in all_elements:
|
||||||
@@ -444,14 +561,19 @@ def main():
|
|||||||
if table_el is None:
|
if table_el is None:
|
||||||
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
|
||||||
path_errors += 1
|
path_errors += 1
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
||||||
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
||||||
if table_dp.startswith('~'):
|
if table_dp.startswith('~'):
|
||||||
table_dp = table_dp[1:]
|
table_dp = table_dp[1:]
|
||||||
root_attr = table_dp.split(".")[0]
|
segments = table_dp.split(".")
|
||||||
|
root_attr = segments[0]
|
||||||
|
if items_broken:
|
||||||
|
continue
|
||||||
|
|
||||||
if root_attr not in attr_map:
|
if root_attr not in attr_map:
|
||||||
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
|
||||||
@@ -465,6 +587,8 @@ def main():
|
|||||||
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
||||||
if path_errors == 0 and path_msg:
|
if path_errors == 0 and path_msg:
|
||||||
report_ok(f"Data bindings: {path_msg}")
|
report_ok(f"Data bindings: {path_msg}")
|
||||||
|
elif path_errors == 0:
|
||||||
|
report_ok("Data bindings: none")
|
||||||
|
|
||||||
# --- Check 6: Button command references ---
|
# --- Check 6: Button command references ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -499,6 +623,8 @@ def main():
|
|||||||
|
|
||||||
if cmd_errors == 0 and cmd_checked > 0:
|
if cmd_errors == 0 and cmd_checked > 0:
|
||||||
report_ok(f"Command references: {cmd_checked} buttons checked")
|
report_ok(f"Command references: {cmd_checked} buttons checked")
|
||||||
|
elif cmd_checked == 0:
|
||||||
|
report_ok("Command references: none")
|
||||||
|
|
||||||
# --- Check 7: Events have handler names ---
|
# --- Check 7: Events have handler names ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -538,12 +664,19 @@ def main():
|
|||||||
|
|
||||||
if event_errors == 0 and event_checked > 0:
|
if event_errors == 0 and event_checked > 0:
|
||||||
report_ok(f"Event handlers: {event_checked} events checked")
|
report_ok(f"Event handlers: {event_checked} events checked")
|
||||||
|
elif event_checked == 0:
|
||||||
|
report_ok("Event handlers: none")
|
||||||
|
|
||||||
# --- Check 8: Command actions ---
|
# --- Check 8: Command actions ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
action_errors = 0
|
action_errors = 0
|
||||||
action_checked = 0
|
action_checked = 0
|
||||||
|
|
||||||
|
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
|
||||||
|
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
|
||||||
|
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
|
||||||
|
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
|
||||||
|
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
|
||||||
for cmd in cmd_nodes:
|
for cmd in cmd_nodes:
|
||||||
if stopped:
|
if stopped:
|
||||||
break
|
break
|
||||||
@@ -551,11 +684,13 @@ def main():
|
|||||||
action_node = cmd.find(f"{{{F_NS}}}Action")
|
action_node = cmd.find(f"{{{F_NS}}}Action")
|
||||||
action_checked += 1
|
action_checked += 1
|
||||||
if action_node is None or not (action_node.text or "").strip():
|
if action_node is None or not (action_node.text or "").strip():
|
||||||
report_error(f"Command '{cmd_name}': missing or empty Action")
|
report_warn(f"Command '{cmd_name}': no Action — handler must be assigned at runtime, otherwise the command does nothing")
|
||||||
action_errors += 1
|
action_errors += 1
|
||||||
|
|
||||||
if action_errors == 0 and action_checked > 0:
|
if action_errors == 0 and action_checked > 0:
|
||||||
report_ok(f"Command actions: {action_checked} commands checked")
|
report_ok(f"Command actions: {action_checked} commands checked")
|
||||||
|
elif action_checked == 0:
|
||||||
|
report_ok("Command actions: none")
|
||||||
|
|
||||||
# --- Check 9: MainAttribute count ---
|
# --- Check 9: MainAttribute count ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -686,6 +821,39 @@ def main():
|
|||||||
if (ext_attr_count + ext_cmd_count) > 0:
|
if (ext_attr_count + ext_cmd_count) > 0:
|
||||||
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
|
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
|
||||||
|
|
||||||
|
# 11d. \u041f\u0443\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442.
|
||||||
|
# Check 5 \u0442\u0430\u043a\u043e\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442: \u0443 \u0437\u0430\u0438\u043c\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043e\u043d \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b (id < 1000000),
|
||||||
|
# \u0430 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u0432 <xr:Link> \u0432\u043e\u043e\u0431\u0449\u0435 \u0432\u043d\u0435 \u0435\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u0435\u0433\u043e\u0432. \u041c\u0435\u0436\u0434\u0443 \u0442\u0435\u043c \u044d\u0442\u043e \u0440\u043e\u0432\u043d\u043e \u0442\u043e\u0442 \u0441\u043b\u0443\u0447\u0430\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c
|
||||||
|
# \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043e\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443: \u00ab\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u043b\u044e - \u041e\u0431\u044a\u0435\u043a\u0442.X\u00bb. \u041f\u0440\u0430\u0432\u0438\u043b\u043e: \u0435\u0441\u043b\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442
|
||||||
|
# \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d \u0432 <Attributes> \u0444\u043e\u0440\u043c\u044b, \u043b\u044e\u0431\u043e\u0439 \u043f\u0443\u0442\u044c \u0441 \u0435\u0433\u043e \u043a\u043e\u0440\u043d\u0435\u043c \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0441\u044f.
|
||||||
|
# \u041a\u043e\u0440\u0435\u043d\u044c \u0431\u0435\u0440\u0451\u0442\u0441\u044f \u0438\u0437 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 BaseForm: \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043e\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0443 \u0444\u043e\u0440\u043c\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u0443 \u0444\u043e\u0440\u043c\u044b
|
||||||
|
# \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u0442\u043e \u00ab\u0421\u043f\u0438\u0441\u043e\u043a\u00bb, \u0443 \u0444\u043e\u0440\u043c\u044b \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430 \u00ab\u0417\u0430\u043f\u0438\u0441\u044c\u00bb. \u0421 \u0437\u0430\u0448\u0438\u0442\u044b\u043c \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0442\u0430\u043a\u0438\u0445
|
||||||
|
# \u0444\u043e\u0440\u043c\u0430\u0445 \u043c\u043e\u043b\u0447\u0430 \u043d\u0435 \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043b\u0430.
|
||||||
|
main_attr_declared = False
|
||||||
|
for attr in attr_nodes:
|
||||||
|
ma_node = attr.find(f"{{{F_NS}}}MainAttribute")
|
||||||
|
if ma_node is not None and (ma_node.text or "").strip() == "true":
|
||||||
|
main_attr_declared = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not main_attr_declared:
|
||||||
|
# \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438\u0432\u044f\u0437\u043e\u043a \u0438\u0449\u0435\u043c \u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u044e\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u0442\u0435\u0433\u0438, \u0438 <xr:DataPath> \u0432\u043d\u0443\u0442\u0440\u0438
|
||||||
|
# <ChoiceParameterLinks>, \u0430 \u0442\u0435 \u0436\u0438\u0432\u0443\u0442 \u0432 \u0447\u0443\u0436\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0435 \u0438\u043c\u0451\u043d.
|
||||||
|
with open(form_path, "r", encoding="utf-8-sig") as fh:
|
||||||
|
raw_form = fh.read()
|
||||||
|
main_base = base_form_node.find(f"{{{F_NS}}}Attributes/{{{F_NS}}}Attribute[{{{F_NS}}}MainAttribute='true']")
|
||||||
|
root_name = main_base.get("name") if main_base is not None and main_base.get("name") else "\u041e\u0431\u044a\u0435\u043a\u0442"
|
||||||
|
root_pat = re.escape(root_name)
|
||||||
|
dangling_paths = set(re.findall(
|
||||||
|
r'<(?:\w+:)?\w*DataPath[^>]*>(' + root_pat + r'\.[^<]+)</(?:\w+:)?\w*DataPath>', raw_form))
|
||||||
|
if dangling_paths:
|
||||||
|
shown = sorted(dangling_paths)
|
||||||
|
sample = ", ".join(shown[:3])
|
||||||
|
suffix = f" (\u0438 \u0435\u0449\u0451 {len(shown) - 3})" if len(shown) > 3 else ""
|
||||||
|
report_error(f"Path(s) rooted at '{root_name}' but the form declares no MainAttribute: {sample}{suffix}")
|
||||||
|
elif main_base is not None:
|
||||||
|
report_ok("Object paths: none dangling (MainAttribute not declared)")
|
||||||
|
|
||||||
# Check callType without BaseForm
|
# Check callType without BaseForm
|
||||||
if not stopped and not is_extension:
|
if not stopped and not is_extension:
|
||||||
call_type_without_base = False
|
call_type_without_base = False
|
||||||
@@ -748,6 +916,62 @@ def main():
|
|||||||
else:
|
else:
|
||||||
report_ok('12. Types: no type values to check')
|
report_ok('12. Types: no type values to check')
|
||||||
|
|
||||||
|
# --- Check 13: префиксы в значениях объявлены в самом файле ---
|
||||||
|
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
|
||||||
|
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
|
||||||
|
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
|
||||||
|
# считаем по узлу (nsmap элемента), а не по корню: локальная xmlns на элементе законна.
|
||||||
|
if not stopped:
|
||||||
|
prefix_errors = 0
|
||||||
|
prefix_checked = 0
|
||||||
|
prefix_re = re.compile(r'^([A-Za-z_][A-Za-z0-9_.-]*):.+$')
|
||||||
|
|
||||||
|
for node in root.iter():
|
||||||
|
if not isinstance(node.tag, str):
|
||||||
|
continue
|
||||||
|
ln = localname(node)
|
||||||
|
values = []
|
||||||
|
if ln in ('Type', 'TypeSet'):
|
||||||
|
values.append((node.text or '').strip())
|
||||||
|
xsi_type = node.get(f'{{{"http://www.w3.org/2001/XMLSchema-instance"}}}type')
|
||||||
|
if xsi_type:
|
||||||
|
values.append(xsi_type.strip())
|
||||||
|
for val in values:
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
m = prefix_re.match(val)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
prefix_checked += 1
|
||||||
|
pfx = m.group(1)
|
||||||
|
if pfx not in node.nsmap:
|
||||||
|
kind = "xsi:type" if val == xsi_type else "Type"
|
||||||
|
report_error(f"13. {kind} '{val}': namespace prefix '{pfx}:' is not declared "
|
||||||
|
"— the platform cannot read the file (XDTO)")
|
||||||
|
prefix_errors += 1
|
||||||
|
|
||||||
|
if prefix_checked == 0:
|
||||||
|
report_ok('13. Namespace prefixes: nothing to check')
|
||||||
|
elif prefix_errors == 0:
|
||||||
|
report_ok(f'13. Namespace prefixes: {prefix_checked} values, all declared')
|
||||||
|
|
||||||
|
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
|
||||||
|
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
|
||||||
|
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
|
||||||
|
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
|
||||||
|
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
|
||||||
|
if not stopped and version_anchor:
|
||||||
|
form_ver = root.get('version', '')
|
||||||
|
dump_ver = detect_format_version(os.path.dirname(os.path.abspath(form_path)))
|
||||||
|
|
||||||
|
if not form_ver:
|
||||||
|
report_ok('14. Format version: not comparable')
|
||||||
|
elif form_ver != dump_ver:
|
||||||
|
report_error(f'14. Format version {form_ver} differs from the dump ({dump_ver}) '
|
||||||
|
'— a dump carries one version, the platform refuses a file it cannot read')
|
||||||
|
else:
|
||||||
|
report_ok(f'14. Format version: {form_ver}, matches the dump')
|
||||||
|
|
||||||
# --- Finalize ---
|
# --- Finalize ---
|
||||||
checks = ok_count + errors + warnings
|
checks = ok_count + errors + warnings
|
||||||
if errors == 0 and warnings == 0 and not detailed:
|
if errors == 0 and warnings == 0 and not detailed:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# help-add v1.9 — Add built-in help to 1C object
|
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -149,10 +149,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
$head = $content.Substring(0, [Math]::Min(2000, $content.Length))
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -195,7 +205,18 @@ $helpXml = @"
|
|||||||
</Help>
|
</Help>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
#
|
||||||
|
# HTML-страница сюда НЕ идёт — платформа хранит её с LF.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $helpXmlPath $helpXml $encBom
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
# --- 2. Help/<lang>.html ---
|
||||||
|
|
||||||
@@ -255,11 +276,26 @@ if (Test-Path $formsDir) {
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $formMeta.FullName) -and ([System.IO.File]::ReadAllText($formMeta.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($formMeta.FullName, $xmlText, $encBom)
|
||||||
|
|
||||||
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# add-help v1.9 — Add built-in help to 1C object
|
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,6 +213,16 @@ def assert_edit_allowed(target_path, require):
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -222,21 +254,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -252,10 +285,24 @@ def save_xml_with_bom(tree, path):
|
|||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
def write_utf8_bom(path, content):
|
||||||
"""Write text to file with UTF-8 BOM."""
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
f.write(text)
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
|
||||||
|
HTML-страница сюда НЕ идёт — платформа хранит её с LF.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -265,7 +312,7 @@ def main():
|
|||||||
parser.add_argument("-ObjectName", required=True)
|
parser.add_argument("-ObjectName", required=True)
|
||||||
parser.add_argument("-Lang", default="ru")
|
parser.add_argument("-Lang", default="ru")
|
||||||
parser.add_argument("-SrcDir", default="src")
|
parser.add_argument("-SrcDir", default="src")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_name = args.ObjectName
|
object_name = args.ObjectName
|
||||||
lang = args.Lang
|
lang = args.Lang
|
||||||
@@ -301,7 +348,7 @@ def main():
|
|||||||
'</Help>'
|
'</Help>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(help_xml_path, help_xml)
|
write_xml_file(help_xml_path, help_xml)
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
# --- 2. Help/<lang>.html ---
|
||||||
|
|
||||||
@@ -324,7 +371,7 @@ def main():
|
|||||||
'</html>'
|
'</html>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(help_html_path, help_html)
|
write_utf8_bom(help_html_path, help_html)
|
||||||
|
|
||||||
# --- 3. Check IncludeHelpInContents in form metadata ---
|
# --- 3. Check IncludeHelpInContents in form metadata ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# img-grid v1.1 — Overlay numbered grid on image
|
# img-grid v1.2 — Overlay numbered grid on image
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
||||||
|
|
||||||
@@ -16,6 +16,28 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
MARGIN_TOP = 20
|
MARGIN_TOP = 20
|
||||||
MARGIN_LEFT = 24
|
MARGIN_LEFT = 24
|
||||||
|
|
||||||
@@ -30,7 +52,7 @@ def main():
|
|||||||
parser.add_argument("-r", "--rows", type=int, default=0,
|
parser.add_argument("-r", "--rows", type=int, default=0,
|
||||||
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
|
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
|
||||||
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
if args.cols <= 0:
|
if args.cols <= 0:
|
||||||
parser.error("--cols must be greater than 0")
|
parser.error("--cols must be greater than 0")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-edit v1.9 — Edit 1C CommandInterface.xml
|
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -162,6 +162,14 @@ Assert-EditAllowed $CIPath 'editable'
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
@@ -202,7 +210,12 @@ if (-not (Test-Path $CIPath)) {
|
|||||||
</CommandInterface>
|
</CommandInterface>
|
||||||
"@
|
"@
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom)
|
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF, без перевода строки в конце.
|
||||||
|
# (Правка существующего файла, наоборот, наследует его стиль — это делает
|
||||||
|
# основной путь сохранения ниже.) Нормализация нужна потому, что here-string
|
||||||
|
# берёт переводы строк из самого .ps1, а он в репозитории хранится с LF.
|
||||||
|
$emptyCI = ($emptyCI -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($CIPath, $emptyCI.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
|
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
|
||||||
} else {
|
} else {
|
||||||
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
|
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
|
||||||
@@ -383,6 +396,10 @@ $script:typeNormMap = @{
|
|||||||
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
|
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
|
||||||
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
|
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
|
||||||
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
|
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
|
||||||
|
"РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister"
|
||||||
|
"ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes"
|
||||||
|
"Роль"="Role"; "ОбщийМакет"="CommonTemplate"; "ЭлементСтиля"="StyleItem"
|
||||||
|
"ОбщийРеквизит"="CommonAttribute"; "ГруппаКоманд"="CommandGroup"
|
||||||
# Russian plural
|
# Russian plural
|
||||||
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
|
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
|
||||||
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
|
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
|
||||||
@@ -392,6 +409,10 @@ $script:typeNormMap = @{
|
|||||||
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
|
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
|
||||||
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
|
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
|
||||||
"Подсистемы"="Subsystem"
|
"Подсистемы"="Subsystem"
|
||||||
|
"РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister"
|
||||||
|
"ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes"
|
||||||
|
"Роли"="Role"; "ОбщиеМакеты"="CommonTemplate"; "ЭлементыСтиля"="StyleItem"
|
||||||
|
"ОбщиеРеквизиты"="CommonAttribute"; "ГруппыКоманд"="CommandGroup"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Normalize-CmdName([string]$name) {
|
function Normalize-CmdName([string]$name) {
|
||||||
@@ -674,8 +695,16 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
Info "Saved: $resolvedPath"
|
Info "Saved: $resolvedPath"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-edit v1.9 — Edit 1C CommandInterface.xml
|
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,65 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -189,6 +248,16 @@ def assert_edit_allowed(target_path, require):
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -282,7 +351,7 @@ def import_ci_fragment(xml_string):
|
|||||||
def parse_value_list(val):
|
def parse_value_list(val):
|
||||||
val = val.strip()
|
val = val.strip()
|
||||||
if val.startswith("["):
|
if val.startswith("["):
|
||||||
arr = json.loads(val)
|
arr = ci_json(json.loads(val))
|
||||||
return [str(item) for item in arr]
|
return [str(item) for item in arr]
|
||||||
return [val]
|
return [val]
|
||||||
|
|
||||||
@@ -304,21 +373,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -357,6 +427,10 @@ TYPE_NORM_MAP = {
|
|||||||
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
||||||
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
||||||
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
|
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
|
||||||
|
'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister',
|
||||||
|
'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes',
|
||||||
|
'Роль': 'Role', 'ОбщийМакет': 'CommonTemplate', 'ЭлементСтиля': 'StyleItem',
|
||||||
|
'ОбщийРеквизит': 'CommonAttribute', 'ГруппаКоманд': 'CommandGroup',
|
||||||
# Russian plural
|
# Russian plural
|
||||||
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
||||||
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
|
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
|
||||||
@@ -366,6 +440,10 @@ TYPE_NORM_MAP = {
|
|||||||
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
||||||
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
||||||
'Подсистемы': 'Subsystem',
|
'Подсистемы': 'Subsystem',
|
||||||
|
'РегистрыРасчёта': 'CalculationRegister', 'РегистрыРасчета': 'CalculationRegister',
|
||||||
|
'ПланыВидовРасчёта': 'ChartOfCalculationTypes', 'ПланыВидовРасчета': 'ChartOfCalculationTypes',
|
||||||
|
'Роли': 'Role', 'ОбщиеМакеты': 'CommonTemplate', 'ЭлементыСтиля': 'StyleItem',
|
||||||
|
'ОбщиеРеквизиты': 'CommonAttribute', 'ГруппыКоманд': 'CommandGroup',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -401,7 +479,7 @@ def main():
|
|||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-CreateIfMissing", action="store_true")
|
parser.add_argument("-CreateIfMissing", action="store_true")
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Mode validation ---
|
# --- Mode validation ---
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
@@ -438,7 +516,12 @@ def main():
|
|||||||
f'\tversion="{format_version}">\n'
|
f'\tversion="{format_version}">\n'
|
||||||
f'</CommandInterface>'
|
f'</CommandInterface>'
|
||||||
)
|
)
|
||||||
with open(ci_path, "w", encoding="utf-8-sig") as fh:
|
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF в разделителях. (Правка
|
||||||
|
# существующего файла, наоборот, наследует его стиль — это делает
|
||||||
|
# save_xml_bom через _detect_xml_style.) newline="" обязателен: без него
|
||||||
|
# текстовый режим дал бы CRLF на Windows и LF на macOS.
|
||||||
|
empty_ci = empty_ci.replace("\r\n", "\n").replace("\n", "\r\n").rstrip("\r\n")
|
||||||
|
with open(ci_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
fh.write(empty_ci)
|
fh.write(empty_ci)
|
||||||
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
|
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
|
||||||
else:
|
else:
|
||||||
@@ -564,7 +647,7 @@ def main():
|
|||||||
|
|
||||||
def do_place(json_val):
|
def do_place(json_val):
|
||||||
nonlocal add_count, modify_count
|
nonlocal add_count, modify_count
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||||
cmd_name = normalize_cmd_name(str(defn["command"]))
|
cmd_name = normalize_cmd_name(str(defn["command"]))
|
||||||
group_name = str(defn["group"])
|
group_name = str(defn["group"])
|
||||||
if not cmd_name or not group_name:
|
if not cmd_name or not group_name:
|
||||||
@@ -592,7 +675,7 @@ def main():
|
|||||||
|
|
||||||
def do_order(json_val):
|
def do_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||||
group_name = str(defn["group"])
|
group_name = str(defn["group"])
|
||||||
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
||||||
if not group_name or not commands:
|
if not group_name or not commands:
|
||||||
@@ -626,7 +709,7 @@ def main():
|
|||||||
|
|
||||||
def do_subsystem_order(json_val):
|
def do_subsystem_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||||
subsystems = [str(s) for s in parsed]
|
subsystems = [str(s) for s in parsed]
|
||||||
if not subsystems:
|
if not subsystems:
|
||||||
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
||||||
@@ -651,7 +734,7 @@ def main():
|
|||||||
|
|
||||||
def do_group_order(json_val):
|
def do_group_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||||
groups = [str(g) for g in parsed]
|
groups = [str(g) for g in parsed]
|
||||||
if not groups:
|
if not groups:
|
||||||
print("group-order requires array of group names", file=sys.stderr)
|
print("group-order requires array of group names", file=sys.stderr)
|
||||||
@@ -681,7 +764,7 @@ def main():
|
|||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
ops = json.loads(fh.read())
|
ops = ci_json(json.loads(fh.read()))
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -691,19 +774,21 @@ def main():
|
|||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
op_name = op.get("operation", args.Operation or "")
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
# PS сравнивает имя операции через switch, а он регистронезависим.
|
||||||
|
op_key = str(op_name).lower()
|
||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "hide":
|
if op_key == "hide":
|
||||||
do_hide(parse_value_list(op_value))
|
do_hide(parse_value_list(op_value))
|
||||||
elif op_name == "show":
|
elif op_key == "show":
|
||||||
do_show(parse_value_list(op_value))
|
do_show(parse_value_list(op_value))
|
||||||
elif op_name == "place":
|
elif op_key == "place":
|
||||||
do_place(op_value)
|
do_place(op_value)
|
||||||
elif op_name == "order":
|
elif op_key == "order":
|
||||||
do_order(op_value)
|
do_order(op_value)
|
||||||
elif op_name == "subsystem-order":
|
elif op_key == "subsystem-order":
|
||||||
do_subsystem_order(op_value)
|
do_subsystem_order(op_value)
|
||||||
elif op_name == "group-order":
|
elif op_key == "group-order":
|
||||||
do_group_order(op_value)
|
do_group_order(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
|
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -51,16 +51,21 @@ $script:output = New-Object System.Text.StringBuilder 8192
|
|||||||
$script:allCommandNames = @()
|
$script:allCommandNames = @()
|
||||||
|
|
||||||
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
||||||
function Report-OK([string]$msg) {
|
function Report-OK {
|
||||||
|
param([string]$msg)
|
||||||
$script:okCount++
|
$script:okCount++
|
||||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||||
}
|
}
|
||||||
function Report-Error([string]$msg) {
|
function Report-Error {
|
||||||
|
param([string]$msg)
|
||||||
$script:errors++
|
$script:errors++
|
||||||
Out-Line "[ERROR] $msg"
|
Out-Line "[ERROR] $msg"
|
||||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
if ($script:errors -ge $MaxErrors) {
|
||||||
|
$script:stopped = $true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function Report-Warn([string]$msg) {
|
function Report-Warn {
|
||||||
|
param([string]$msg)
|
||||||
$script:warnings++
|
$script:warnings++
|
||||||
Out-Line "[WARN] $msg"
|
Out-Line "[WARN] $msg"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
|
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
|
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
||||||
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
|
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
|
||||||
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
||||||
@@ -83,7 +105,7 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
ci_path = args.CIPath
|
ci_path = args.CIPath
|
||||||
detailed = args.Detailed
|
detailed = args.Detailed
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-compile v1.76 — Compile 1C metadata object from JSON
|
# meta-compile v1.94 — Compile 1C metadata object from JSON
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -161,7 +161,10 @@ if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq '
|
|||||||
$idx = 0
|
$idx = 0
|
||||||
foreach ($item in $def) {
|
foreach ($item in $def) {
|
||||||
$idx++
|
$idx++
|
||||||
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json"
|
# Имя с GUID, а не "batch-$idx": фиксированное имя в общем %TEMP% сталкивало
|
||||||
|
# два параллельных запуска навыка на одной машине — Set-Content падал с
|
||||||
|
# «file is being used by another process». py-порт уже брал mkstemp.
|
||||||
|
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx-$([guid]::NewGuid().ToString('N')).json"
|
||||||
try {
|
try {
|
||||||
$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
|
$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
|
||||||
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
|
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
|
||||||
@@ -405,6 +408,11 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac
|
|||||||
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
|
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
|
||||||
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
|
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
|
||||||
"CommonPicture","CommonTemplate")
|
"CommonPicture","CommonTemplate")
|
||||||
|
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
|
||||||
|
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
|
||||||
|
# Прощаем регистр, но приводим к канону списка.
|
||||||
|
$canonType = $validTypes | Where-Object { $_ -eq $objType } | Select-Object -First 1
|
||||||
|
if ($canonType) { $objType = $canonType }
|
||||||
if ($objType -notin $validTypes) {
|
if ($objType -notin $validTypes) {
|
||||||
Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')"
|
Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -544,6 +552,10 @@ $script:typeNamespaceMap = @{
|
|||||||
}
|
}
|
||||||
# Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…)
|
# Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…)
|
||||||
# и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory).
|
# и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory).
|
||||||
|
# Префикс current-config для ссылочных типов. 'cfg' — для файлов, чья шапка его объявляет
|
||||||
|
# (объектный XML, Ext/Form.xml общей формы). $null на время сборки Ext/Predefined.xml, чья
|
||||||
|
# шапка его НЕ объявляет: там и платформа уходит на локальное объявление.
|
||||||
|
$script:cfgPrefix = 'cfg'
|
||||||
$script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion")
|
$script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion")
|
||||||
$script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
|
$script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
|
||||||
"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister",
|
"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister",
|
||||||
@@ -574,7 +586,20 @@ function Resolve-TypeStr {
|
|||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
|
|
||||||
# Check for parameterized types: Number(15,2), Строка(100), etc.
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$baseName = $Matches[1].Trim()
|
$baseName = $Matches[1].Trim()
|
||||||
$params = $Matches[2]
|
$params = $Matches[2]
|
||||||
@@ -583,7 +608,7 @@ function Resolve-TypeStr {
|
|||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$dotIdx = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $dotIdx)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
@@ -593,10 +618,9 @@ function Resolve-TypeStr {
|
|||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simple name lookup
|
# Простое имя
|
||||||
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
if ($resolved) { return $resolved }
|
if ($resolved) { return $resolved }
|
||||||
|
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,10 +629,44 @@ function Emit-TypeContent {
|
|||||||
if (-not $typeStr) { return }
|
if (-not $typeStr) { return }
|
||||||
|
|
||||||
# Composite type: "Type1 + Type2 + Type3"
|
# Composite type: "Type1 + Type2 + Type3"
|
||||||
|
# Платформа пишет сначала ВСЕ <v8:Type>/<v8:TypeSet>, и только потом блоки
|
||||||
|
# квалификаторов — а рекурсия ниже печатала бы каждую часть целиком (тип вместе со
|
||||||
|
# своими квалификаторами). На одиночном типе оба порядка совпадают, поэтому
|
||||||
|
# расхождение вылезало только на составном.
|
||||||
|
# Порядок самих блоков квалификаторов тоже канонический и НЕ зеркалит порядок типов:
|
||||||
|
# Number, String, Date (корпус acc+erp, контрпримеров нет — при типах
|
||||||
|
# boolean,string,dateTime,decimal квалификаторы идут Number,String,Date).
|
||||||
|
# Порядок типов при этом сохраняем как в DSL: он и есть порядок источника.
|
||||||
if ($typeStr.Contains(' + ')) {
|
if ($typeStr.Contains(' + ')) {
|
||||||
$parts = $typeStr -split '\s*\+\s*'
|
$parts = $typeStr -split '\s*\+\s*'
|
||||||
|
$typeLines = New-Object System.Collections.ArrayList
|
||||||
|
$qualBlocks = @{} # 'Number'|'String'|'Date' → строки блока
|
||||||
foreach ($part in $parts) {
|
foreach ($part in $parts) {
|
||||||
|
# X пишет в StringBuilder, поэтому «перехват» — это запомнить длину, вызвать
|
||||||
|
# эмиттер и откатить добавленное. В py-порту X добавляет в список, и там тот
|
||||||
|
# же алгоритм выражен срезом — различие рантаймов, не логики.
|
||||||
|
$before = $script:xml.Length
|
||||||
Emit-TypeContent $indent $part.Trim()
|
Emit-TypeContent $indent $part.Trim()
|
||||||
|
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
|
||||||
|
[void]$script:xml.Remove($before, $script:xml.Length - $before)
|
||||||
|
$curQual = $null
|
||||||
|
foreach ($line in ($chunk -split "`r?`n")) {
|
||||||
|
if ($line -eq '') { continue }
|
||||||
|
if ($line -match '<v8:(String|Number|Date)Qualifiers>') {
|
||||||
|
$curQual = $Matches[1]
|
||||||
|
$qualBlocks[$curQual] = New-Object System.Collections.ArrayList
|
||||||
|
}
|
||||||
|
if ($curQual) {
|
||||||
|
[void]$qualBlocks[$curQual].Add($line)
|
||||||
|
if ($line -match '</v8:(String|Number|Date)Qualifiers>') { $curQual = $null }
|
||||||
|
} else {
|
||||||
|
[void]$typeLines.Add($line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($line in $typeLines) { X $line }
|
||||||
|
foreach ($q in @('Number', 'String', 'Date')) {
|
||||||
|
if ($qualBlocks.ContainsKey($q)) { foreach ($line in $qualBlocks[$q]) { X $line } }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -723,9 +781,22 @@ function Emit-TypeContent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reference types — use local xmlns declaration for 1C compatibility
|
# Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
|
||||||
|
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке ($script:xmlnsDecl):
|
||||||
|
# формально эквивалентно (значим URI, не префикс) и платформой принималось, но
|
||||||
|
# первый же цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип
|
||||||
|
# в cfg: — то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
|
||||||
|
# действительно не работает; в метаданных такого ограничения нет.
|
||||||
|
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. $script:typeNamespaceMap.
|
||||||
|
# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||||
|
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||||
|
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
|
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
|
||||||
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
|
if ($script:cfgPrefix) {
|
||||||
|
X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
|
||||||
|
} else {
|
||||||
|
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,15 +1316,21 @@ $script:standardAttributesByType = @{
|
|||||||
"Document" = @("Posted","Ref","DeletionMark","Date","Number")
|
"Document" = @("Posted","Ref","DeletionMark","Date","Number")
|
||||||
"Enum" = @("Order","Ref")
|
"Enum" = @("Order","Ref")
|
||||||
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
|
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
|
||||||
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period")
|
"AccumulationRegister" = @("RecordType","Active","LineNumber","Recorder","Period")
|
||||||
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account")
|
"AccountingRegister" = @("PeriodAdjustment","Account","RecordType","Active","LineNumber","Recorder","Period")
|
||||||
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry")
|
"CalculationRegister" = @("RegistrationPeriod","ReversingEntry","Active","EndOfBasePeriod","BegOfBasePeriod","EndOfActionPeriod","BegOfActionPeriod","ActionPeriod","CalculationType","LineNumber","Recorder")
|
||||||
"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
|
"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
|
||||||
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType")
|
"ChartOfCharacteristicTypes" = @("PredefinedDataName","ValueType","Description","Code","IsFolder","Parent","Predefined","DeletionMark","Ref")
|
||||||
"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
|
"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
|
||||||
"BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask")
|
"BusinessProcess" = @("Started","HeadTask","Completed","Ref","DeletionMark","Date","Number")
|
||||||
"Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess")
|
"Task" = @("Executed","Description","RoutePoint","BusinessProcess","Ref","DeletionMark","Date","Number")
|
||||||
"ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo")
|
# Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
|
||||||
|
# Условные члены перечислены в $script:stdAttrConditions — позицию они берут отсюда, а
|
||||||
|
# присутствие определяется свойствами объекта.
|
||||||
|
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
|
||||||
|
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
|
||||||
|
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
|
||||||
|
"ExchangePlan" = @("ThisNode","ReceivedNo","SentNo","Ref","DeletionMark","Description","Code")
|
||||||
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
|
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,6 +1449,55 @@ function Emit-StandardAttribute {
|
|||||||
# - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override.
|
# - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override.
|
||||||
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
|
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
|
||||||
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
|
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
|
||||||
|
|
||||||
|
# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а присутствие —
|
||||||
|
# из свойств самого объекта, как у платформы. Предикат ОБЯЗАН принимать определение параметром:
|
||||||
|
# scriptblock не видит $def вызывающей функции, и отказ был бы молчаливым.
|
||||||
|
$script:stdAttrConditions = @{
|
||||||
|
"AccountingRegister" = @{
|
||||||
|
"PeriodAdjustment" = { param($d) $v = 0; if ($null -ne $d.periodAdjustmentLength) { $v = [int]"$($d.periodAdjustmentLength)" }; $v -gt 0 }
|
||||||
|
"RecordType" = { param($d) -not ($d.correspondence -eq $true) }
|
||||||
|
}
|
||||||
|
"AccumulationRegister" = @{
|
||||||
|
"RecordType" = { param($d) $raw = if ($d.registerType) { "$($d.registerType)" } else { "Balance" }; (Normalize-EnumValue "RegisterType" $raw) -eq "Balance" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
|
||||||
|
# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся
|
||||||
|
# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но
|
||||||
|
# это однородность выборки, а не правило). Поэтому — шаблон, а не список.
|
||||||
|
$script:stdAttrTailPattern = @{
|
||||||
|
"AccountingRegister" = '^ExtDimension(Type)?\d+$'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Состав хвоста задаёт не DSL, а объект, на который регистр ссылается: пар субконто столько,
|
||||||
|
# сколько у плана счетов MaxExtDimensionCount. Читаем его из выгрузки — как версию формата из
|
||||||
|
# Configuration.xml, — чтобы регистр, описанный неполным DSL, совпал с тем, что материализует
|
||||||
|
# платформа. План не найден → хвост не генерируем и говорим об этом в выводе.
|
||||||
|
$script:stdAttrTailHint = $null
|
||||||
|
$script:stdAttrTailDerived = @{
|
||||||
|
"AccountingRegister" = {
|
||||||
|
param($d, $objectName, $outDir)
|
||||||
|
$ref = "$($d.chartOfAccounts)"
|
||||||
|
if (-not $ref) { return @() }
|
||||||
|
$chartName = $ref -replace '^.*\.', '' # ссылка вида ChartOfAccounts.X (имя объекта точек не содержит)
|
||||||
|
$path = Join-Path (Join-Path $outDir "ChartsOfAccounts") "$chartName.xml"
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
$script:stdAttrTailHint = "ChartOfAccounts '$chartName' not found in dump — ExtDimension pairs not generated (platform will add them on load)"
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$n = 0
|
||||||
|
if ([System.IO.File]::ReadAllText($path) -match '<MaxExtDimensionCount>(\d+)</MaxExtDimensionCount>') { $n = [int]$matches[1] }
|
||||||
|
$out = @()
|
||||||
|
for ($i = 1; $i -le $n; $i++) {
|
||||||
|
# ExtDimensionN связан с Account через LinkByType (LinkItem = номер), ExtDimensionTypeN — нет.
|
||||||
|
$out += @{ name = "ExtDimension$i"; ov = @{ LinkByType = @{ dataPath = "AccountingRegister.$objectName.StandardAttribute.Account"; linkItem = $i } } }
|
||||||
|
$out += @{ name = "ExtDimensionType$i"; ov = @{} }
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
}
|
||||||
function Emit-StandardAttributes {
|
function Emit-StandardAttributes {
|
||||||
param([string]$indent, [string]$objectType)
|
param([string]$indent, [string]$objectType)
|
||||||
$attrs = $script:standardAttributesByType[$objectType]
|
$attrs = $script:standardAttributesByType[$objectType]
|
||||||
@@ -1381,14 +1507,45 @@ function Emit-StandardAttributes {
|
|||||||
if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет
|
if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет
|
||||||
if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо)
|
if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо)
|
||||||
$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
|
$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
|
||||||
# Доп. (опциональные) стандартные реквизиты вне фикс-списка типа — напр. ExchangeDate у части ПланОбмена
|
# Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
|
||||||
# (легаси, присутствие не выводится из свойств). Эмитим по факту наличия ключа в DSL, ПЕРЕД фикс-списком (их позиция).
|
# их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
|
||||||
$extra = @()
|
# перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
|
||||||
if ($sa) { foreach ($k in $sa.PSObject.Properties.Name) { if ($attrs -notcontains $k) { $extra += $k } } }
|
# поэтому «условные скопом вперёд» не выражает канон.
|
||||||
|
$cond = $script:stdAttrConditions[$objectType]
|
||||||
|
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
|
||||||
|
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
|
||||||
|
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
|
||||||
|
# ExtDimensionTypeN (порядок платформы).
|
||||||
|
$tailRe = $script:stdAttrTailPattern[$objectType]
|
||||||
|
$extra = @(); $tail = @()
|
||||||
|
if ($sa) {
|
||||||
|
foreach ($k in $sa.PSObject.Properties.Name) {
|
||||||
|
if ($attrs -contains $k) { continue }
|
||||||
|
if ($tailRe -and $k -match $tailRe) { $tail += $k } else { $extra += $k }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Хвост, выведенный из связанного объекта: дополняет DSL, а не заменяет его — лишнее из DSL
|
||||||
|
# остаётся (прощаем), недостающее добавляется вместе со своими значениями по умолчанию.
|
||||||
|
$derivedOv = @{}
|
||||||
|
$gen = $script:stdAttrTailDerived[$objectType]
|
||||||
|
if ($gen) {
|
||||||
|
foreach ($e in @(& $gen $def $objName $OutputDir)) {
|
||||||
|
$derivedOv[$e.name] = $e.ov
|
||||||
|
if ($tail -notcontains $e.name) { $tail += $e.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tail = @($tail | Sort-Object @{e={[int]([regex]::Match($_, '\d+').Value)}}, @{e={ if ($_ -match 'Type\d+$') { 1 } else { 0 } }})
|
||||||
X "$indent<StandardAttributes>"
|
X "$indent<StandardAttributes>"
|
||||||
foreach ($a in ($extra + $attrs)) {
|
foreach ($a in ($extra + $attrs + $tail)) {
|
||||||
|
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
|
||||||
|
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
|
||||||
|
if ($cond -and $cond.ContainsKey($a)) {
|
||||||
|
$present = ($sa -and $sa.PSObject.Properties[$a]) -or (& $cond[$a] $def)
|
||||||
|
if (-not $present) { continue }
|
||||||
|
}
|
||||||
$ov = @{}
|
$ov = @{}
|
||||||
if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
|
if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
|
||||||
|
if ($derivedOv.ContainsKey($a)) { foreach ($k in $derivedOv[$a].Keys) { $ov[$k] = $derivedOv[$a][$k] } }
|
||||||
if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
|
if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
|
||||||
$d = $sa.$a
|
$d = $sa.$a
|
||||||
if ($d) {
|
if ($d) {
|
||||||
@@ -1946,9 +2103,12 @@ function Emit-Attribute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use — only for catalog top-level attributes
|
# Use — у реквизитов справочника и ПВХ. Позиция РАЗНАЯ: справочник пишет Use ПЕРЕД
|
||||||
|
# Indexing, ПВХ — ПОСЛЕ него (корпус acc+erp: Catalog `Use,Indexing,FullTextSearch`,
|
||||||
|
# ПВХ `Indexing,Use,FullTextSearch,DataHistory`). Отсюда отдельный контекст "cct":
|
||||||
|
# структурно реквизит ПВХ совпадает со справочником, расходится только этим порядком.
|
||||||
|
$use = if ($parsed.use) { $parsed.use } else { "ForItem" }
|
||||||
if ($context -eq "catalog") {
|
if ($context -eq "catalog") {
|
||||||
$use = if ($parsed.use) { $parsed.use } else { "ForItem" }
|
|
||||||
X "$indent`t`t<Use>$use</Use>"
|
X "$indent`t`t<Use>$use</Use>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1964,6 +2124,7 @@ function Emit-Attribute {
|
|||||||
if ($parsed.indexing) { $indexing = $parsed.indexing }
|
if ($parsed.indexing) { $indexing = $parsed.indexing }
|
||||||
X "$indent`t`t<Indexing>$indexing</Indexing>"
|
X "$indent`t`t<Indexing>$indexing</Indexing>"
|
||||||
}
|
}
|
||||||
|
if ($context -eq "cct") { X "$indent`t`t<Use>$use</Use>" }
|
||||||
|
|
||||||
# Реквизит адресации задачи: AddressingDimension (ссылка на измерение регистра исполнителей), между Indexing и FullTextSearch.
|
# Реквизит адресации задачи: AddressingDimension (ссылка на измерение регистра исполнителей), между Indexing и FullTextSearch.
|
||||||
if ($context -eq "task-addressing" -and $elemTag -eq "AddressingAttribute") {
|
if ($context -eq "task-addressing" -and $elemTag -eq "AddressingAttribute") {
|
||||||
@@ -2131,6 +2292,11 @@ function Emit-EnumValue {
|
|||||||
X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
|
X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
|
||||||
Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
|
Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
|
||||||
if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
|
if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
|
||||||
|
# Цвет значения перечисления — свойство формата 2.21 (8.5), последним в Properties.
|
||||||
|
if ($script:isFormat221) {
|
||||||
|
$color = if ($parsed.color) { "$($parsed.color)" } else { "auto" }
|
||||||
|
X "$indent`t`t<Color>$(Esc-XmlText $color)</Color>"
|
||||||
|
}
|
||||||
X "$indent`t</Properties>"
|
X "$indent`t</Properties>"
|
||||||
X "$indent</EnumValue>"
|
X "$indent</EnumValue>"
|
||||||
}
|
}
|
||||||
@@ -2797,6 +2963,11 @@ function Emit-CommonFormProperties {
|
|||||||
} else {
|
} else {
|
||||||
X "$i<UsePurposes/>"
|
X "$i<UsePurposes/>"
|
||||||
}
|
}
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# между UsePurposes и UseStandardCommands.
|
||||||
|
if ($script:isFormat221) {
|
||||||
|
X "$i<UseInInterfaceCompatibilityMode>$(Get-EnumProp 'UseInInterfaceCompatibilityMode' 'useInInterfaceCompatibilityMode' 'Any')</UseInInterfaceCompatibilityMode>"
|
||||||
|
}
|
||||||
$useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" }
|
$useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" }
|
||||||
X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
|
X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
|
||||||
Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
|
Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
|
||||||
@@ -3040,7 +3211,9 @@ function Emit-ScheduledJobProperties {
|
|||||||
if ($description) { X "$i<Description>$(Esc-XmlText $description)</Description>" } else { X "$i<Description/>" }
|
if ($description) { X "$i<Description>$(Esc-XmlText $description)</Description>" } else { X "$i<Description/>" }
|
||||||
|
|
||||||
$key = if ($def.key) { "$($def.key)" } else { "" }
|
$key = if ($def.key) { "$($def.key)" } else { "" }
|
||||||
X "$i<Key>$(Esc-XmlText $key)</Key>"
|
# Пустое значение → самозакрывающийся, как у <Description> выше: Конфигуратор
|
||||||
|
# не пишет пустых пар.
|
||||||
|
if ($key) { X "$i<Key>$(Esc-XmlText $key)</Key>" } else { X "$i<Key/>" }
|
||||||
|
|
||||||
$use = if ($def.use -eq $true) { "true" } else { "false" }
|
$use = if ($def.use -eq $true) { "true" } else { "false" }
|
||||||
X "$i<Use>$use</Use>"
|
X "$i<Use>$use</Use>"
|
||||||
@@ -3105,6 +3278,8 @@ function Emit-ReportProperties {
|
|||||||
Emit-VerbatimRef $i "DefaultSettingsForm" $def.defaultSettingsForm
|
Emit-VerbatimRef $i "DefaultSettingsForm" $def.defaultSettingsForm
|
||||||
Emit-VerbatimRef $i "AuxiliarySettingsForm" $def.auxiliarySettingsForm
|
Emit-VerbatimRef $i "AuxiliarySettingsForm" $def.auxiliarySettingsForm
|
||||||
Emit-VerbatimRef $i "DefaultVariantForm" $def.defaultVariantForm
|
Emit-VerbatimRef $i "DefaultVariantForm" $def.defaultVariantForm
|
||||||
|
# Вспомогательная форма варианта отчёта — свойство формата 2.21 (8.5).
|
||||||
|
if ($script:isFormat221) { Emit-VerbatimRef $i "AuxiliaryVariantForm" $def.auxiliaryVariantForm }
|
||||||
Emit-VerbatimRef $i "VariantsStorage" $def.variantsStorage
|
Emit-VerbatimRef $i "VariantsStorage" $def.variantsStorage
|
||||||
Emit-VerbatimRef $i "SettingsStorage" $def.settingsStorage
|
Emit-VerbatimRef $i "SettingsStorage" $def.settingsStorage
|
||||||
$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
|
$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
|
||||||
@@ -3840,7 +4015,8 @@ function Emit-WebServiceProperties {
|
|||||||
if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
|
if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
|
||||||
|
|
||||||
$namespace = if ($def.namespace) { "$($def.namespace)" } else { "" }
|
$namespace = if ($def.namespace) { "$($def.namespace)" } else { "" }
|
||||||
X "$i<Namespace>$(Esc-XmlText $namespace)</Namespace>"
|
# Пустое значение → самозакрывающийся, как у <Comment> выше.
|
||||||
|
if ($namespace) { X "$i<Namespace>$(Esc-XmlText $namespace)</Namespace>" } else { X "$i<Namespace/>" }
|
||||||
|
|
||||||
# XDTOPackages — СПИСОК элементов, а не скаляр: значение либо ссылка на пакет конфигурации
|
# XDTOPackages — СПИСОК элементов, а не скаляр: значение либо ссылка на пакет конфигурации
|
||||||
# (xr:MDObjectRef "XDTOPackage.Имя"), либо URI внешнего пространства имён (xs:string).
|
# (xr:MDObjectRef "XDTOPackage.Имя"), либо URI внешнего пространства имён (xs:string).
|
||||||
@@ -4082,6 +4258,14 @@ $script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
@@ -4143,6 +4327,15 @@ $script:compatMode = Detect-CompatibilityMode $OutputDir
|
|||||||
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
|
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
|
||||||
$script:isFormat218 = (Get-FormatRank $script:formatVersion) -ge 218
|
$script:isFormat218 = (Get-FormatRank $script:formatVersion) -ge 218
|
||||||
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
|
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
|
||||||
|
$script:isFormat221 = (Get-FormatRank $script:formatVersion) -ge 221
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту.
|
||||||
|
# Только для шапок MetaDataObject и Form — в файлах с корнем extrnprops
|
||||||
|
# (Ext/ClientApplicationInterface.xml и т.п.) платформа его не пишет.
|
||||||
|
if ($script:isFormat221) {
|
||||||
|
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', "$palNs xmlns:style="
|
||||||
|
}
|
||||||
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
||||||
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }
|
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }
|
||||||
|
|
||||||
@@ -4268,7 +4461,7 @@ if ($objType -in $typesWithAttrTS) {
|
|||||||
"Catalog" { "catalog" }
|
"Catalog" { "catalog" }
|
||||||
"Document" { "document" }
|
"Document" { "document" }
|
||||||
{ $_ -in @("DataProcessor","Report") } { "processor" }
|
{ $_ -in @("DataProcessor","Report") } { "processor" }
|
||||||
"ChartOfCharacteristicTypes" { "catalog" } # реквизиты ПВХ структурно как у справочника (Use/FillFromFillingValue/DataHistory)
|
"ChartOfCharacteristicTypes" { "cct" } # как catalog (Use/FillFromFillingValue/DataHistory), но Use ПОСЛЕ Indexing
|
||||||
{ $_ -in @("ChartOfAccounts","ChartOfCalculationTypes") } { "account" } # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
|
{ $_ -in @("ChartOfAccounts","ChartOfCalculationTypes") } { "account" } # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
|
||||||
default { "object" }
|
default { "object" }
|
||||||
}
|
}
|
||||||
@@ -4356,16 +4549,31 @@ if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegis
|
|||||||
$regCtx = switch ($objType) { "InformationRegister" { "register-info" } "CalculationRegister" { "register-calc" } default { "register-other" } }
|
$regCtx = switch ($objType) { "InformationRegister" { "register-info" } "CalculationRegister" { "register-calc" } default { "register-other" } }
|
||||||
# Все семейства регистров: ресурсы/измерения — через богатый Emit-Attribute (общий слой object-свойств).
|
# Все семейства регистров: ресурсы/измерения — через богатый Emit-Attribute (общий слой object-свойств).
|
||||||
$dimResCtx = switch ($objType) { "InformationRegister" { "register-info" } "AccumulationRegister" { "register-accum" } "CalculationRegister" { "register-calc" } "AccountingRegister" { "register-account" } default { $null } }
|
$dimResCtx = switch ($objType) { "InformationRegister" { "register-info" } "AccumulationRegister" { "register-accum" } "CalculationRegister" { "register-calc" } "AccountingRegister" { "register-account" } default { $null } }
|
||||||
foreach ($r in $resources) {
|
# Порядок видов детей — канон выгрузки, снят с корпуса (acc+erp, разброса внутри
|
||||||
if ($dimResCtx) { Emit-Attribute "`t`t`t" $r $dimResCtx "Resource" }
|
# типа нет): у большинства регистров Resource, Attribute, Dimension, а у
|
||||||
else { Emit-Resource "`t`t`t" $r $objType }
|
# бухгалтерского — Dimension, Resource, Attribute. Команды у платформы идут
|
||||||
}
|
# последними, как и здесь.
|
||||||
foreach ($d in $dims) {
|
$kindOrder = if ($objType -eq "AccountingRegister") { @('dim','res','attr') } else { @('res','attr','dim') }
|
||||||
if ($dimResCtx) { Emit-Attribute "`t`t`t" $d $dimResCtx "Dimension" }
|
foreach ($kind in $kindOrder) {
|
||||||
else { Emit-Dimension "`t`t`t" $d $objType }
|
switch ($kind) {
|
||||||
}
|
'res' {
|
||||||
foreach ($a in $regAttrs) {
|
foreach ($r in $resources) {
|
||||||
Emit-Attribute "`t`t`t" $a $regCtx
|
if ($dimResCtx) { Emit-Attribute "`t`t`t" $r $dimResCtx "Resource" }
|
||||||
|
else { Emit-Resource "`t`t`t" $r $objType }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'dim' {
|
||||||
|
foreach ($d in $dims) {
|
||||||
|
if ($dimResCtx) { Emit-Attribute "`t`t`t" $d $dimResCtx "Dimension" }
|
||||||
|
else { Emit-Dimension "`t`t`t" $d $objType }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'attr' {
|
||||||
|
foreach ($a in $regAttrs) {
|
||||||
|
Emit-Attribute "`t`t`t" $a $regCtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
foreach ($cmd in $regCommands) {
|
foreach ($cmd in $regCommands) {
|
||||||
Emit-Command "`t`t`t" $cmd.name $cmd.def
|
Emit-Command "`t`t`t" $cmd.name $cmd.def
|
||||||
@@ -4609,9 +4817,13 @@ function Build-PredefinedXml {
|
|||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
|
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
|
||||||
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n")
|
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n")
|
||||||
foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType }
|
# Шапка Predefined.xml не объявляет cfg (predef/v8/xr/xs/xsi) — на время сборки этого
|
||||||
|
# файла ссылочный тип уходит на локальную форму, как делает и платформа.
|
||||||
|
$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
|
||||||
|
try { foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType } }
|
||||||
|
finally { $script:cfgPrefix = $savedCfgPrefix }
|
||||||
[void]$sb.Append("</PredefinedData>`n")
|
[void]$sb.Append("</PredefinedData>`n")
|
||||||
return $sb.ToString()
|
return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
|
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
|
||||||
@@ -4697,9 +4909,12 @@ function Build-PredefinedAccountXml {
|
|||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
|
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
|
||||||
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"ChartOfAccountsPredefinedItems`" version=`"$($script:formatVersion)`">`n")
|
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"ChartOfAccountsPredefinedItems`" version=`"$($script:formatVersion)`">`n")
|
||||||
foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef }
|
# См. Build-PredefinedXml: шапка этого файла cfg не объявляет.
|
||||||
|
$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
|
||||||
|
try { foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef } }
|
||||||
|
finally { $script:cfgPrefix = $savedCfgPrefix }
|
||||||
[void]$sb.Append("</PredefinedData>`n")
|
[void]$sb.Append("</PredefinedData>`n")
|
||||||
return $sb.ToString()
|
return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]"
|
# --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]"
|
||||||
@@ -4726,7 +4941,7 @@ function Build-PredefinedCalcTypeXml {
|
|||||||
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"CalculationTypePredefinedItems`" version=`"$($script:formatVersion)`">`n")
|
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"CalculationTypePredefinedItems`" version=`"$($script:formatVersion)`">`n")
|
||||||
foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" }
|
foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" }
|
||||||
[void]$sb.Append("</PredefinedData>`n")
|
[void]$sb.Append("</PredefinedData>`n")
|
||||||
return $sb.ToString()
|
return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
$extDir = Join-Path $objSubDir "Ext"
|
$extDir = Join-Path $objSubDir "Ext"
|
||||||
@@ -4741,7 +4956,20 @@ if ($objType -notin $typesNoSubDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($mainXmlPath, $metadataXml, $enc)
|
|
||||||
|
# Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла —
|
||||||
|
# последний байт `>`; сборка через AppendLine добавляла лишний.
|
||||||
|
# KeepEol в имени — отличие от одноимённой функции в скелетных навыках
|
||||||
|
# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать
|
||||||
|
# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос,
|
||||||
|
# синоним, значение заполнения), и сплошная нормализация меняла бы содержимое.
|
||||||
|
# Разделители тут и так CRLF: документ собран через AppendLine.
|
||||||
|
# Модули .bsl сюда НЕ идут: у них свой хвост.
|
||||||
|
function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
|
||||||
|
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc
|
||||||
|
|
||||||
# Module files
|
# Module files
|
||||||
$modulesCreated = @()
|
$modulesCreated = @()
|
||||||
@@ -4822,8 +5050,10 @@ if ($objType -eq "CommonForm") {
|
|||||||
$cfFormXmlPath = Join-Path $extDir "Form.xml"
|
$cfFormXmlPath = Join-Path $extDir "Form.xml"
|
||||||
if (-not (Test-Path $cfFormXmlPath)) {
|
if (-not (Test-Path $cfFormXmlPath)) {
|
||||||
$cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
$cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
$cfFormXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n<Form $cfFormNs version=`"$($script:formatVersion)`">`n`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">`n`t`t<Autofill>true</Autofill>`n`t</AutoCommandBar>`n`t<ChildItems/>`n</Form>`n"
|
# Шапка Form на 2.21 тоже несёт палитру — см. комментарий у $script:xmlnsDecl.
|
||||||
[System.IO.File]::WriteAllText($cfFormXmlPath, $cfFormXml, $enc)
|
if ($script:isFormat221) { $cfFormNs = $cfFormNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' }
|
||||||
|
$cfFormXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Form $cfFormNs version=`"$($script:formatVersion)`">`r`n`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">`r`n`t`t<Autofill>true</Autofill>`r`n`t</AutoCommandBar>`r`n`t<ChildItems/>`r`n</Form>`r`n"
|
||||||
|
Write-XmlFileKeepEol $cfFormXmlPath $cfFormXml $enc
|
||||||
$modulesCreated += $cfFormXmlPath
|
$modulesCreated += $cfFormXmlPath
|
||||||
}
|
}
|
||||||
$cfModuleDir = Join-Path $extDir "Form"
|
$cfModuleDir = Join-Path $extDir "Form"
|
||||||
@@ -4876,7 +5106,7 @@ if ($objType -eq "ExchangePlan") {
|
|||||||
[void]$sbC.Append("`t</Item>`r`n")
|
[void]$sbC.Append("`t</Item>`r`n")
|
||||||
}
|
}
|
||||||
[void]$sbC.Append("</ExchangePlanContent>`r`n")
|
[void]$sbC.Append("</ExchangePlanContent>`r`n")
|
||||||
[System.IO.File]::WriteAllText($contentPath, $sbC.ToString(), $enc)
|
Write-XmlFileKeepEol $contentPath $sbC.ToString() $enc
|
||||||
$modulesCreated += $contentPath
|
$modulesCreated += $contentPath
|
||||||
} elseif (-not (Test-Path $contentPath)) {
|
} elseif (-not (Test-Path $contentPath)) {
|
||||||
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
|
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
|
||||||
@@ -4884,7 +5114,7 @@ if ($objType -eq "ExchangePlan") {
|
|||||||
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
|
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
|
||||||
Ensure-ExtDir
|
Ensure-ExtDir
|
||||||
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`"/>`r`n"
|
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`"/>`r`n"
|
||||||
[System.IO.File]::WriteAllText($contentPath, $contentXml, $enc)
|
Write-XmlFileKeepEol $contentPath $contentXml $enc
|
||||||
$modulesCreated += $contentPath
|
$modulesCreated += $contentPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4893,7 +5123,7 @@ if ($objType -eq "BusinessProcess") {
|
|||||||
if (-not (Test-Path $flowchartPath)) {
|
if (-not (Test-Path $flowchartPath)) {
|
||||||
Ensure-ExtDir
|
Ensure-ExtDir
|
||||||
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
|
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
|
||||||
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc)
|
Write-XmlFileKeepEol $flowchartPath $flowchartXml $enc
|
||||||
$modulesCreated += $flowchartPath
|
$modulesCreated += $flowchartPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4908,20 +5138,20 @@ if ($objType -eq 'ChartOfAccounts' -and $def.predefined -and @($def.predefined).
|
|||||||
$edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' }
|
$edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' }
|
||||||
$predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef
|
$predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef
|
||||||
$predefPath = Join-Path $extDir "Predefined.xml"
|
$predefPath = Join-Path $extDir "Predefined.xml"
|
||||||
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc)
|
Write-XmlFileKeepEol $predefPath $predefXml $enc
|
||||||
$modulesCreated += $predefPath
|
$modulesCreated += $predefPath
|
||||||
} elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) {
|
} elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) {
|
||||||
Ensure-ExtDir
|
Ensure-ExtDir
|
||||||
$predefXml = Build-PredefinedCalcTypeXml @($def.predefined)
|
$predefXml = Build-PredefinedCalcTypeXml @($def.predefined)
|
||||||
$predefPath = Join-Path $extDir "Predefined.xml"
|
$predefPath = Join-Path $extDir "Predefined.xml"
|
||||||
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc)
|
Write-XmlFileKeepEol $predefPath $predefXml $enc
|
||||||
$modulesCreated += $predefPath
|
$modulesCreated += $predefPath
|
||||||
} elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) {
|
} elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) {
|
||||||
Ensure-ExtDir
|
Ensure-ExtDir
|
||||||
$catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' }
|
$catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' }
|
||||||
$predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType
|
$predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType
|
||||||
$predefPath = Join-Path $extDir "Predefined.xml"
|
$predefPath = Join-Path $extDir "Predefined.xml"
|
||||||
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc)
|
Write-XmlFileKeepEol $predefPath $predefXml $enc
|
||||||
$modulesCreated += $predefPath
|
$modulesCreated += $predefPath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4990,15 +5220,31 @@ if (Test-Path $configXmlPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save
|
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
|
||||||
|
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
|
||||||
|
# `encoding="UTF-8"` и `<a/>`.
|
||||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
$cfgSettings.Indent = $false
|
$cfgSettings.Indent = $false
|
||||||
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create)
|
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings)
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
|
||||||
$configDoc.Save($writer)
|
$configDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
|
||||||
|
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
$regResult = "added"
|
$regResult = "added"
|
||||||
}
|
}
|
||||||
@@ -5059,6 +5305,9 @@ switch ($regResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Cross-reference hints
|
# Cross-reference hints
|
||||||
|
if ($script:stdAttrTailHint) {
|
||||||
|
Write-Host "[HINT] $($script:stdAttrTailHint)"
|
||||||
|
}
|
||||||
if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) {
|
if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) {
|
||||||
Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:"
|
Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:"
|
||||||
Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
|
Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# meta-compile v1.76 — Compile 1C metadata object from JSON
|
# meta-compile v1.94 — Compile 1C metadata object from JSON
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -16,6 +16,69 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Регистронезависимый ввод — паритет с PS1. В PowerShell регистр не значим нигде, куда
|
||||||
|
# смотрит пользовательский ввод: свойства объекта из ConvertFrom-Json, ключи Hashtable,
|
||||||
|
# -eq/-contains, имена параметров, ValidateSet. В Python совпадение точное, поэтому порт
|
||||||
|
# молча терял свойства DSL, написанные в другом регистре. Обёртки ниже выравнивают поведение.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
@@ -206,9 +269,22 @@ def new_uuid():
|
|||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file_keep_eol(path, content):
|
||||||
|
# Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла —
|
||||||
|
# последний байт `>`.
|
||||||
|
# keep_eol в имени — отличие от одноимённой функции в скелетных навыках
|
||||||
|
# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать
|
||||||
|
# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос,
|
||||||
|
# синоним, значение заполнения). Разделители и так CRLF — их даёт join строк.
|
||||||
|
# Модули .bsl сюда НЕ идут: у них свой хвост.
|
||||||
|
write_utf8_bom(path, content.rstrip('\r\n'))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# XML builder (lines list)
|
# XML builder (lines list)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -289,7 +365,7 @@ def split_camel_case(name):
|
|||||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||||
parser.add_argument('-JsonPath', required=True)
|
parser.add_argument('-JsonPath', required=True)
|
||||||
parser.add_argument('-OutputDir', required=True)
|
parser.add_argument('-OutputDir', required=True)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
json_path = args.JsonPath
|
json_path = args.JsonPath
|
||||||
output_dir = args.OutputDir
|
output_dir = args.OutputDir
|
||||||
@@ -301,7 +377,7 @@ if not os.path.isfile(json_path):
|
|||||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
||||||
json_text = f.read()
|
json_text = f.read()
|
||||||
|
|
||||||
defn = json.loads(json_text)
|
defn = ci_json(json.loads(json_text))
|
||||||
|
|
||||||
assert_edit_allowed(output_dir, "editable")
|
assert_edit_allowed(output_dir, "editable")
|
||||||
|
|
||||||
@@ -395,6 +471,10 @@ enum_value_aliases = {
|
|||||||
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
|
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Словари, по которым ищут ПОЛЬЗОВАТЕЛЬСКИЙ ввод, — регистронезависимы, как хеш-таблицы PS1.
|
||||||
|
object_type_synonyms = CIDict(object_type_synonyms)
|
||||||
|
enum_value_aliases = CIDict(enum_value_aliases)
|
||||||
|
|
||||||
# Valid enum values per property (from meta-validate)
|
# Valid enum values per property (from meta-validate)
|
||||||
valid_enum_values = {
|
valid_enum_values = {
|
||||||
'RegisterType': ['Balance', 'Turnovers'],
|
'RegisterType': ['Balance', 'Turnovers'],
|
||||||
@@ -553,6 +633,8 @@ valid_types = [
|
|||||||
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
|
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
|
||||||
'CommonPicture', 'CommonTemplate',
|
'CommonPicture', 'CommonTemplate',
|
||||||
]
|
]
|
||||||
|
# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка
|
||||||
|
obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type)
|
||||||
if obj_type not in valid_types:
|
if obj_type not in valid_types:
|
||||||
print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr)
|
print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -627,47 +709,103 @@ type_namespace_map = {
|
|||||||
"SpreadsheetDocument": {"ns": "http://v8.1c.ru/8.2/data/spreadsheet", "prefix": "mxl"},
|
"SpreadsheetDocument": {"ns": "http://v8.1c.ru/8.2/data/spreadsheet", "prefix": "mxl"},
|
||||||
}
|
}
|
||||||
# Типы current-config пространства (cfg:, объявлено в корне): голые и объектные. Ссылочные — отдельно (d5p1).
|
# Типы current-config пространства (cfg:, объявлено в корне): голые и объектные. Ссылочные — отдельно (d5p1).
|
||||||
|
# Префикс current-config для ссылочных типов. 'cfg' — для файлов, чья шапка его объявляет
|
||||||
|
# (объектный XML, Ext/Form.xml общей формы). None на время сборки Ext/Predefined.xml, чья
|
||||||
|
# шапка его НЕ объявляет: там и платформа уходит на локальное объявление.
|
||||||
|
cfg_prefix = 'cfg'
|
||||||
cfg_bare_types = {"ConstantsSet", "ReportBuilder", "FilterCriterion"}
|
cfg_bare_types = {"ConstantsSet", "ReportBuilder", "FilterCriterion"}
|
||||||
cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ChartOfCalculationTypes", "ExchangePlan", "BusinessProcess", "Task", "InformationRegister",
|
"ChartOfCalculationTypes", "ExchangePlan", "BusinessProcess", "Task", "InformationRegister",
|
||||||
"AccumulationRegister", "AccountingRegister", "CalculationRegister", "DataProcessor", "Report",
|
"AccumulationRegister", "AccountingRegister", "CalculationRegister", "DataProcessor", "Report",
|
||||||
"DocumentJournal", "Constant", "ConstantValue", "Sequence", "Recalculation"}
|
"DocumentJournal", "Constant", "ConstantValue", "Sequence", "Recalculation"}
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
type_synonyms = CIDict(type_synonyms)
|
||||||
|
TYPE_SYNONYMS = type_synonyms
|
||||||
|
|
||||||
|
|
||||||
def resolve_type_str(type_str):
|
def resolve_type_str(type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return type_str
|
return type_str
|
||||||
# Parameterized types: Number(15,2), Строка(100), etc.
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if type_str.startswith('cfg:'):
|
||||||
|
type_str = type_str[4:]
|
||||||
|
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
|
||||||
|
type_str = type_str[type_str.index(':') + 1:]
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
||||||
if m:
|
if m:
|
||||||
base_name = m.group(1).strip()
|
base_name = m.group(1).strip()
|
||||||
params = m.group(2)
|
params = m.group(2)
|
||||||
resolved = type_synonyms.get(base_name.lower())
|
resolved = TYPE_SYNONYMS.get(base_name.lower())
|
||||||
if resolved:
|
if resolved:
|
||||||
return f'{resolved}({params})'
|
return f'{resolved}({params})'
|
||||||
return type_str
|
return type_str
|
||||||
# Reference types: СправочникСсылка.Организации -> CatalogRef.Организации
|
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
|
||||||
if '.' in type_str:
|
if '.' in type_str:
|
||||||
dot_idx = type_str.index('.')
|
dot_idx = type_str.index('.')
|
||||||
prefix = type_str[:dot_idx]
|
prefix = type_str[:dot_idx]
|
||||||
suffix = type_str[dot_idx:] # includes the dot
|
suffix = type_str[dot_idx:] # includes the dot
|
||||||
resolved = type_synonyms.get(prefix.lower())
|
resolved = TYPE_SYNONYMS.get(prefix.lower())
|
||||||
if resolved:
|
if resolved:
|
||||||
return f'{resolved}{suffix}'
|
return f'{resolved}{suffix}'
|
||||||
return type_str
|
return type_str
|
||||||
# Simple name lookup
|
# Простое имя
|
||||||
resolved = type_synonyms.get(type_str.lower())
|
resolved = TYPE_SYNONYMS.get(type_str.lower())
|
||||||
if resolved:
|
if resolved:
|
||||||
return resolved
|
return resolved
|
||||||
return type_str
|
return type_str
|
||||||
|
|
||||||
def emit_type_content(indent, type_str):
|
def emit_type_content(indent, type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return
|
return
|
||||||
# Composite type: "Type1 + Type2 + Type3"
|
# Composite type: "Type1 + Type2 + Type3"
|
||||||
|
# Платформа пишет сначала ВСЕ <v8:Type>/<v8:TypeSet>, и только потом блоки
|
||||||
|
# квалификаторов — а рекурсия ниже печатала бы каждую часть целиком (тип вместе со
|
||||||
|
# своими квалификаторами). На одиночном типе оба порядка совпадают, поэтому
|
||||||
|
# расхождение вылезало только на составном.
|
||||||
|
# Порядок самих блоков квалификаторов тоже канонический и НЕ зеркалит порядок типов:
|
||||||
|
# Number, String, Date (корпус acc+erp, контрпримеров нет — при типах
|
||||||
|
# boolean,string,dateTime,decimal квалификаторы идут Number,String,Date).
|
||||||
|
# Порядок типов при этом сохраняем как в DSL: он и есть порядок источника.
|
||||||
if ' + ' in type_str:
|
if ' + ' in type_str:
|
||||||
parts = [p.strip() for p in type_str.split('+')]
|
parts = [p.strip() for p in type_str.split('+')]
|
||||||
|
type_lines = []
|
||||||
|
qual_blocks = {}
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
# X добавляет в список lines, поэтому «перехват» — это срез и откат хвоста.
|
||||||
|
# В PS-порту X пишет в StringBuilder и тот же алгоритм выражен через
|
||||||
|
# Length/Remove — различие рантаймов, не логики.
|
||||||
|
before = len(lines)
|
||||||
emit_type_content(indent, part)
|
emit_type_content(indent, part)
|
||||||
|
chunk = lines[before:]
|
||||||
|
del lines[before:]
|
||||||
|
cur_qual = None
|
||||||
|
for line in chunk:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = re.search(r'<v8:(String|Number|Date)Qualifiers>', line)
|
||||||
|
if m:
|
||||||
|
cur_qual = m.group(1)
|
||||||
|
qual_blocks[cur_qual] = []
|
||||||
|
if cur_qual:
|
||||||
|
qual_blocks[cur_qual].append(line)
|
||||||
|
if re.search(r'</v8:(String|Number|Date)Qualifiers>', line):
|
||||||
|
cur_qual = None
|
||||||
|
else:
|
||||||
|
type_lines.append(line)
|
||||||
|
for line in type_lines:
|
||||||
|
X(line)
|
||||||
|
for q in ('Number', 'String', 'Date'):
|
||||||
|
if q in qual_blocks:
|
||||||
|
for line in qual_blocks[q]:
|
||||||
|
X(line)
|
||||||
return
|
return
|
||||||
type_str = resolve_type_str(type_str)
|
type_str = resolve_type_str(type_str)
|
||||||
# Boolean
|
# Boolean
|
||||||
@@ -760,10 +898,22 @@ def emit_type_content(indent, type_str):
|
|||||||
X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>')
|
X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Reference types — use local xmlns declaration for 1C compatibility
|
# Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
|
||||||
|
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке: формально
|
||||||
|
# эквивалентно (значим URI, не префикс) и платформой принималось, но первый же
|
||||||
|
# цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип в cfg: —
|
||||||
|
# то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
|
||||||
|
# действительно не работает; в метаданных такого ограничения нет.
|
||||||
|
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. type_namespace_map.
|
||||||
|
# cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||||
|
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||||
|
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||||
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
|
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
|
||||||
if m:
|
if m:
|
||||||
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
|
if cfg_prefix:
|
||||||
|
X(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
|
||||||
|
else:
|
||||||
|
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
|
||||||
return
|
return
|
||||||
# Fallback
|
# Fallback
|
||||||
X(f'{indent}<v8:Type>{type_str}</v8:Type>')
|
X(f'{indent}<v8:Type>{type_str}</v8:Type>')
|
||||||
@@ -1270,15 +1420,24 @@ standard_attributes_by_type = {
|
|||||||
'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'],
|
'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'],
|
||||||
'Enum': ['Order', 'Ref'],
|
'Enum': ['Order', 'Ref'],
|
||||||
'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
|
'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
|
||||||
'AccumulationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
|
'AccumulationRegister': ['RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
|
||||||
'AccountingRegister': ['Active', 'Period', 'Recorder', 'LineNumber', 'Account'],
|
'AccountingRegister': ['PeriodAdjustment', 'Account', 'RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
|
||||||
'CalculationRegister': ['Active', 'Recorder', 'LineNumber', 'RegistrationPeriod', 'CalculationType', 'ReversingEntry'],
|
'CalculationRegister': ['RegistrationPeriod', 'ReversingEntry', 'Active', 'EndOfBasePeriod', 'BegOfBasePeriod', 'EndOfActionPeriod', 'BegOfActionPeriod', 'ActionPeriod', 'CalculationType', 'LineNumber', 'Recorder'],
|
||||||
'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
|
'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
|
||||||
'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'],
|
'ChartOfCharacteristicTypes': ['PredefinedDataName', 'ValueType', 'Description', 'Code', 'IsFolder', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
|
||||||
'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'],
|
'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'],
|
||||||
'BusinessProcess': ['Ref', 'DeletionMark', 'Date', 'Number', 'Started', 'Completed', 'HeadTask'],
|
# Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
|
||||||
'Task': ['Ref', 'DeletionMark', 'Date', 'Number', 'Executed', 'Description', 'RoutePoint', 'BusinessProcess'],
|
# Условные члены перечислены в std_attr_conditions — позицию они берут отсюда, а
|
||||||
'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'],
|
# присутствие определяется свойствами объекта.
|
||||||
|
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
|
||||||
|
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
|
||||||
|
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
|
||||||
|
'BusinessProcess': ['Started', 'HeadTask', 'Completed', 'Ref', 'DeletionMark', 'Date', 'Number'],
|
||||||
|
'Task': ['Executed', 'Description', 'RoutePoint', 'BusinessProcess', 'Ref', 'DeletionMark', 'Date', 'Number'],
|
||||||
|
# Порядок снят с выгрузки: у плана обмена блок начинается с ThisNode, а не с Ref
|
||||||
|
# (acc+erp, 8 объектов, разброса нет). Прочие типы в этой таблице совпадают с
|
||||||
|
# платформой — расхождений порядка по ним корпусный раундтрип не показал.
|
||||||
|
'ExchangePlan': ['ThisNode', 'ReceivedNo', 'SentNo', 'Ref', 'DeletionMark', 'Description', 'Code'],
|
||||||
'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'],
|
'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,6 +1557,64 @@ def emit_standard_attribute(indent, attr_name, ov=None):
|
|||||||
# std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes).
|
# std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes).
|
||||||
# Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты.
|
# Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты.
|
||||||
std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'}
|
std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'}
|
||||||
|
|
||||||
|
# Условные члены списка типа: позиция берётся из standard_attributes_by_type, а присутствие —
|
||||||
|
# из свойств самого объекта, как у платформы. Предикат принимает определение параметром
|
||||||
|
# (зеркало .ps1, где scriptblock не видит $def вызывающей функции).
|
||||||
|
def _period_adjustment_used(d):
|
||||||
|
v = d.get('periodAdjustmentLength')
|
||||||
|
return v is not None and int(str(v)) > 0
|
||||||
|
|
||||||
|
std_attr_conditions = {
|
||||||
|
'AccountingRegister': {
|
||||||
|
'PeriodAdjustment': _period_adjustment_used,
|
||||||
|
'RecordType': lambda d: d.get('correspondence') is not True,
|
||||||
|
},
|
||||||
|
'AccumulationRegister': {
|
||||||
|
'RecordType': lambda d: normalize_enum_value('RegisterType', str(d.get('registerType') or 'Balance')) == 'Balance',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
|
||||||
|
# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся
|
||||||
|
# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но
|
||||||
|
# это однородность выборки, а не правило). Поэтому — шаблон, а не список.
|
||||||
|
std_attr_tail_pattern = {
|
||||||
|
'AccountingRegister': r'^ExtDimension(Type)?\d+$',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Состав хвоста задаёт не DSL, а объект, на который регистр ссылается: пар субконто столько,
|
||||||
|
# сколько у плана счетов MaxExtDimensionCount. Читаем его из выгрузки — как версию формата из
|
||||||
|
# Configuration.xml, — чтобы регистр, описанный неполным DSL, совпал с тем, что материализует
|
||||||
|
# платформа. План не найден → хвост не генерируем и говорим об этом в выводе.
|
||||||
|
std_attr_tail_hint = None
|
||||||
|
|
||||||
|
def _acc_register_ext_dimension_tail(d, object_name, out_dir):
|
||||||
|
global std_attr_tail_hint
|
||||||
|
ref = str(d.get('chartOfAccounts') or '')
|
||||||
|
if not ref:
|
||||||
|
return []
|
||||||
|
chart_name = re.sub(r'^.*\.', '', ref) # ссылка вида ChartOfAccounts.X (имя объекта точек не содержит)
|
||||||
|
path = os.path.join(out_dir, 'ChartsOfAccounts', chart_name + '.xml')
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
std_attr_tail_hint = ("ChartOfAccounts '%s' not found in dump — ExtDimension pairs not generated "
|
||||||
|
"(platform will add them on load)" % chart_name)
|
||||||
|
return []
|
||||||
|
with open(path, encoding='utf-8-sig') as f:
|
||||||
|
m = re.search(r'<MaxExtDimensionCount>(\d+)</MaxExtDimensionCount>', f.read())
|
||||||
|
n = int(m.group(1)) if m else 0
|
||||||
|
out = []
|
||||||
|
for i in range(1, n + 1):
|
||||||
|
# ExtDimensionN связан с Account через LinkByType (LinkItem = номер), ExtDimensionTypeN — нет.
|
||||||
|
out.append(('ExtDimension%d' % i,
|
||||||
|
{'LinkByType': {'dataPath': 'AccountingRegister.%s.StandardAttribute.Account' % object_name,
|
||||||
|
'linkItem': i}}))
|
||||||
|
out.append(('ExtDimensionType%d' % i, {}))
|
||||||
|
return out
|
||||||
|
|
||||||
|
std_attr_tail_derived = {
|
||||||
|
'AccountingRegister': _acc_register_ext_dimension_tail,
|
||||||
|
}
|
||||||
def emit_standard_attributes(indent, object_type):
|
def emit_standard_attributes(indent, object_type):
|
||||||
attrs = standard_attributes_by_type.get(object_type)
|
attrs = standard_attributes_by_type.get(object_type)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
@@ -1409,12 +1626,45 @@ def emit_standard_attributes(indent, object_type):
|
|||||||
if isinstance(sa, str) and sa == '':
|
if isinstance(sa, str) and sa == '':
|
||||||
return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок)
|
return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок)
|
||||||
profile = std_attr_profile.get(object_type, {})
|
profile = std_attr_profile.get(object_type, {})
|
||||||
# Доп. (опциональные) стандартные реквизиты вне фикс-списка — напр. ExchangeDate у части ПланОбмена
|
# Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
|
||||||
# (легаси, присутствие не выводится). Эмитим по факту ключа в DSL, ПЕРЕД фикс-списком (их позиция).
|
# их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
|
||||||
extra = [k for k in sa if k not in attrs] if isinstance(sa, dict) else []
|
# перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
|
||||||
|
# поэтому «условные скопом вперёд» не выражает канон.
|
||||||
|
cond = std_attr_conditions.get(object_type)
|
||||||
|
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
|
||||||
|
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
|
||||||
|
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
|
||||||
|
# ExtDimensionTypeN (порядок платформы).
|
||||||
|
tail_re = std_attr_tail_pattern.get(object_type)
|
||||||
|
extra, tail = [], []
|
||||||
|
if isinstance(sa, dict):
|
||||||
|
for k in sa:
|
||||||
|
if k in attrs:
|
||||||
|
continue
|
||||||
|
if tail_re and re.match(tail_re, k):
|
||||||
|
tail.append(k)
|
||||||
|
else:
|
||||||
|
extra.append(k)
|
||||||
|
# Хвост, выведенный из связанного объекта: дополняет DSL, а не заменяет его — лишнее из DSL
|
||||||
|
# остаётся (прощаем), недостающее добавляется вместе со своими значениями по умолчанию.
|
||||||
|
derived_ov = {}
|
||||||
|
gen = std_attr_tail_derived.get(object_type)
|
||||||
|
if gen:
|
||||||
|
for name, dov in gen(defn, obj_name, output_dir):
|
||||||
|
derived_ov[name] = dov
|
||||||
|
if name not in tail:
|
||||||
|
tail.append(name)
|
||||||
|
tail.sort(key=lambda k: (int(re.search(r'\d+', k).group()), 1 if re.search(r'Type\d+$', k) else 0))
|
||||||
X(f'{indent}<StandardAttributes>')
|
X(f'{indent}<StandardAttributes>')
|
||||||
for a in extra + list(attrs):
|
for a in extra + list(attrs) + tail:
|
||||||
|
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
|
||||||
|
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
|
||||||
|
if cond and a in cond:
|
||||||
|
present = (isinstance(sa, dict) and a in sa) or cond[a](defn)
|
||||||
|
if not present:
|
||||||
|
continue
|
||||||
ov = dict(profile.get(a, {}))
|
ov = dict(profile.get(a, {}))
|
||||||
|
ov.update(derived_ov.get(a, {}))
|
||||||
if isinstance(sa, dict):
|
if isinstance(sa, dict):
|
||||||
d = sa.get(a)
|
d = sa.get(a)
|
||||||
if d:
|
if d:
|
||||||
@@ -2016,8 +2266,13 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
|||||||
X(f'{indent}\t\t<ExtDimensionAccountingFlag>{esc_xml_text(str(parsed["extDimensionAccountingFlag"]))}</ExtDimensionAccountingFlag>')
|
X(f'{indent}\t\t<ExtDimensionAccountingFlag>{esc_xml_text(str(parsed["extDimensionAccountingFlag"]))}</ExtDimensionAccountingFlag>')
|
||||||
else:
|
else:
|
||||||
X(f'{indent}\t\t<ExtDimensionAccountingFlag/>')
|
X(f'{indent}\t\t<ExtDimensionAccountingFlag/>')
|
||||||
|
# Use — у реквизитов справочника и ПВХ. Позиция РАЗНАЯ: справочник пишет Use ПЕРЕД
|
||||||
|
# Indexing, ПВХ — ПОСЛЕ него (корпус acc+erp: Catalog `Use,Indexing,FullTextSearch`,
|
||||||
|
# ПВХ `Indexing,Use,FullTextSearch,DataHistory`). Отсюда отдельный контекст 'cct':
|
||||||
|
# структурно реквизит ПВХ совпадает со справочником, расходится только этим порядком.
|
||||||
|
use_value = parsed.get("use") or "ForItem"
|
||||||
if context == 'catalog':
|
if context == 'catalog':
|
||||||
X(f'{indent}\t\t<Use>{parsed.get("use") or "ForItem"}</Use>')
|
X(f'{indent}\t\t<Use>{use_value}</Use>')
|
||||||
if context not in ('processor', 'processor-tabular'):
|
if context not in ('processor', 'processor-tabular'):
|
||||||
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
||||||
if context != 'account-flag':
|
if context != 'account-flag':
|
||||||
@@ -2031,6 +2286,8 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
|||||||
if parsed.get('indexing'):
|
if parsed.get('indexing'):
|
||||||
indexing = parsed['indexing']
|
indexing = parsed['indexing']
|
||||||
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
|
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
|
||||||
|
if context == 'cct':
|
||||||
|
X(f'{indent}\t\t<Use>{use_value}</Use>')
|
||||||
# Реквизит адресации задачи: AddressingDimension (между Indexing и FullTextSearch).
|
# Реквизит адресации задачи: AddressingDimension (между Indexing и FullTextSearch).
|
||||||
if context == 'task-addressing' and elem_tag == 'AddressingAttribute':
|
if context == 'task-addressing' and elem_tag == 'AddressingAttribute':
|
||||||
if parsed.get('addressingDimension'):
|
if parsed.get('addressingDimension'):
|
||||||
@@ -2187,6 +2444,10 @@ def emit_enum_value(indent, parsed):
|
|||||||
X(f'{indent}\t\t<Comment>{esc_xml_text(parsed["comment"])}</Comment>')
|
X(f'{indent}\t\t<Comment>{esc_xml_text(parsed["comment"])}</Comment>')
|
||||||
else:
|
else:
|
||||||
X(f'{indent}\t\t<Comment/>')
|
X(f'{indent}\t\t<Comment/>')
|
||||||
|
# Цвет значения перечисления — свойство формата 2.21 (8.5), последним в Properties.
|
||||||
|
if is_format_221:
|
||||||
|
color = str(parsed['color']) if parsed.get('color') else 'auto'
|
||||||
|
X(f'{indent}\t\t<Color>{esc_xml_text(color)}</Color>')
|
||||||
X(f'{indent}\t</Properties>')
|
X(f'{indent}\t</Properties>')
|
||||||
X(f'{indent}</EnumValue>')
|
X(f'{indent}</EnumValue>')
|
||||||
|
|
||||||
@@ -2811,6 +3072,12 @@ def emit_common_form_properties(indent):
|
|||||||
X(f'{i}</UsePurposes>')
|
X(f'{i}</UsePurposes>')
|
||||||
else:
|
else:
|
||||||
X(f'{i}<UsePurposes/>')
|
X(f'{i}<UsePurposes/>')
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# между UsePurposes и UseStandardCommands.
|
||||||
|
if is_format_221:
|
||||||
|
X(f'{i}<UseInInterfaceCompatibilityMode>'
|
||||||
|
f'{get_enum_prop("UseInInterfaceCompatibilityMode", "useInInterfaceCompatibilityMode", "Any")}'
|
||||||
|
f'</UseInInterfaceCompatibilityMode>')
|
||||||
use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false'
|
use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false'
|
||||||
X(f'{i}<UseStandardCommands>{use_std_cmds}</UseStandardCommands>')
|
X(f'{i}<UseStandardCommands>{use_std_cmds}</UseStandardCommands>')
|
||||||
emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation'))
|
emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation'))
|
||||||
@@ -3069,7 +3336,12 @@ def emit_scheduled_job_properties(indent):
|
|||||||
else:
|
else:
|
||||||
X(f'{i}<Description/>')
|
X(f'{i}<Description/>')
|
||||||
key = str(defn['key']) if defn.get('key') else ''
|
key = str(defn['key']) if defn.get('key') else ''
|
||||||
X(f'{i}<Key>{esc_xml_text(key)}</Key>')
|
# Пустое значение → самозакрывающийся, как у <Description> выше: Конфигуратор
|
||||||
|
# не пишет пустых пар.
|
||||||
|
if key:
|
||||||
|
X(f'{i}<Key>{esc_xml_text(key)}</Key>')
|
||||||
|
else:
|
||||||
|
X(f'{i}<Key/>')
|
||||||
use = 'true' if defn.get('use') is True else 'false'
|
use = 'true' if defn.get('use') is True else 'false'
|
||||||
X(f'{i}<Use>{use}</Use>')
|
X(f'{i}<Use>{use}</Use>')
|
||||||
predefined = 'true' if defn.get('predefined') is True else 'false'
|
predefined = 'true' if defn.get('predefined') is True else 'false'
|
||||||
@@ -3123,6 +3395,9 @@ def emit_report_properties(indent):
|
|||||||
emit_verbatim_ref(i, 'DefaultSettingsForm', defn.get('defaultSettingsForm'))
|
emit_verbatim_ref(i, 'DefaultSettingsForm', defn.get('defaultSettingsForm'))
|
||||||
emit_verbatim_ref(i, 'AuxiliarySettingsForm', defn.get('auxiliarySettingsForm'))
|
emit_verbatim_ref(i, 'AuxiliarySettingsForm', defn.get('auxiliarySettingsForm'))
|
||||||
emit_verbatim_ref(i, 'DefaultVariantForm', defn.get('defaultVariantForm'))
|
emit_verbatim_ref(i, 'DefaultVariantForm', defn.get('defaultVariantForm'))
|
||||||
|
# Вспомогательная форма варианта отчёта — свойство формата 2.21 (8.5).
|
||||||
|
if is_format_221:
|
||||||
|
emit_verbatim_ref(i, 'AuxiliaryVariantForm', defn.get('auxiliaryVariantForm'))
|
||||||
emit_verbatim_ref(i, 'VariantsStorage', defn.get('variantsStorage'))
|
emit_verbatim_ref(i, 'VariantsStorage', defn.get('variantsStorage'))
|
||||||
emit_verbatim_ref(i, 'SettingsStorage', defn.get('settingsStorage'))
|
emit_verbatim_ref(i, 'SettingsStorage', defn.get('settingsStorage'))
|
||||||
incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false'
|
incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false'
|
||||||
@@ -3760,7 +4035,8 @@ def emit_web_service_properties(indent):
|
|||||||
emit_mltext(i, 'Synonym', synonym)
|
emit_mltext(i, 'Synonym', synonym)
|
||||||
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>' if defn.get('comment') else f'{i}<Comment/>')
|
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>' if defn.get('comment') else f'{i}<Comment/>')
|
||||||
namespace = str(defn['namespace']) if defn.get('namespace') else ''
|
namespace = str(defn['namespace']) if defn.get('namespace') else ''
|
||||||
X(f'{i}<Namespace>{esc_xml_text(namespace)}</Namespace>')
|
# Пустое значение → самозакрывающийся, как у <Comment> выше.
|
||||||
|
X(f'{i}<Namespace>{esc_xml_text(namespace)}</Namespace>' if namespace else f'{i}<Namespace/>')
|
||||||
# XDTOPackages — СПИСОК элементов: ссылка на пакет конфигурации (xr:MDObjectRef) либо URI
|
# XDTOPackages — СПИСОК элементов: ссылка на пакет конфигурации (xr:MDObjectRef) либо URI
|
||||||
# внешнего пространства имён (xs:string). Presentation пуст, CheckState 0 (корпус: 19/19).
|
# внешнего пространства имён (xs:string). Presentation пуст, CheckState 0 (корпус: 19/19).
|
||||||
pkgs = defn.get('xdtoPackages') or []
|
pkgs = defn.get('xdtoPackages') or []
|
||||||
@@ -3982,6 +4258,16 @@ xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -4034,6 +4320,15 @@ compat_mode = detect_compatibility_mode(output_dir)
|
|||||||
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
|
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
|
||||||
is_format_218 = format_rank(format_version) >= 218
|
is_format_218 = format_rank(format_version) >= 218
|
||||||
is_format_220 = format_rank(format_version) >= 220
|
is_format_220 = format_rank(format_version) >= 220
|
||||||
|
is_format_221 = format_rank(format_version) >= 221
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту.
|
||||||
|
# Только для шапок MetaDataObject и Form — в файлах с корнем extrnprops
|
||||||
|
# (Ext/ClientApplicationInterface.xml и т.п.) платформа его не пишет.
|
||||||
|
if is_format_221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
|
||||||
line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5
|
line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5
|
||||||
|
|
||||||
@@ -4174,7 +4469,7 @@ if obj_type in types_with_attr_ts:
|
|||||||
elif obj_type in ('DataProcessor', 'Report'):
|
elif obj_type in ('DataProcessor', 'Report'):
|
||||||
context = 'processor'
|
context = 'processor'
|
||||||
elif obj_type == 'ChartOfCharacteristicTypes':
|
elif obj_type == 'ChartOfCharacteristicTypes':
|
||||||
context = 'catalog' # реквизиты ПВХ структурно как у справочника (Use/FillFromFillingValue/DataHistory)
|
context = 'cct' # как catalog (Use/FillFromFillingValue/DataHistory), но Use ПОСЛЕ Indexing
|
||||||
elif obj_type in ('ChartOfAccounts', 'ChartOfCalculationTypes'):
|
elif obj_type in ('ChartOfAccounts', 'ChartOfCalculationTypes'):
|
||||||
context = 'account' # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
|
context = 'account' # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
|
||||||
else:
|
else:
|
||||||
@@ -4245,18 +4540,27 @@ if obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegist
|
|||||||
# Все семейства регистров: ресурсы/измерения — через богатый emit_attribute (общий слой object-свойств).
|
# Все семейства регистров: ресурсы/измерения — через богатый emit_attribute (общий слой object-свойств).
|
||||||
dim_res_ctx = {'InformationRegister': 'register-info', 'AccumulationRegister': 'register-accum',
|
dim_res_ctx = {'InformationRegister': 'register-info', 'AccumulationRegister': 'register-accum',
|
||||||
'CalculationRegister': 'register-calc', 'AccountingRegister': 'register-account'}.get(obj_type)
|
'CalculationRegister': 'register-calc', 'AccountingRegister': 'register-account'}.get(obj_type)
|
||||||
for r in resources:
|
# Порядок видов детей — канон выгрузки, снят с корпуса (acc+erp, разброса внутри
|
||||||
if dim_res_ctx:
|
# типа нет): у большинства регистров Resource, Attribute, Dimension, а у
|
||||||
emit_attribute('\t\t\t', r, dim_res_ctx, 'Resource')
|
# бухгалтерского — Dimension, Resource, Attribute. Команды у платформы идут
|
||||||
|
# последними, как и здесь.
|
||||||
|
kind_order = ['dim', 'res', 'attr'] if obj_type == 'AccountingRegister' else ['res', 'attr', 'dim']
|
||||||
|
for kind in kind_order:
|
||||||
|
if kind == 'res':
|
||||||
|
for r in resources:
|
||||||
|
if dim_res_ctx:
|
||||||
|
emit_attribute('\t\t\t', r, dim_res_ctx, 'Resource')
|
||||||
|
else:
|
||||||
|
emit_resource('\t\t\t', r, obj_type)
|
||||||
|
elif kind == 'dim':
|
||||||
|
for d in dims:
|
||||||
|
if dim_res_ctx:
|
||||||
|
emit_attribute('\t\t\t', d, dim_res_ctx, 'Dimension')
|
||||||
|
else:
|
||||||
|
emit_dimension('\t\t\t', d, obj_type)
|
||||||
else:
|
else:
|
||||||
emit_resource('\t\t\t', r, obj_type)
|
for a in reg_attrs:
|
||||||
for d in dims:
|
emit_attribute('\t\t\t', a, reg_ctx)
|
||||||
if dim_res_ctx:
|
|
||||||
emit_attribute('\t\t\t', d, dim_res_ctx, 'Dimension')
|
|
||||||
else:
|
|
||||||
emit_dimension('\t\t\t', d, obj_type)
|
|
||||||
for a in reg_attrs:
|
|
||||||
emit_attribute('\t\t\t', a, reg_ctx)
|
|
||||||
for cmd in reg_commands:
|
for cmd in reg_commands:
|
||||||
emit_command('\t\t\t', cmd['name'], cmd['def'])
|
emit_command('\t\t\t', cmd['name'], cmd['def'])
|
||||||
X('\t\t</ChildObjects>')
|
X('\t\t</ChildObjects>')
|
||||||
@@ -4360,7 +4664,7 @@ if obj_type == 'WebService':
|
|||||||
X(f'\t</{obj_type}>')
|
X(f'\t</{obj_type}>')
|
||||||
X('</MetaDataObject>')
|
X('</MetaDataObject>')
|
||||||
|
|
||||||
metadata_xml = '\n'.join(lines) + '\n'
|
metadata_xml = '\r\n'.join(lines)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 16. Write files
|
# 16. Write files
|
||||||
@@ -4422,7 +4726,7 @@ os.makedirs(type_dir, exist_ok=True)
|
|||||||
if obj_type not in types_no_sub_dir:
|
if obj_type not in types_no_sub_dir:
|
||||||
os.makedirs(obj_sub_dir, exist_ok=True)
|
os.makedirs(obj_sub_dir, exist_ok=True)
|
||||||
|
|
||||||
write_utf8_bom(main_xml_path, metadata_xml)
|
write_xml_file_keep_eol(main_xml_path, metadata_xml)
|
||||||
|
|
||||||
# Module files
|
# Module files
|
||||||
modules_created = []
|
modules_created = []
|
||||||
@@ -4498,10 +4802,14 @@ if obj_type == 'CommonForm':
|
|||||||
'xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" '
|
'xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" '
|
||||||
'xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
'xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
||||||
cf_form_xml = ('<?xml version="1.0" encoding="UTF-8"?>\n<Form ' + cf_ns + ' version="' + format_version + '">\n'
|
# Шапка Form на 2.21 тоже несёт палитру — см. комментарий у xmlns_decl.
|
||||||
'\t<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">\n\t\t<Autofill>true</Autofill>\n\t</AutoCommandBar>\n'
|
if is_format_221:
|
||||||
'\t<ChildItems/>\n</Form>\n')
|
cf_ns = cf_ns.replace(' xmlns:style=',
|
||||||
write_utf8_bom(cf_form_xml_path, cf_form_xml)
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
cf_form_xml = ('<?xml version="1.0" encoding="UTF-8"?>\r\n<Form ' + cf_ns + ' version="' + format_version + '">\r\n'
|
||||||
|
'\t<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">\r\n\t\t<Autofill>true</Autofill>\r\n\t</AutoCommandBar>\r\n'
|
||||||
|
'\t<ChildItems/>\r\n</Form>\r\n')
|
||||||
|
write_xml_file_keep_eol(cf_form_xml_path, cf_form_xml)
|
||||||
modules_created.append(cf_form_xml_path)
|
modules_created.append(cf_form_xml_path)
|
||||||
cf_module_dir = os.path.join(ext_dir, 'Form')
|
cf_module_dir = os.path.join(ext_dir, 'Form')
|
||||||
os.makedirs(cf_module_dir, exist_ok=True)
|
os.makedirs(cf_module_dir, exist_ok=True)
|
||||||
@@ -4589,10 +4897,18 @@ def emit_predef_item(out, val, indent, code_type):
|
|||||||
def build_predefined_xml(items, xsi_type, code_type):
|
def build_predefined_xml(items, xsi_type, code_type):
|
||||||
out = ['<?xml version="1.0" encoding="UTF-8"?>']
|
out = ['<?xml version="1.0" encoding="UTF-8"?>']
|
||||||
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{xsi_type}" version="{format_version}">')
|
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{xsi_type}" version="{format_version}">')
|
||||||
for it in items:
|
# Шапка Predefined.xml не объявляет cfg (predef/v8/xr/xs/xsi) — на время сборки этого
|
||||||
emit_predef_item(out, it, '\t', code_type)
|
# файла ссылочный тип уходит на локальную форму, как делает и платформа.
|
||||||
|
global cfg_prefix
|
||||||
|
saved_cfg_prefix = cfg_prefix
|
||||||
|
cfg_prefix = None
|
||||||
|
try:
|
||||||
|
for it in items:
|
||||||
|
emit_predef_item(out, it, '\t', code_type)
|
||||||
|
finally:
|
||||||
|
cfg_prefix = saved_cfg_prefix
|
||||||
out.append('</PredefinedData>')
|
out.append('</PredefinedData>')
|
||||||
return '\n'.join(out) + '\n'
|
return '\r\n'.join(out)
|
||||||
|
|
||||||
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
|
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
|
||||||
# ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. ---
|
# ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. ---
|
||||||
@@ -4687,10 +5003,17 @@ def emit_predef_account(out, val, indent, obj_nm, acct_flag_names, ext_dim_flag_
|
|||||||
def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''):
|
def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''):
|
||||||
out = ['<?xml version="1.0" encoding="UTF-8"?>']
|
out = ['<?xml version="1.0" encoding="UTF-8"?>']
|
||||||
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ChartOfAccountsPredefinedItems" version="{format_version}">')
|
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ChartOfAccountsPredefinedItems" version="{format_version}">')
|
||||||
for it in items:
|
# См. build_predefined_xml: шапка этого файла cfg не объявляет.
|
||||||
emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref)
|
global cfg_prefix
|
||||||
|
saved_cfg_prefix = cfg_prefix
|
||||||
|
cfg_prefix = None
|
||||||
|
try:
|
||||||
|
for it in items:
|
||||||
|
emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref)
|
||||||
|
finally:
|
||||||
|
cfg_prefix = saved_cfg_prefix
|
||||||
out.append('</PredefinedData>')
|
out.append('</PredefinedData>')
|
||||||
return '\n'.join(out) + '\n'
|
return '\r\n'.join(out)
|
||||||
|
|
||||||
# Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase).
|
# Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase).
|
||||||
def emit_predef_calc_type(out, val, indent):
|
def emit_predef_calc_type(out, val, indent):
|
||||||
@@ -4712,7 +5035,7 @@ def build_predefined_calc_type_xml(items):
|
|||||||
for it in items:
|
for it in items:
|
||||||
emit_predef_calc_type(out, it, '\t')
|
emit_predef_calc_type(out, it, '\t')
|
||||||
out.append('</PredefinedData>')
|
out.append('</PredefinedData>')
|
||||||
return '\n'.join(out) + '\n'
|
return '\r\n'.join(out)
|
||||||
|
|
||||||
# Special files
|
# Special files
|
||||||
# --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`:
|
# --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`:
|
||||||
@@ -4766,7 +5089,7 @@ if obj_type == 'ExchangePlan':
|
|||||||
parts.append(f'\t\t<AutoRecord>{it["autoRecord"]}</AutoRecord>\r\n')
|
parts.append(f'\t\t<AutoRecord>{it["autoRecord"]}</AutoRecord>\r\n')
|
||||||
parts.append('\t</Item>\r\n')
|
parts.append('\t</Item>\r\n')
|
||||||
parts.append('</ExchangePlanContent>\r\n')
|
parts.append('</ExchangePlanContent>\r\n')
|
||||||
write_utf8_bom(content_path, ''.join(parts))
|
write_xml_file_keep_eol(content_path, ''.join(parts))
|
||||||
modules_created.append(content_path)
|
modules_created.append(content_path)
|
||||||
elif not os.path.isfile(content_path):
|
elif not os.path.isfile(content_path):
|
||||||
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
|
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
|
||||||
@@ -4774,7 +5097,7 @@ if obj_type == 'ExchangePlan':
|
|||||||
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
|
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
|
||||||
ensure_ext_dir()
|
ensure_ext_dir()
|
||||||
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent {xep_ns} version="{format_version}"/>\r\n'
|
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent {xep_ns} version="{format_version}"/>\r\n'
|
||||||
write_utf8_bom(content_path, content_xml)
|
write_xml_file_keep_eol(content_path, content_xml)
|
||||||
modules_created.append(content_path)
|
modules_created.append(content_path)
|
||||||
|
|
||||||
if obj_type == 'BusinessProcess':
|
if obj_type == 'BusinessProcess':
|
||||||
@@ -4782,7 +5105,7 @@ if obj_type == 'BusinessProcess':
|
|||||||
if not os.path.isfile(flowchart_path):
|
if not os.path.isfile(flowchart_path):
|
||||||
ensure_ext_dir()
|
ensure_ext_dir()
|
||||||
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
|
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
|
||||||
write_utf8_bom(flowchart_path, flowchart_xml)
|
write_xml_file_keep_eol(flowchart_path, flowchart_xml)
|
||||||
modules_created.append(flowchart_path)
|
modules_created.append(flowchart_path)
|
||||||
|
|
||||||
# Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу.
|
# Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу.
|
||||||
@@ -4795,20 +5118,20 @@ if obj_type == 'ChartOfAccounts' and defn.get('predefined'):
|
|||||||
edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else ''
|
edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else ''
|
||||||
predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref)
|
predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref)
|
||||||
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
||||||
write_utf8_bom(predef_path, predef_xml)
|
write_xml_file_keep_eol(predef_path, predef_xml)
|
||||||
modules_created.append(predef_path)
|
modules_created.append(predef_path)
|
||||||
elif obj_type == 'ChartOfCalculationTypes' and defn.get('predefined'):
|
elif obj_type == 'ChartOfCalculationTypes' and defn.get('predefined'):
|
||||||
ensure_ext_dir()
|
ensure_ext_dir()
|
||||||
predef_xml = build_predefined_calc_type_xml(defn['predefined'])
|
predef_xml = build_predefined_calc_type_xml(defn['predefined'])
|
||||||
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
||||||
write_utf8_bom(predef_path, predef_xml)
|
write_xml_file_keep_eol(predef_path, predef_xml)
|
||||||
modules_created.append(predef_path)
|
modules_created.append(predef_path)
|
||||||
elif obj_type in predef_root_by_type and defn.get('predefined'):
|
elif obj_type in predef_root_by_type and defn.get('predefined'):
|
||||||
ensure_ext_dir()
|
ensure_ext_dir()
|
||||||
cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
|
cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
|
||||||
predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type)
|
predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type)
|
||||||
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
predef_path = os.path.join(ext_dir, 'Predefined.xml')
|
||||||
write_utf8_bom(predef_path, predef_xml)
|
write_xml_file_keep_eol(predef_path, predef_xml)
|
||||||
modules_created.append(predef_path)
|
modules_created.append(predef_path)
|
||||||
|
|
||||||
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
|
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
|
||||||
@@ -4949,6 +5272,8 @@ elif reg_result == 'no-config':
|
|||||||
print(f' Configuration.xml: not found at {config_xml_path} (register manually)')
|
print(f' Configuration.xml: not found at {config_xml_path} (register manually)')
|
||||||
|
|
||||||
# Cross-reference hints
|
# Cross-reference hints
|
||||||
|
if std_attr_tail_hint:
|
||||||
|
print(f'[HINT] {std_attr_tail_hint}')
|
||||||
if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'):
|
if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'):
|
||||||
print('[HINT] AccountingRegister requires ChartOfAccounts reference:')
|
print('[HINT] AccountingRegister requires ChartOfAccounts reference:')
|
||||||
print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
|
print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
#
|
#
|
||||||
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
#
|
#
|
||||||
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
||||||
@@ -20,6 +20,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespaces (зеркало XmlNamespaceManager) ---
|
# --- Namespaces (зеркало XmlNamespaceManager) ---
|
||||||
NS_MD = "http://v8.1c.ru/8.3/MDClasses"
|
NS_MD = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||||
@@ -2099,7 +2121,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
|
parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
|
||||||
parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
|
parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
|
||||||
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
if not os.path.exists(object_path):
|
if not os.path.exists(object_path):
|
||||||
|
|||||||
@@ -132,8 +132,7 @@ JSON — строки и/или объекты (для групп с вложе
|
|||||||
|
|
||||||
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
|
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
|
||||||
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
|
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
|
||||||
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка
|
Свойство можно задать, даже если у реквизита оно ещё не выставлено.
|
||||||
(правка не теряется молча).
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
||||||
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта.
|
Допустимы имена свойств соответствующего типа объекта.
|
||||||
|
|
||||||
### Type — тип значения (Константа, ПВХ)
|
### Type — тип значения (Константа, ПВХ)
|
||||||
|
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
```powershell
|
```powershell
|
||||||
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
||||||
```
|
```
|
||||||
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать
|
Структурные свойства (со вложенными узлами) через `Ключ=Значение` не задаются — исключение только `Type`.
|
||||||
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
|
|
||||||
|
|
||||||
## Свойства-списки
|
## Свойства-списки
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-edit v1.24 — Edit existing 1C metadata object XML
|
# meta-edit v1.38 — Edit existing 1C metadata object XML
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
@@ -89,7 +89,7 @@ $script:validEnumValues = @{
|
|||||||
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
|
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
|
||||||
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
|
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
|
||||||
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
|
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
|
||||||
"ReuseSessions" = @("DontUse","AutoUse")
|
"ReuseSessions" = @("DontUse","Use","AutoUse")
|
||||||
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
|
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
|
||||||
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
|
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
|
||||||
}
|
}
|
||||||
@@ -313,6 +313,23 @@ function Info($msg) {
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
$root = $script:xmlDoc.DocumentElement
|
$root = $script:xmlDoc.DocumentElement
|
||||||
|
|
||||||
|
# Префикс пространства current-config, объявленный в КОРНЕ файла (у платформы — cfg).
|
||||||
|
# Ищем по объявлениям корня, а не через GetPrefixOfNamespace: ссылочный тип живёт в
|
||||||
|
# ТЕКСТЕ узла, поэтому XML-слой этот префикс не отслеживает. $null = корень URI не
|
||||||
|
# объявляет → эмиттер остаётся на самодостаточной локальной форме.
|
||||||
|
# Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств
|
||||||
|
# появилась в поздних версиях (напр. <Color> у значения перечисления — в 2.21).
|
||||||
|
$script:formatVersion = $root.GetAttribute("version")
|
||||||
|
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
|
||||||
|
$script:isFormat221 = ($script:formatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221
|
||||||
|
|
||||||
|
$script:cfgUri = 'http://v8.1c.ru/8.1/data/enterprise/current-config'
|
||||||
|
$script:cfgPrefix = $null
|
||||||
|
foreach ($a in $root.Attributes) {
|
||||||
|
if ($a.Prefix -eq 'xmlns' -and $a.Value -eq $script:cfgUri) { $script:cfgPrefix = $a.LocalName; break }
|
||||||
|
}
|
||||||
|
|
||||||
if ($root.LocalName -ne "MetaDataObject") {
|
if ($root.LocalName -ne "MetaDataObject") {
|
||||||
Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)"
|
Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -444,7 +461,20 @@ function Resolve-TypeStr {
|
|||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
|
|
||||||
# Parameterized: Number(15,2), Строка(100)
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$baseName = $Matches[1].Trim()
|
$baseName = $Matches[1].Trim()
|
||||||
$params = $Matches[2]
|
$params = $Matches[2]
|
||||||
@@ -453,17 +483,17 @@ function Resolve-TypeStr {
|
|||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reference: СправочникСсылка.Организации
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$dotIdx = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $dotIdx)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
$suffix = $typeStr.Substring($dotIdx)
|
$suffix = $typeStr.Substring($dotIdx) # includes the dot
|
||||||
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
||||||
if ($resolved) { return "$resolved$suffix" }
|
if ($resolved) { return "$resolved$suffix" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simple
|
# Простое имя
|
||||||
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
if ($resolved) { return $resolved }
|
if ($resolved) { return $resolved }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
@@ -557,9 +587,19 @@ function Build-TypeContentXml {
|
|||||||
return $sb.ToString().TrimEnd("`r","`n")
|
return $sb.ToString().TrimEnd("`r","`n")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reference types — use local xmlns declaration for 1C compatibility
|
# Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
|
||||||
|
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
|
||||||
|
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
|
||||||
|
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
|
||||||
|
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
|
||||||
|
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
|
||||||
|
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
|
||||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') {
|
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') {
|
||||||
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>") | Out-Null
|
if ($script:cfgPrefix) {
|
||||||
|
$sb.AppendLine("$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>") | Out-Null
|
||||||
|
} else {
|
||||||
|
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"$script:cfgUri`">d5p1:$typeStr</v8:Type>") | Out-Null
|
||||||
|
}
|
||||||
return $sb.ToString().TrimEnd("`r","`n")
|
return $sb.ToString().TrimEnd("`r","`n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1279,6 +1319,9 @@ function Build-EnumValueFragment {
|
|||||||
$sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>") | Out-Null
|
$sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>") | Out-Null
|
||||||
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null
|
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null
|
||||||
$sb.AppendLine("$indent`t`t<Comment/>") | Out-Null
|
$sb.AppendLine("$indent`t`t<Comment/>") | Out-Null
|
||||||
|
# Цвет значения — свойство формата 2.21 (8.5). Без него добавленное значение
|
||||||
|
# отличалось бы от соседних, написанных платформой.
|
||||||
|
if ($script:isFormat221) { $sb.AppendLine("$indent`t`t<Color>auto</Color>") | Out-Null }
|
||||||
$sb.AppendLine("$indent`t</Properties>") | Out-Null
|
$sb.AppendLine("$indent`t</Properties>") | Out-Null
|
||||||
$sb.Append("$indent</EnumValue>") | Out-Null
|
$sb.Append("$indent</EnumValue>") | Out-Null
|
||||||
return $sb.ToString()
|
return $sb.ToString()
|
||||||
@@ -2067,6 +2110,11 @@ function Modify-Properties($propsDef) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Значение свойства-перечисления приводим к канону (как это делает meta-compile): иначе
|
||||||
|
# в XML уезжает то, что дала модель, и платформа отвергает выгрузку уже при загрузке.
|
||||||
|
# Неизвестное свойство функция пропускает как есть, неизвестное значение — отвергает.
|
||||||
|
$valueStr = Normalize-EnumValue $propName $valueStr
|
||||||
|
|
||||||
$propEl.InnerText = $valueStr
|
$propEl.InnerText = $valueStr
|
||||||
Info "Modified property: $propName = $valueStr"
|
Info "Modified property: $propName = $valueStr"
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
@@ -2811,7 +2859,11 @@ function Build-ChoiceParametersXml([string]$indent, $cp) {
|
|||||||
$script:fillBoolTrue = @('true','истина','да')
|
$script:fillBoolTrue = @('true','истина','да')
|
||||||
$script:fillBoolFalse = @('false','ложь','нет')
|
$script:fillBoolFalse = @('false','ложь','нет')
|
||||||
|
|
||||||
function Esc-XmlText([string]$s) { return $s.Replace('&','&').Replace('<','<').Replace('>','>') }
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
|
|
||||||
function Get-FillTypeCategory([string]$typeStr) {
|
function Get-FillTypeCategory([string]$typeStr) {
|
||||||
if (-not $typeStr) { return 'String' }
|
if (-not $typeStr) { return 'String' }
|
||||||
@@ -3138,7 +3190,8 @@ function Add-PredefinedItems($items) {
|
|||||||
$hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$version`">`r`n"
|
$hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$version`">`r`n"
|
||||||
$text = "$hdr$itemsXml</PredefinedData>`r`n"
|
$text = "$hdr$itemsXml</PredefinedData>`r`n"
|
||||||
}
|
}
|
||||||
[System.IO.File]::WriteAllText($path, $text, $utf8Bom)
|
# Создаваемый файл — по канону: без перевода строки в конце.
|
||||||
|
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$n = @($items).Count
|
$n = @($items).Count
|
||||||
Info "Added $n predefined item(s) → $path"
|
Info "Added $n predefined item(s) → $path"
|
||||||
$script:addCount += $n
|
$script:addCount += $n
|
||||||
@@ -3208,9 +3261,17 @@ if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) {
|
|||||||
$text = $text.Substring(1)
|
$text = $text.Substring(1)
|
||||||
}
|
}
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
# Write with BOM
|
# Write with BOM
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
|
|
||||||
Info "Saved: $resolvedPath"
|
Info "Saved: $resolvedPath"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# meta-edit v1.24 — Edit existing 1C metadata object XML
|
# meta-edit v1.38 — Edit existing 1C metadata object XML
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,6 +11,65 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -198,6 +257,12 @@ V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||||
CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config"
|
CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config"
|
||||||
|
# Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств
|
||||||
|
# появилась в поздних версиях (напр. <Color> у значения перечисления — в 2.21).
|
||||||
|
is_format_221 = False
|
||||||
|
# Префикс current-config, объявленный в КОРНЕ правимого файла (у платформы — cfg).
|
||||||
|
# None = корень его не объявляет → эмиттер ссылочных типов остаётся на локальной форме.
|
||||||
|
cfg_prefix = None
|
||||||
|
|
||||||
NSMAP_WRAPPER = {
|
NSMAP_WRAPPER = {
|
||||||
None: MD_NS,
|
None: MD_NS,
|
||||||
@@ -252,7 +317,8 @@ def localname(el):
|
|||||||
|
|
||||||
|
|
||||||
def esc_xml(s):
|
def esc_xml(s):
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -291,6 +357,9 @@ enum_value_aliases = {
|
|||||||
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
|
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Словарь ищет ПОЛЬЗОВАТЕЛЬСКИЙ ввод — поиск регистронезависим, как хеш-таблица PS1.
|
||||||
|
enum_value_aliases = CIDict(enum_value_aliases)
|
||||||
|
|
||||||
valid_enum_values = {
|
valid_enum_values = {
|
||||||
'RegisterType': ['Balance', 'Turnovers'],
|
'RegisterType': ['Balance', 'Turnovers'],
|
||||||
'WriteMode': ['Independent', 'RecorderSubordinate'],
|
'WriteMode': ['Independent', 'RecorderSubordinate'],
|
||||||
@@ -311,7 +380,7 @@ valid_enum_values = {
|
|||||||
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
|
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
|
||||||
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
|
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
|
||||||
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
|
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
|
||||||
'ReuseSessions': ['DontUse', 'AutoUse'],
|
'ReuseSessions': ['DontUse', 'Use', 'AutoUse'],
|
||||||
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
|
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
|
||||||
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
|
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
|
||||||
}
|
}
|
||||||
@@ -410,37 +479,48 @@ type_synonyms = {
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
TYPE_SYNONYMS = type_synonyms
|
||||||
|
|
||||||
|
|
||||||
def resolve_type_str(type_str):
|
def resolve_type_str(type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return type_str
|
return type_str
|
||||||
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
# Parameterized: Number(15,2), Строка(100)
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
m = re.match(r"^([^(]+)\((.+)\)$", type_str)
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if type_str.startswith('cfg:'):
|
||||||
|
type_str = type_str[4:]
|
||||||
|
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
|
||||||
|
type_str = type_str[type_str.index(':') + 1:]
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
||||||
if m:
|
if m:
|
||||||
base_name = m.group(1).strip()
|
base_name = m.group(1).strip()
|
||||||
params = m.group(2)
|
params = m.group(2)
|
||||||
resolved = type_synonyms.get(base_name.lower())
|
resolved = TYPE_SYNONYMS.get(base_name.lower())
|
||||||
if resolved:
|
if resolved:
|
||||||
return f"{resolved}({params})"
|
return f'{resolved}({params})'
|
||||||
return type_str
|
return type_str
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
|
||||||
# Reference: СправочникСсылка.Организации
|
if '.' in type_str:
|
||||||
if "." in type_str:
|
dot_idx = type_str.index('.')
|
||||||
dot_idx = type_str.index(".")
|
|
||||||
prefix = type_str[:dot_idx]
|
prefix = type_str[:dot_idx]
|
||||||
suffix = type_str[dot_idx:]
|
suffix = type_str[dot_idx:] # includes the dot
|
||||||
resolved = type_synonyms.get(prefix.lower())
|
resolved = TYPE_SYNONYMS.get(prefix.lower())
|
||||||
if resolved:
|
if resolved:
|
||||||
return f"{resolved}{suffix}"
|
return f'{resolved}{suffix}'
|
||||||
return type_str
|
return type_str
|
||||||
|
# Простое имя
|
||||||
# Simple
|
resolved = TYPE_SYNONYMS.get(type_str.lower())
|
||||||
resolved = type_synonyms.get(type_str.lower())
|
|
||||||
if resolved:
|
if resolved:
|
||||||
return resolved
|
return resolved
|
||||||
return type_str
|
return type_str
|
||||||
|
|
||||||
|
|
||||||
def build_type_content_xml(indent, type_str):
|
def build_type_content_xml(indent, type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return ""
|
return ""
|
||||||
@@ -525,14 +605,23 @@ def build_type_content_xml(indent, type_str):
|
|||||||
lines.append(f"{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>")
|
lines.append(f"{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>")
|
||||||
return "\r\n".join(lines)
|
return "\r\n".join(lines)
|
||||||
|
|
||||||
# Reference types — use local xmlns declaration for 1C compatibility
|
# Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
|
||||||
|
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
|
||||||
|
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
|
||||||
|
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
|
||||||
|
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
|
||||||
|
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
|
||||||
|
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
|
||||||
m = re.match(
|
m = re.match(
|
||||||
r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|"
|
r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|"
|
||||||
r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$",
|
r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$",
|
||||||
type_str,
|
type_str,
|
||||||
)
|
)
|
||||||
if m:
|
if m:
|
||||||
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
|
if cfg_prefix:
|
||||||
|
lines.append(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
|
||||||
|
else:
|
||||||
|
lines.append(f'{indent}<v8:Type xmlns:d5p1="{CFG_NS}">d5p1:{type_str}</v8:Type>')
|
||||||
return "\r\n".join(lines)
|
return "\r\n".join(lines)
|
||||||
|
|
||||||
# Fallback
|
# Fallback
|
||||||
@@ -1254,6 +1343,10 @@ def build_enum_value_fragment(parsed, indent):
|
|||||||
lines.append(f"{indent}\t\t<Name>{esc_xml_text(parsed['name'])}</Name>")
|
lines.append(f"{indent}\t\t<Name>{esc_xml_text(parsed['name'])}</Name>")
|
||||||
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"]))
|
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"]))
|
||||||
lines.append(f"{indent}\t\t<Comment/>")
|
lines.append(f"{indent}\t\t<Comment/>")
|
||||||
|
# Цвет значения — свойство формата 2.21 (8.5). Без него добавленное значение
|
||||||
|
# отличалось бы от соседних, написанных платформой.
|
||||||
|
if is_format_221:
|
||||||
|
lines.append(f"{indent}\t\t<Color>auto</Color>")
|
||||||
lines.append(f"{indent}\t</Properties>")
|
lines.append(f"{indent}\t</Properties>")
|
||||||
lines.append(f"{indent}</EnumValue>")
|
lines.append(f"{indent}</EnumValue>")
|
||||||
return "\r\n".join(lines)
|
return "\r\n".join(lines)
|
||||||
@@ -1936,6 +2029,11 @@ def modify_properties(props_def):
|
|||||||
print(f"meta-edit: modify-property: свойство '{prop_name}' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается", file=sys.stderr)
|
print(f"meta-edit: modify-property: свойство '{prop_name}' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Значение свойства-перечисления приводим к канону (как это делает meta-compile): иначе
|
||||||
|
# в XML уезжает то, что дала модель, и платформа отвергает выгрузку уже при загрузке.
|
||||||
|
# Неизвестное свойство функция пропускает как есть, неизвестное значение — отвергает.
|
||||||
|
value_str = normalize_enum_value(prop_name, value_str)
|
||||||
|
|
||||||
# Set inner text — clear children first, set text
|
# Set inner text — clear children first, set text
|
||||||
for ch in list(prop_el):
|
for ch in list(prop_el):
|
||||||
prop_el.remove(ch)
|
prop_el.remove(ch)
|
||||||
@@ -2962,21 +3060,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -2985,13 +3084,9 @@ def save_xml(tree, path):
|
|||||||
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
||||||
style = _detect_xml_style(path)
|
style = _detect_xml_style(path)
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
# Fix d5p1 namespace declarations stripped by lxml (it treats them as unused
|
# Костыль, возвращавший xmlns:d5p1 (lxml выбрасывал его как неиспользуемый, ведь
|
||||||
# because d5p1: appears only in text content, not in element/attribute names)
|
# префикс встречается только в тексте узла), удалён вместе с переходом на корневой
|
||||||
xml_bytes = re.sub(
|
# cfg: — объявление теперь берётся из шапки самого файла и не требует починки.
|
||||||
b'(<v8:Type)(?! xmlns:d5p1)(>d5p1:)',
|
|
||||||
b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2',
|
|
||||||
xml_bytes
|
|
||||||
)
|
|
||||||
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
if style is None or style["bom"]:
|
if style is None or style["bom"]:
|
||||||
@@ -3094,7 +3189,9 @@ def add_predefined_items(items):
|
|||||||
item_list = items if isinstance(items, list) else [items]
|
item_list = items if isinstance(items, list) else [items]
|
||||||
items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list)
|
items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
with open(path, 'r', encoding='utf-8-sig') as f:
|
# newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
|
||||||
|
# в LF при чтении и файл будет переписан в LF (#44/#46/#47).
|
||||||
|
with open(path, 'r', encoding='utf-8-sig', newline='') as f:
|
||||||
text = f.read()
|
text = f.read()
|
||||||
text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>')
|
text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>')
|
||||||
else:
|
else:
|
||||||
@@ -3103,7 +3200,10 @@ def add_predefined_items(items):
|
|||||||
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||||||
f'xsi:type="{xsi_type}" version="{version}">\r\n')
|
f'xsi:type="{xsi_type}" version="{version}">\r\n')
|
||||||
text = hdr + items_xml + '</PredefinedData>\r\n'
|
text = hdr + items_xml + '</PredefinedData>'
|
||||||
|
# Без перевода строки в конце — канон #57. Срезаем в ОБЕИХ ветках: файл, созданный
|
||||||
|
# прежней версией навыка, мог унести хвост, а PS-порт срезает безусловно.
|
||||||
|
text = text.rstrip('\r\n')
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
f.write(b'\xef\xbb\xbf')
|
f.write(b'\xef\xbb\xbf')
|
||||||
f.write(text.encode('utf-8'))
|
f.write(text.encode('utf-8'))
|
||||||
@@ -3141,7 +3241,7 @@ def main():
|
|||||||
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
|
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
|
||||||
parser.add_argument("-Value", default=None, help="Inline value")
|
parser.add_argument("-Value", default=None, help="Inline value")
|
||||||
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
|
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Mode validation ---
|
# --- Mode validation ---
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
@@ -3155,7 +3255,7 @@ def main():
|
|||||||
if not os.path.exists(args.DefinitionFile):
|
if not os.path.exists(args.DefinitionFile):
|
||||||
die(f"Definition file not found: {args.DefinitionFile}")
|
die(f"Definition file not found: {args.DefinitionFile}")
|
||||||
with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f:
|
with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f:
|
||||||
definition = json.load(f)
|
definition = ci_json(json.load(f))
|
||||||
|
|
||||||
# --- Resolve object path ---
|
# --- Resolve object path ---
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
@@ -3192,6 +3292,13 @@ def main():
|
|||||||
xml_tree = etree.parse(resolved_path, xml_parser)
|
xml_tree = etree.parse(resolved_path, xml_parser)
|
||||||
xml_root = xml_tree.getroot()
|
xml_root = xml_tree.getroot()
|
||||||
|
|
||||||
|
# Префикс current-config берём из объявлений корня — им и пишем ссылочные типы.
|
||||||
|
global cfg_prefix, is_format_221
|
||||||
|
cfg_prefix = next((p for p, u in (xml_root.nsmap or {}).items() if u == CFG_NS and p), None)
|
||||||
|
_fv = xml_root.get("version") or "2.17"
|
||||||
|
_m = re.match(r'^(\d+)\.(\d+)$', _fv)
|
||||||
|
is_format_221 = bool(_m) and int(_m.group(1)) * 100 + int(_m.group(2)) >= 221
|
||||||
|
|
||||||
# --- Detect object type ---
|
# --- Detect object type ---
|
||||||
if localname(xml_root) != "MetaDataObject":
|
if localname(xml_root) != "MetaDataObject":
|
||||||
die(f"Root element must be MetaDataObject, got: {localname(xml_root)}")
|
die(f"Root element must be MetaDataObject, got: {localname(xml_root)}")
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -Obj
|
|||||||
| `brief` | Всё одной-двумя строками: имена полей, счётчики |
|
| `brief` | Всё одной-двумя строками: имена полей, счётчики |
|
||||||
| `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы |
|
| `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы |
|
||||||
|
|
||||||
`-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, шаблон URL, операцию веб-сервиса).
|
`-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, стандартный реквизит,
|
||||||
|
шаблон URL, операцию веб-сервиса). Составной тип в сводке свёрнут в счётчик — полный список
|
||||||
|
типов даёт drill-down: `-Name ТипЗначения` у ПВХ, `-Name Владелец` у подчинённого справочника.
|
||||||
|
|
||||||
## Поддерживаемые типы (23)
|
## Поддерживаемые типы (23)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-info v1.4 — Compact summary of 1C metadata object
|
# meta-info v1.10 — Compact summary of 1C metadata object (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
|
||||||
@@ -170,6 +170,22 @@ function Get-MLText($node) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса
|
||||||
|
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой,
|
||||||
|
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый.
|
||||||
|
function Format-SingleTypeSet([string]$raw) {
|
||||||
|
$raw = $raw -replace '^d\d+p\d+:', 'cfg:'
|
||||||
|
if ($raw -match '^cfg:DefinedType\.(.+)$') { return "ОпределяемыйТип.$($Matches[1])" }
|
||||||
|
if ($raw -match '^cfg:Characteristic\.(.+)$') { return "Характеристика.$($Matches[1])" }
|
||||||
|
if ($raw -eq 'cfg:AnyRef') { return "ЛюбаяСсылка" }
|
||||||
|
if ($raw -eq 'cfg:AnyIBRef') { return "ЛюбаяСсылкаИБ" }
|
||||||
|
if ($raw -match '^cfg:(\w+Ref)$' -and $refTypeMap.ContainsKey($Matches[1])) {
|
||||||
|
return $refTypeMap[$Matches[1]]
|
||||||
|
}
|
||||||
|
if ($raw -match '^cfg:(.+)$') { return $Matches[1] }
|
||||||
|
return $raw
|
||||||
|
}
|
||||||
|
|
||||||
function Format-Type($typeNode) {
|
function Format-Type($typeNode) {
|
||||||
if (-not $typeNode) { return "" }
|
if (-not $typeNode) { return "" }
|
||||||
$types = @()
|
$types = @()
|
||||||
@@ -178,14 +194,7 @@ function Format-Type($typeNode) {
|
|||||||
$types += Format-SingleType $raw $typeNode
|
$types += Format-SingleType $raw $typeNode
|
||||||
}
|
}
|
||||||
foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) {
|
foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) {
|
||||||
$raw = $t.InnerText
|
$types += Format-SingleTypeSet $t.InnerText
|
||||||
if ($raw -match '^cfg:DefinedType\.(.+)$') {
|
|
||||||
$types += "ОпределяемыйТип.$($Matches[1])"
|
|
||||||
} elseif ($raw -match '^cfg:Characteristic\.(.+)$') {
|
|
||||||
$types += "Характеристика.$($Matches[1])"
|
|
||||||
} else {
|
|
||||||
$types += $raw
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ($types.Count -eq 0) { return "" }
|
if ($types.Count -eq 0) { return "" }
|
||||||
if ($types.Count -eq 1) { return $types[0] }
|
if ($types.Count -eq 1) { return $types[0] }
|
||||||
@@ -287,6 +296,202 @@ function Format-Flags($propsNode, [bool]$isDimension = $false) {
|
|||||||
return " [$($flags -join ', ')]"
|
return " [$($flags -join ', ')]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылка на объект метаданных (`Catalog.Валюты` в Owners и подобных) — не тип значения,
|
||||||
|
# но в выводе показываем именно тип, который присваивается: СправочникСсылка.Валюты.
|
||||||
|
function Format-MDObjectRef([string]$raw) {
|
||||||
|
if ($raw -match '^(\w+)\.(.+)$') {
|
||||||
|
$key = "$($Matches[1])Ref"
|
||||||
|
if ($refTypeMap.ContainsKey($key)) { return "$($refTypeMap[$key]).$($Matches[2])" }
|
||||||
|
}
|
||||||
|
return $raw
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Стандартные реквизиты ---
|
||||||
|
|
||||||
|
# Сколько типов состава печатать списком, прежде чем свернуть в счётчик. По корпусу
|
||||||
|
# acc/erp/ut/unf состав ПВХ доходит до 151 типа, при этом 20 из 42 ПВХ укладываются в 5.
|
||||||
|
$script:composedTypeThreshold = 5
|
||||||
|
|
||||||
|
# Имена полей, состав которых свернули в счётчик, — по ним в конце вывода печатается
|
||||||
|
# единственная подсказка, чем состав развернуть.
|
||||||
|
$script:collapsedNames = @()
|
||||||
|
|
||||||
|
# Блок StandardAttributes в XML опционален: платформа материализует его только когда хотя бы
|
||||||
|
# один стандартный реквизит кастомизирован (docs/meta-dsl-spec.md §7.1.1). Когда блока нет,
|
||||||
|
# действуют платформенные дефолты — профиль ниже совпадает с $stdAttrProfile в meta-compile
|
||||||
|
# (выведен из корпуса acc+erp). Правки держать синхронными, иначе навыки разъедутся молча.
|
||||||
|
$stdAttrRequiredDefault = @{
|
||||||
|
"Catalog" = @{ "Owner" = $true; "Description" = $true }
|
||||||
|
"Document" = @{ "Date" = $true }
|
||||||
|
"ExchangePlan" = @{ "Description" = $true; "Code" = $true }
|
||||||
|
"ChartOfAccounts" = @{ "Description" = $true; "Code" = $true }
|
||||||
|
"ChartOfCharacteristicTypes" = @{ "Description" = $true }
|
||||||
|
"ChartOfCalculationTypes" = @{ "Description" = $true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StdAttrRequired($propsNode, [string]$attrName, [string]$objType) {
|
||||||
|
$fc = $propsNode.SelectSingleNode("md:StandardAttributes/xr:StandardAttribute[@name='$attrName']/xr:FillChecking", $ns)
|
||||||
|
if ($fc) { return ($fc.InnerText -eq "ShowError") }
|
||||||
|
if ($stdAttrRequiredDefault.ContainsKey($objType) -and $stdAttrRequiredDefault[$objType].ContainsKey($attrName)) {
|
||||||
|
return $stdAttrRequiredDefault[$objType][$attrName]
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-StdAttr([string]$name, [string]$type, $flags) {
|
||||||
|
$f = @($flags | Where-Object { $_ })
|
||||||
|
$flagStr = if ($f.Count -gt 0) { " [$($f -join ', ')]" } else { "" }
|
||||||
|
return @{ Name = $name; Type = $type; Flags = $flagStr }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты, наличие и характеристики которых задаются настройками объекта, —
|
||||||
|
# их нельзя вывести из остального вывода, поэтому они попадают и в overview. Ссылка,
|
||||||
|
# ПометкаУдаления, Родитель и прочее следуют из типа объекта и строки «Иерархический» —
|
||||||
|
# они только в full (см. Get-StandardAttributesFull).
|
||||||
|
function Get-StandardAttributes($propsNode, [string]$objType, [string]$mode) {
|
||||||
|
$result = @()
|
||||||
|
|
||||||
|
# Владелец — только у подчинённого справочника; состав типов ниоткуда не выводится
|
||||||
|
if ($objType -eq "Catalog") {
|
||||||
|
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $ns)
|
||||||
|
$ownerTypes = @()
|
||||||
|
if ($ownersNode) {
|
||||||
|
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) {
|
||||||
|
$ownerTypes += Format-MDObjectRef $it.InnerText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ownerTypes.Count -gt 0) {
|
||||||
|
$req = if (Test-StdAttrRequired $propsNode "Owner" $objType) { "обязательный" } else { $null }
|
||||||
|
$result += New-StdAttr "Владелец" ($ownerTypes -join ", ") @($req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ТипЗначения ПВХ — до полутора сотен типов, полный список раздул бы сводку в обоих
|
||||||
|
# режимах, поэтому сворачиваем в счётчик. Состав смотреть через -Name ТипЗначения.
|
||||||
|
if ($objType -eq "ChartOfCharacteristicTypes") {
|
||||||
|
$vt = $propsNode.SelectSingleNode("md:Type", $ns)
|
||||||
|
if ($vt) {
|
||||||
|
$cnt = $vt.SelectNodes("v8:Type", $ns).Count + $vt.SelectNodes("v8:TypeSet", $ns).Count
|
||||||
|
$typeStr = if ($cnt -gt $script:composedTypeThreshold) {
|
||||||
|
$script:collapsedNames += "ТипЗначения"
|
||||||
|
"Составной ($cnt)"
|
||||||
|
} else { Format-Type $vt }
|
||||||
|
if ($typeStr) { $result += New-StdAttr "ТипЗначения" $typeStr @() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Дата — есть всегда, ценна флагом обязательности
|
||||||
|
if ($objType -in @("Document", "BusinessProcess", "Task")) {
|
||||||
|
$req = if (Test-StdAttrRequired $propsNode "Date" $objType) { "обязательный" } else { $null }
|
||||||
|
$result += New-StdAttr "Дата" "Дата" @($req)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Номер — тип и длина задаются объектом, при NumberLength=0 номера нет
|
||||||
|
if ($objType -in @("Document", "BusinessProcess", "Task")) {
|
||||||
|
$numLen = $propsNode.SelectSingleNode("md:NumberLength", $ns)
|
||||||
|
if ($numLen -and [int]$numLen.InnerText -gt 0) {
|
||||||
|
$numType = $propsNode.SelectSingleNode("md:NumberType", $ns)
|
||||||
|
$ntRu = if ($numType -and $numType.InnerText -eq "Number") { "Число" } else { "Строка" }
|
||||||
|
$flags = @()
|
||||||
|
if (Test-StdAttrRequired $propsNode "Number" $objType) { $flags += "обязательный" }
|
||||||
|
$numPer = $propsNode.SelectSingleNode("md:NumberPeriodicity", $ns)
|
||||||
|
if ($numPer -and $numberPeriodMap.ContainsKey($numPer.InnerText)) { $flags += $numberPeriodMap[$numPer.InnerText] }
|
||||||
|
$numAllowed = $propsNode.SelectSingleNode("md:NumberAllowedLength", $ns)
|
||||||
|
if ($numAllowed -and $numAllowed.InnerText -eq "Fixed") { $flags += "фикс. длина" }
|
||||||
|
$autoNum = $propsNode.SelectSingleNode("md:Autonumbering", $ns)
|
||||||
|
if ($autoNum -and $autoNum.InnerText -eq "true") { $flags += "авто" }
|
||||||
|
$result += New-StdAttr "Номер" "$ntRu($($numLen.InnerText))" $flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Код — при CodeLength=0 кода у объекта нет вовсе, строку не печатаем
|
||||||
|
$codeLen = $propsNode.SelectSingleNode("md:CodeLength", $ns)
|
||||||
|
if ($codeLen -and [int]$codeLen.InnerText -gt 0) {
|
||||||
|
$codeType = $propsNode.SelectSingleNode("md:CodeType", $ns)
|
||||||
|
$ctRu = if ($codeType -and $codeType.InnerText -eq "Number") { "Число" } else { "Строка" }
|
||||||
|
$flags = @()
|
||||||
|
if (Test-StdAttrRequired $propsNode "Code" $objType) { $flags += "обязательный" }
|
||||||
|
$codeAllowed = $propsNode.SelectSingleNode("md:CodeAllowedLength", $ns)
|
||||||
|
if ($codeAllowed -and $codeAllowed.InnerText -eq "Fixed") { $flags += "фикс. длина" }
|
||||||
|
$result += New-StdAttr "Код" "$ctRu($($codeLen.InnerText))" $flags
|
||||||
|
}
|
||||||
|
|
||||||
|
# Наименование — при DescriptionLength=0 наименования нет
|
||||||
|
$descLen = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
|
||||||
|
if ($descLen -and [int]$descLen.InnerText -gt 0) {
|
||||||
|
$req = if (Test-StdAttrRequired $propsNode "Description" $objType) { "обязательный" } else { $null }
|
||||||
|
$result += New-StdAttr "Наименование" "Строка($($descLen.InnerText))" @($req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Остальные стандартные реквизиты — предсказуемы по типу объекта, но в full полезны
|
||||||
|
# как перечень доступных полей для запроса.
|
||||||
|
function Get-StandardAttributesFull($propsNode, [string]$objType, [string]$objName) {
|
||||||
|
$selfRef = switch ($objType) {
|
||||||
|
"Catalog" { "СправочникСсылка.$objName" }
|
||||||
|
"ChartOfCharacteristicTypes" { "ПВХСсылка.$objName" }
|
||||||
|
"ChartOfAccounts" { "ПланСчетовСсылка.$objName" }
|
||||||
|
"ChartOfCalculationTypes" { "ПВРСсылка.$objName" }
|
||||||
|
"ExchangePlan" { "ПланОбменаСсылка.$objName" }
|
||||||
|
"Document" { "ДокументСсылка.$objName" }
|
||||||
|
"BusinessProcess" { "БизнесПроцессСсылка.$objName" }
|
||||||
|
"Task" { "ЗадачаСсылка.$objName" }
|
||||||
|
default { "" }
|
||||||
|
}
|
||||||
|
$result = @()
|
||||||
|
if ($selfRef) { $result += New-StdAttr "Ссылка" $selfRef @() }
|
||||||
|
$result += New-StdAttr "ПометкаУдаления" "Булево" @()
|
||||||
|
|
||||||
|
# Родитель и ЭтоГруппа существуют только у иерархических объектов
|
||||||
|
$hier = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
|
||||||
|
$isHier = ($hier -and $hier.InnerText -eq "true") -or $objType -eq "ChartOfAccounts"
|
||||||
|
if ($isHier -and $selfRef) {
|
||||||
|
$result += New-StdAttr "Родитель" $selfRef @()
|
||||||
|
$ht = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
|
||||||
|
if ($objType -eq "Catalog" -and (-not $ht -or $ht.InnerText -eq "HierarchyFoldersAndItems")) {
|
||||||
|
$result += New-StdAttr "ЭтоГруппа" "Булево" @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($objType) {
|
||||||
|
"Document" {
|
||||||
|
$result += New-StdAttr "Проведен" "Булево" @()
|
||||||
|
}
|
||||||
|
"ExchangePlan" {
|
||||||
|
$result += New-StdAttr "ЭтотУзел" "Булево" @()
|
||||||
|
$result += New-StdAttr "НомерОтправленного" "Число" @()
|
||||||
|
$result += New-StdAttr "НомерПринятого" "Число" @()
|
||||||
|
}
|
||||||
|
"BusinessProcess" {
|
||||||
|
$result += New-StdAttr "Стартован" "Булево" @()
|
||||||
|
$result += New-StdAttr "Завершен" "Булево" @()
|
||||||
|
$result += New-StdAttr "ВедущаяЗадача" "ЗадачаСсылка" @()
|
||||||
|
}
|
||||||
|
"Task" {
|
||||||
|
$result += New-StdAttr "Выполнена" "Булево" @()
|
||||||
|
$result += New-StdAttr "БизнесПроцесс" "БизнесПроцессСсылка" @()
|
||||||
|
$result += New-StdAttr "ТочкаМаршрута" "БизнесПроцессТочкаМаршрутаСсылка" @()
|
||||||
|
}
|
||||||
|
"ChartOfAccounts" {
|
||||||
|
$result += New-StdAttr "Вид" "ВидСчета" @()
|
||||||
|
$result += New-StdAttr "Забалансовый" "Булево" @()
|
||||||
|
$result += New-StdAttr "Порядок" "Число" @()
|
||||||
|
}
|
||||||
|
"ChartOfCalculationTypes" {
|
||||||
|
$result += New-StdAttr "ПериодДействияБазовый" "Булево" @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Предопределённые данные есть у всех перечисленных типов, кроме документов и задач
|
||||||
|
if ($objType -in @("Catalog", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes")) {
|
||||||
|
$result += New-StdAttr "Предопределенный" "Булево" @()
|
||||||
|
$result += New-StdAttr "ИмяПредопределенныхДанных" "Строка" @()
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) {
|
function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) {
|
||||||
$result = @()
|
$result = @()
|
||||||
foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) {
|
foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) {
|
||||||
@@ -329,7 +534,10 @@ function Get-MaxNameLen($attrs) {
|
|||||||
function Get-SimpleChildren($parentNode, [string]$tag) {
|
function Get-SimpleChildren($parentNode, [string]$tag) {
|
||||||
$result = @()
|
$result = @()
|
||||||
foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) {
|
foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) {
|
||||||
$result += $child.InnerText
|
# Form/Template в ChildObjects — простые узлы с именем в тексте, а Command — узел
|
||||||
|
# с вложенным Properties: у него InnerText склеил бы всё содержимое в одну строку.
|
||||||
|
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $ns)
|
||||||
|
if ($nameNode) { $result += $nameNode.InnerText } else { $result += $child.InnerText }
|
||||||
}
|
}
|
||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
@@ -650,6 +858,43 @@ if ($Name -and $childObjs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Не нашли среди дочерних объектов — пробуем стандартные реквизиты ниже
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drill-down по стандартному реквизиту — единственный способ увидеть состав типов целиком,
|
||||||
|
# когда в сводке он свёрнут в счётчик. Живёт в Properties, а не в ChildObjects, поэтому идёт
|
||||||
|
# отдельной веткой и работает даже у объектов без ChildObjects.
|
||||||
|
if ($Name -and -not $drillDone) {
|
||||||
|
$stdAll = @(Get-StandardAttributes $props $mdType "full") + @(Get-StandardAttributesFull $props $mdType $objName)
|
||||||
|
foreach ($s in $stdAll) {
|
||||||
|
if ($s.Name -ne $Name) { continue }
|
||||||
|
Out "Стандартный реквизит: $($s.Name)"
|
||||||
|
|
||||||
|
# Состав типов разворачиваем списком: ради этого drill-down и нужен
|
||||||
|
$typeSrc = $null
|
||||||
|
if ($Name -eq "ТипЗначения") { $typeSrc = $props.SelectSingleNode("md:Type", $ns) }
|
||||||
|
$typeList = @()
|
||||||
|
if ($typeSrc) {
|
||||||
|
foreach ($t in $typeSrc.SelectNodes("v8:Type", $ns)) { $typeList += Format-SingleType $t.InnerText $typeSrc }
|
||||||
|
foreach ($t in $typeSrc.SelectNodes("v8:TypeSet", $ns)) { $typeList += Format-SingleTypeSet $t.InnerText }
|
||||||
|
} elseif ($Name -eq "Владелец") {
|
||||||
|
$ownersNode = $props.SelectSingleNode("md:Owners", $ns)
|
||||||
|
if ($ownersNode) {
|
||||||
|
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) { $typeList += Format-MDObjectRef $it.InnerText }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($typeList.Count -gt 0) {
|
||||||
|
Out " Типы ($($typeList.Count)):"
|
||||||
|
foreach ($t in $typeList) { Out " $t" }
|
||||||
|
} else {
|
||||||
|
Out " Тип: $($s.Type)"
|
||||||
|
}
|
||||||
|
$flagText = $s.Flags.Trim()
|
||||||
|
if ($flagText) { Out " Свойства: $($flagText.Trim('[', ']'))" }
|
||||||
|
$drillDone = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
if (-not $drillDone) {
|
if (-not $drillDone) {
|
||||||
Write-Host "[ERROR] '$Name' not found in $objName"
|
Write-Host "[ERROR] '$Name' not found in $objName"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -680,6 +925,20 @@ if (-not $drillDone) {
|
|||||||
|
|
||||||
# --- Mode: brief ---
|
# --- Mode: brief ---
|
||||||
if ($Mode -eq "brief") {
|
if ($Mode -eq "brief") {
|
||||||
|
# Подчинённость — единственный факт структуры, который из brief не выводится никак,
|
||||||
|
# а без него код записи элемента падает. Про обязательность здесь намеренно молчим:
|
||||||
|
# обязательными бывают и обычные реквизиты, а их brief не разбирает.
|
||||||
|
if ($mdType -eq "Catalog") {
|
||||||
|
$ownersNode = $props.SelectSingleNode("md:Owners", $ns)
|
||||||
|
$ownerNames = @()
|
||||||
|
if ($ownersNode) {
|
||||||
|
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) {
|
||||||
|
$ownerNames += ($it.InnerText -replace '^\w+\.', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ownerNames.Count -gt 0) { Out "Подчинён: $($ownerNames -join ', ')" }
|
||||||
|
}
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
$attrs = @()
|
$attrs = @()
|
||||||
if ($childObjs) { $attrs = @(Get-Attributes $childObjs) }
|
if ($childObjs) { $attrs = @(Get-Attributes $childObjs) }
|
||||||
@@ -828,51 +1087,47 @@ if (-not $drillDone) {
|
|||||||
|
|
||||||
# Document-specific header properties
|
# Document-specific header properties
|
||||||
if ($mdType -eq "Document") {
|
if ($mdType -eq "Document") {
|
||||||
$numType = $props.SelectSingleNode("md:NumberType", $ns)
|
|
||||||
$numLen = $props.SelectSingleNode("md:NumberLength", $ns)
|
|
||||||
$numPer = $props.SelectSingleNode("md:NumberPeriodicity", $ns)
|
|
||||||
$autoNum = $props.SelectSingleNode("md:Autonumbering", $ns)
|
|
||||||
$posting = $props.SelectSingleNode("md:Posting", $ns)
|
$posting = $props.SelectSingleNode("md:Posting", $ns)
|
||||||
|
|
||||||
$parts = @()
|
$parts = @()
|
||||||
if ($numType -and $numLen) {
|
# Номер уехал в блок стандартных реквизитов — здесь остаются свойства объекта
|
||||||
$nt = if ($numType.InnerText -eq "String") { "Строка" } else { "Число" }
|
|
||||||
$piece = "Номер: $nt($($numLen.InnerText))"
|
|
||||||
if ($numPer) {
|
|
||||||
$perRu = if ($numberPeriodMap.ContainsKey($numPer.InnerText)) { $numberPeriodMap[$numPer.InnerText] } else { $numPer.InnerText }
|
|
||||||
$piece += ", $perRu"
|
|
||||||
}
|
|
||||||
if ($autoNum -and $autoNum.InnerText -eq "true") { $piece += ", авто" }
|
|
||||||
$parts += $piece
|
|
||||||
}
|
|
||||||
if ($posting) {
|
if ($posting) {
|
||||||
$parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })"
|
$parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })"
|
||||||
}
|
}
|
||||||
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
|
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Catalog-specific header properties
|
# Свойства иерархии и подчинения. Иерархия — не только у справочников: свойство
|
||||||
if ($mdType -eq "Catalog") {
|
# Hierarchical есть и у ПВХ (в корпусе acc/erp/ut/unf их 13), а без этой строки
|
||||||
$parts = @()
|
# в full появлялся Родитель, происхождение которого было ниоткуда не видно.
|
||||||
$hier = $props.SelectSingleNode("md:Hierarchical", $ns)
|
$parts = @()
|
||||||
if ($hier -and $hier.InnerText -eq "true") {
|
$hier = $props.SelectSingleNode("md:Hierarchical", $ns)
|
||||||
$ht = $props.SelectSingleNode("md:HierarchyType", $ns)
|
if ($hier -and $hier.InnerText -eq "true") {
|
||||||
$htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" }
|
$ht = $props.SelectSingleNode("md:HierarchyType", $ns)
|
||||||
$limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns)
|
$htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" }
|
||||||
$levelNode = $props.SelectSingleNode("md:LevelCount", $ns)
|
$limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns)
|
||||||
if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) {
|
$levelNode = $props.SelectSingleNode("md:LevelCount", $ns)
|
||||||
$htText += ", уровней: $($levelNode.InnerText)"
|
if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) {
|
||||||
} else {
|
$htText += ", уровней: $($levelNode.InnerText)"
|
||||||
$htText += ", без ограничения уровней"
|
} else {
|
||||||
}
|
$htText += ", без ограничения уровней"
|
||||||
$parts += "Иерархический: $htText"
|
|
||||||
}
|
}
|
||||||
$codeLen = $props.SelectSingleNode("md:CodeLength", $ns)
|
$parts += "Иерархический: $htText"
|
||||||
$descLen = $props.SelectSingleNode("md:DescriptionLength", $ns)
|
|
||||||
if ($codeLen -and [int]$codeLen.InnerText -gt 0) { $parts += "Код($($codeLen.InnerText))" }
|
|
||||||
if ($descLen -and [int]$descLen.InnerText -gt 0) { $parts += "Наименование($($descLen.InnerText))" }
|
|
||||||
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
|
|
||||||
}
|
}
|
||||||
|
# Код и Наименование уехали в блок стандартных реквизитов — здесь только свойства объекта.
|
||||||
|
# Подчинение печатаем лишь когда оно отличается от дефолта ToItems.
|
||||||
|
if ($mdType -eq "Catalog") {
|
||||||
|
$sub = $props.SelectSingleNode("md:SubordinationUse", $ns)
|
||||||
|
if ($sub -and $sub.InnerText -ne "ToItems") {
|
||||||
|
$subRu = switch ($sub.InnerText) {
|
||||||
|
"ToFolders" { "группам" }
|
||||||
|
"ToFoldersAndItems" { "группам и элементам" }
|
||||||
|
default { $sub.InnerText }
|
||||||
|
}
|
||||||
|
$parts += "Подчинение: $subRu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
|
||||||
|
|
||||||
# Register-specific header properties
|
# Register-specific header properties
|
||||||
if ($mdType -match "Register$") {
|
if ($mdType -match "Register$") {
|
||||||
@@ -1059,6 +1314,16 @@ if (-not $drillDone) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Standard attributes ---
|
||||||
|
$stdAttrs = @(Get-StandardAttributes $props $mdType $Mode)
|
||||||
|
if ($Mode -eq "full") { $stdAttrs += @(Get-StandardAttributesFull $props $mdType $objName) }
|
||||||
|
if ($stdAttrs.Count -gt 0) {
|
||||||
|
Out ""
|
||||||
|
Out "Стандартные реквизиты:"
|
||||||
|
$ml = Get-MaxNameLen $stdAttrs
|
||||||
|
foreach ($s in $stdAttrs) { Out (Format-AttrLine $s $ml) }
|
||||||
|
}
|
||||||
|
|
||||||
# --- Dimensions (registers) ---
|
# --- Dimensions (registers) ---
|
||||||
if ($mdType -match "Register$" -and $childObjs) {
|
if ($mdType -match "Register$" -and $childObjs) {
|
||||||
$dims = @(Get-Attributes $childObjs "Dimension" $true)
|
$dims = @(Get-Attributes $childObjs "Dimension" $true)
|
||||||
@@ -1177,6 +1442,13 @@ if (-not $drillDone) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Единственная подсказка на весь вывод — чем развернуть свёрнутый в счётчик состав
|
||||||
|
if ($script:collapsedNames.Count -gt 0) {
|
||||||
|
$hints = ($script:collapsedNames | ForEach-Object { "-Name $_" }) -join ", "
|
||||||
|
Out ""
|
||||||
|
Out "Полный состав типов: $hints"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Pagination and output ---
|
# --- Pagination and output ---
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-info v1.4 — Compact summary of 1C metadata object (Python port)
|
# meta-info v1.10 — Compact summary of 1C metadata object (Python port) (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -10,6 +10,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# ── arg parsing ──────────────────────────────────────────────
|
# ── arg parsing ──────────────────────────────────────────────
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||||
@@ -19,7 +41,7 @@ parser.add_argument("-Name", default="")
|
|||||||
parser.add_argument("-Limit", type=int, default=150)
|
parser.add_argument("-Limit", type=int, default=150)
|
||||||
parser.add_argument("-Offset", type=int, default=0)
|
parser.add_argument("-Offset", type=int, default=0)
|
||||||
parser.add_argument("-OutFile", default="")
|
parser.add_argument("-OutFile", default="")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
mode = args.Mode
|
mode = args.Mode
|
||||||
@@ -227,6 +249,30 @@ def get_ml_text(node):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса
|
||||||
|
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой,
|
||||||
|
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый.
|
||||||
|
def format_single_type_set(raw):
|
||||||
|
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
|
||||||
|
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
|
||||||
|
if m:
|
||||||
|
return f"ОпределяемыйТип.{m.group(1)}"
|
||||||
|
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
|
||||||
|
if m:
|
||||||
|
return f"Характеристика.{m.group(1)}"
|
||||||
|
if raw == "cfg:AnyRef":
|
||||||
|
return "ЛюбаяСсылка"
|
||||||
|
if raw == "cfg:AnyIBRef":
|
||||||
|
return "ЛюбаяСсылкаИБ"
|
||||||
|
m = re.match(r'^cfg:(\w+Ref)$', raw)
|
||||||
|
if m and m.group(1) in ref_type_map:
|
||||||
|
return ref_type_map[m.group(1)]
|
||||||
|
m = re.match(r'^cfg:(.+)$', raw)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def format_type(type_node_el):
|
def format_type(type_node_el):
|
||||||
if type_node_el is None:
|
if type_node_el is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -234,16 +280,7 @@ def format_type(type_node_el):
|
|||||||
for t in find_all(type_node_el, "v8:Type"):
|
for t in find_all(type_node_el, "v8:Type"):
|
||||||
types.append(format_single_type(inner_text(t), type_node_el))
|
types.append(format_single_type(inner_text(t), type_node_el))
|
||||||
for t in find_all(type_node_el, "v8:TypeSet"):
|
for t in find_all(type_node_el, "v8:TypeSet"):
|
||||||
raw = inner_text(t)
|
types.append(format_single_type_set(inner_text(t)))
|
||||||
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
|
|
||||||
if m:
|
|
||||||
types.append(f"ОпределяемыйТип.{m.group(1)}")
|
|
||||||
continue
|
|
||||||
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
|
|
||||||
if m:
|
|
||||||
types.append(f"Характеристика.{m.group(1)}")
|
|
||||||
continue
|
|
||||||
types.append(raw)
|
|
||||||
if len(types) == 0:
|
if len(types) == 0:
|
||||||
return ""
|
return ""
|
||||||
if len(types) == 1:
|
if len(types) == 1:
|
||||||
@@ -341,6 +378,190 @@ def format_flags(a_props, is_dimension=False):
|
|||||||
return f" [{', '.join(flags)}]"
|
return f" [{', '.join(flags)}]"
|
||||||
|
|
||||||
|
|
||||||
|
# Ссылка на объект метаданных (`Catalog.Валюты` в Owners и подобных) — не тип значения,
|
||||||
|
# но в выводе показываем именно тип, который присваивается: СправочникСсылка.Валюты.
|
||||||
|
def format_md_object_ref(raw):
|
||||||
|
m = re.match(r'^(\w+)\.(.+)$', raw)
|
||||||
|
if m:
|
||||||
|
key = f"{m.group(1)}Ref"
|
||||||
|
if key in ref_type_map:
|
||||||
|
return f"{ref_type_map[key]}.{m.group(2)}"
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
# ── Стандартные реквизиты ────────────────────────────────────
|
||||||
|
|
||||||
|
# Сколько типов состава печатать списком, прежде чем свернуть в счётчик. По корпусу
|
||||||
|
# acc/erp/ut/unf состав ПВХ доходит до 151 типа, при этом 20 из 42 ПВХ укладываются в 5.
|
||||||
|
COMPOSED_TYPE_THRESHOLD = 5
|
||||||
|
|
||||||
|
# Имена полей, состав которых свернули в счётчик, — по ним в конце вывода печатается
|
||||||
|
# единственная подсказка, чем состав развернуть.
|
||||||
|
collapsed_names = []
|
||||||
|
|
||||||
|
# Блок StandardAttributes в XML опционален: платформа материализует его только когда хотя бы
|
||||||
|
# один стандартный реквизит кастомизирован (docs/meta-dsl-spec.md §7.1.1). Когда блока нет,
|
||||||
|
# действуют платформенные дефолты — профиль ниже совпадает с $stdAttrProfile в meta-compile
|
||||||
|
# (выведен из корпуса acc+erp). Правки держать синхронными, иначе навыки разъедутся молча.
|
||||||
|
std_attr_required_default = {
|
||||||
|
"Catalog": {"Owner": True, "Description": True},
|
||||||
|
"Document": {"Date": True},
|
||||||
|
"ExchangePlan": {"Description": True, "Code": True},
|
||||||
|
"ChartOfAccounts": {"Description": True, "Code": True},
|
||||||
|
"ChartOfCharacteristicTypes": {"Description": True},
|
||||||
|
"ChartOfCalculationTypes": {"Description": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_std_attr_required(props_node, attr_name, obj_type):
|
||||||
|
fc = find(props_node, f"md:StandardAttributes/xr:StandardAttribute[@name='{attr_name}']/xr:FillChecking")
|
||||||
|
if fc is not None:
|
||||||
|
return inner_text(fc) == "ShowError"
|
||||||
|
return std_attr_required_default.get(obj_type, {}).get(attr_name, False)
|
||||||
|
|
||||||
|
|
||||||
|
def new_std_attr(name, type_str, flags):
|
||||||
|
f = [x for x in flags if x]
|
||||||
|
flag_str = f" [{', '.join(f)}]" if f else ""
|
||||||
|
return {"Name": name, "Type": type_str, "Flags": flag_str}
|
||||||
|
|
||||||
|
|
||||||
|
# Стандартные реквизиты, наличие и характеристики которых задаются настройками объекта, —
|
||||||
|
# их нельзя вывести из остального вывода, поэтому они попадают и в overview. Ссылка,
|
||||||
|
# ПометкаУдаления, Родитель и прочее следуют из типа объекта и строки «Иерархический» —
|
||||||
|
# они только в full (см. get_standard_attributes_full).
|
||||||
|
def get_standard_attributes(props_node, obj_type, mode_name):
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# Владелец — только у подчинённого справочника; состав типов ниоткуда не выводится
|
||||||
|
if obj_type == "Catalog":
|
||||||
|
owners_node = find(props_node, "md:Owners")
|
||||||
|
owner_types = []
|
||||||
|
if owners_node is not None:
|
||||||
|
for it in find_all(owners_node, "xr:Item"):
|
||||||
|
owner_types.append(format_md_object_ref(inner_text(it)))
|
||||||
|
if owner_types:
|
||||||
|
req = "обязательный" if test_std_attr_required(props_node, "Owner", obj_type) else None
|
||||||
|
result.append(new_std_attr("Владелец", ", ".join(owner_types), [req]))
|
||||||
|
|
||||||
|
# ТипЗначения ПВХ — до полутора сотен типов, полный список раздул бы сводку в обоих
|
||||||
|
# режимах, поэтому сворачиваем в счётчик. Состав смотреть через -Name ТипЗначения.
|
||||||
|
if obj_type == "ChartOfCharacteristicTypes":
|
||||||
|
vt = find(props_node, "md:Type")
|
||||||
|
if vt is not None:
|
||||||
|
cnt = len(find_all(vt, "v8:Type")) + len(find_all(vt, "v8:TypeSet"))
|
||||||
|
if cnt > COMPOSED_TYPE_THRESHOLD:
|
||||||
|
type_str = f"Составной ({cnt})"
|
||||||
|
collapsed_names.append("ТипЗначения")
|
||||||
|
else:
|
||||||
|
type_str = format_type(vt)
|
||||||
|
if type_str:
|
||||||
|
result.append(new_std_attr("ТипЗначения", type_str, []))
|
||||||
|
|
||||||
|
# Дата — есть всегда, ценна флагом обязательности
|
||||||
|
if obj_type in ("Document", "BusinessProcess", "Task"):
|
||||||
|
req = "обязательный" if test_std_attr_required(props_node, "Date", obj_type) else None
|
||||||
|
result.append(new_std_attr("Дата", "Дата", [req]))
|
||||||
|
|
||||||
|
# Номер — тип и длина задаются объектом, при NumberLength=0 номера нет
|
||||||
|
if obj_type in ("Document", "BusinessProcess", "Task"):
|
||||||
|
num_len = find(props_node, "md:NumberLength")
|
||||||
|
if num_len is not None and inner_text(num_len).isdigit() and int(inner_text(num_len)) > 0:
|
||||||
|
num_type = find(props_node, "md:NumberType")
|
||||||
|
nt_ru = "Число" if num_type is not None and inner_text(num_type) == "Number" else "Строка"
|
||||||
|
flags = []
|
||||||
|
if test_std_attr_required(props_node, "Number", obj_type):
|
||||||
|
flags.append("обязательный")
|
||||||
|
num_per = find(props_node, "md:NumberPeriodicity")
|
||||||
|
if num_per is not None and inner_text(num_per) in number_period_map:
|
||||||
|
flags.append(number_period_map[inner_text(num_per)])
|
||||||
|
num_allowed = find(props_node, "md:NumberAllowedLength")
|
||||||
|
if num_allowed is not None and inner_text(num_allowed) == "Fixed":
|
||||||
|
flags.append("фикс. длина")
|
||||||
|
auto_num = find(props_node, "md:Autonumbering")
|
||||||
|
if auto_num is not None and inner_text(auto_num) == "true":
|
||||||
|
flags.append("авто")
|
||||||
|
result.append(new_std_attr("Номер", f"{nt_ru}({inner_text(num_len)})", flags))
|
||||||
|
|
||||||
|
# Код — при CodeLength=0 кода у объекта нет вовсе, строку не печатаем
|
||||||
|
code_len = find(props_node, "md:CodeLength")
|
||||||
|
if code_len is not None and inner_text(code_len).isdigit() and int(inner_text(code_len)) > 0:
|
||||||
|
code_type = find(props_node, "md:CodeType")
|
||||||
|
ct_ru = "Число" if code_type is not None and inner_text(code_type) == "Number" else "Строка"
|
||||||
|
flags = []
|
||||||
|
if test_std_attr_required(props_node, "Code", obj_type):
|
||||||
|
flags.append("обязательный")
|
||||||
|
code_allowed = find(props_node, "md:CodeAllowedLength")
|
||||||
|
if code_allowed is not None and inner_text(code_allowed) == "Fixed":
|
||||||
|
flags.append("фикс. длина")
|
||||||
|
result.append(new_std_attr("Код", f"{ct_ru}({inner_text(code_len)})", flags))
|
||||||
|
|
||||||
|
# Наименование — при DescriptionLength=0 наименования нет
|
||||||
|
desc_len = find(props_node, "md:DescriptionLength")
|
||||||
|
if desc_len is not None and inner_text(desc_len).isdigit() and int(inner_text(desc_len)) > 0:
|
||||||
|
req = "обязательный" if test_std_attr_required(props_node, "Description", obj_type) else None
|
||||||
|
result.append(new_std_attr("Наименование", f"Строка({inner_text(desc_len)})", [req]))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Остальные стандартные реквизиты — предсказуемы по типу объекта, но в full полезны
|
||||||
|
# как перечень доступных полей для запроса.
|
||||||
|
def get_standard_attributes_full(props_node, obj_type, obj_name):
|
||||||
|
self_ref_map = {
|
||||||
|
"Catalog": "СправочникСсылка",
|
||||||
|
"ChartOfCharacteristicTypes": "ПВХСсылка",
|
||||||
|
"ChartOfAccounts": "ПланСчетовСсылка",
|
||||||
|
"ChartOfCalculationTypes": "ПВРСсылка",
|
||||||
|
"ExchangePlan": "ПланОбменаСсылка",
|
||||||
|
"Document": "ДокументСсылка",
|
||||||
|
"BusinessProcess": "БизнесПроцессСсылка",
|
||||||
|
"Task": "ЗадачаСсылка",
|
||||||
|
}
|
||||||
|
self_ref = f"{self_ref_map[obj_type]}.{obj_name}" if obj_type in self_ref_map else ""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
if self_ref:
|
||||||
|
result.append(new_std_attr("Ссылка", self_ref, []))
|
||||||
|
result.append(new_std_attr("ПометкаУдаления", "Булево", []))
|
||||||
|
|
||||||
|
# Родитель и ЭтоГруппа существуют только у иерархических объектов
|
||||||
|
hier = find(props_node, "md:Hierarchical")
|
||||||
|
is_hier = (hier is not None and inner_text(hier) == "true") or obj_type == "ChartOfAccounts"
|
||||||
|
if is_hier and self_ref:
|
||||||
|
result.append(new_std_attr("Родитель", self_ref, []))
|
||||||
|
ht = find(props_node, "md:HierarchyType")
|
||||||
|
if obj_type == "Catalog" and (ht is None or inner_text(ht) == "HierarchyFoldersAndItems"):
|
||||||
|
result.append(new_std_attr("ЭтоГруппа", "Булево", []))
|
||||||
|
|
||||||
|
if obj_type == "Document":
|
||||||
|
result.append(new_std_attr("Проведен", "Булево", []))
|
||||||
|
elif obj_type == "ExchangePlan":
|
||||||
|
result.append(new_std_attr("ЭтотУзел", "Булево", []))
|
||||||
|
result.append(new_std_attr("НомерОтправленного", "Число", []))
|
||||||
|
result.append(new_std_attr("НомерПринятого", "Число", []))
|
||||||
|
elif obj_type == "BusinessProcess":
|
||||||
|
result.append(new_std_attr("Стартован", "Булево", []))
|
||||||
|
result.append(new_std_attr("Завершен", "Булево", []))
|
||||||
|
result.append(new_std_attr("ВедущаяЗадача", "ЗадачаСсылка", []))
|
||||||
|
elif obj_type == "Task":
|
||||||
|
result.append(new_std_attr("Выполнена", "Булево", []))
|
||||||
|
result.append(new_std_attr("БизнесПроцесс", "БизнесПроцессСсылка", []))
|
||||||
|
result.append(new_std_attr("ТочкаМаршрута", "БизнесПроцессТочкаМаршрутаСсылка", []))
|
||||||
|
elif obj_type == "ChartOfAccounts":
|
||||||
|
result.append(new_std_attr("Вид", "ВидСчета", []))
|
||||||
|
result.append(new_std_attr("Забалансовый", "Булево", []))
|
||||||
|
result.append(new_std_attr("Порядок", "Число", []))
|
||||||
|
elif obj_type == "ChartOfCalculationTypes":
|
||||||
|
result.append(new_std_attr("ПериодДействияБазовый", "Булево", []))
|
||||||
|
|
||||||
|
# Предопределённые данные есть у всех перечисленных типов, кроме документов и задач
|
||||||
|
if obj_type in ("Catalog", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes"):
|
||||||
|
result.append(new_std_attr("Предопределенный", "Булево", []))
|
||||||
|
result.append(new_std_attr("ИмяПредопределенныхДанных", "Строка", []))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_attributes(parent_node, child_tag="Attribute", is_dimension=False):
|
def get_attributes(parent_node, child_tag="Attribute", is_dimension=False):
|
||||||
result = []
|
result = []
|
||||||
for attr in find_all(parent_node, f"md:{child_tag}"):
|
for attr in find_all(parent_node, f"md:{child_tag}"):
|
||||||
@@ -381,7 +602,10 @@ def get_max_name_len(attrs):
|
|||||||
def get_simple_children(parent_node, tag):
|
def get_simple_children(parent_node, tag):
|
||||||
result = []
|
result = []
|
||||||
for child in find_all(parent_node, f"md:{tag}"):
|
for child in find_all(parent_node, f"md:{tag}"):
|
||||||
result.append(inner_text(child))
|
# Form/Template в ChildObjects — простые узлы с именем в тексте, а Command — узел
|
||||||
|
# с вложенным Properties: у него inner_text склеил бы всё содержимое в одну строку.
|
||||||
|
name_node = find(child, "md:Properties/md:Name")
|
||||||
|
result.append(inner_text(name_node) if name_node is not None else inner_text(child))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -472,7 +696,7 @@ def get_ws_operations(child_objs):
|
|||||||
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
|
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
|
||||||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||||||
# object's support rule. Never throws — degrades to "не на поддержке".
|
# object's support rule. Never throws — degrades to "не на поддержке".
|
||||||
def _meta_is_external_root(xml_path):
|
def _sg_is_external_root(xml_path):
|
||||||
if not os.path.isfile(xml_path):
|
if not os.path.isfile(xml_path):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -487,7 +711,7 @@ def _meta_is_external_root(xml_path):
|
|||||||
|
|
||||||
def get_object_support_status(obj_uuid):
|
def get_object_support_status(obj_uuid):
|
||||||
try:
|
try:
|
||||||
if _meta_is_external_root(object_path):
|
if _sg_is_external_root(object_path):
|
||||||
return None
|
return None
|
||||||
d = os.path.dirname(object_path)
|
d = os.path.dirname(object_path)
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -705,6 +929,44 @@ if drill_name and child_objs is not None:
|
|||||||
drill_done = True
|
drill_done = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Не нашли среди дочерних объектов — пробуем стандартные реквизиты ниже
|
||||||
|
|
||||||
|
# Drill-down по стандартному реквизиту — единственный способ увидеть состав типов целиком,
|
||||||
|
# когда в сводке он свёрнут в счётчик. Живёт в Properties, а не в ChildObjects, поэтому идёт
|
||||||
|
# отдельной веткой и работает даже у объектов без ChildObjects.
|
||||||
|
if drill_name and not drill_done:
|
||||||
|
std_all = get_standard_attributes(props, md_type, "full") + get_standard_attributes_full(props, md_type, obj_name)
|
||||||
|
for s in std_all:
|
||||||
|
if s["Name"] != drill_name:
|
||||||
|
continue
|
||||||
|
out(f"Стандартный реквизит: {s['Name']}")
|
||||||
|
|
||||||
|
# Состав типов разворачиваем списком: ради этого drill-down и нужен
|
||||||
|
type_list = []
|
||||||
|
type_src = find(props, "md:Type") if drill_name == "ТипЗначения" else None
|
||||||
|
if type_src is not None:
|
||||||
|
for t in find_all(type_src, "v8:Type"):
|
||||||
|
type_list.append(format_single_type(inner_text(t), type_src))
|
||||||
|
for t in find_all(type_src, "v8:TypeSet"):
|
||||||
|
type_list.append(format_single_type_set(inner_text(t)))
|
||||||
|
elif drill_name == "Владелец":
|
||||||
|
owners_node = find(props, "md:Owners")
|
||||||
|
if owners_node is not None:
|
||||||
|
for it in find_all(owners_node, "xr:Item"):
|
||||||
|
type_list.append(format_md_object_ref(inner_text(it)))
|
||||||
|
|
||||||
|
if type_list:
|
||||||
|
out(f" Типы ({len(type_list)}):")
|
||||||
|
for t in type_list:
|
||||||
|
out(f" {t}")
|
||||||
|
else:
|
||||||
|
out(f" Тип: {s['Type']}")
|
||||||
|
flag_text = s["Flags"].strip()
|
||||||
|
if flag_text:
|
||||||
|
out(f" Свойства: {flag_text.strip('[]')}")
|
||||||
|
drill_done = True
|
||||||
|
break
|
||||||
|
|
||||||
if not drill_done:
|
if not drill_done:
|
||||||
print(f"[ERROR] '{drill_name}' not found in {obj_name}")
|
print(f"[ERROR] '{drill_name}' not found in {obj_name}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -736,6 +998,18 @@ if not drill_done:
|
|||||||
out(f"Расширенное представление списка: {ext_list_presentation}")
|
out(f"Расширенное представление списка: {ext_list_presentation}")
|
||||||
|
|
||||||
if mode == "brief":
|
if mode == "brief":
|
||||||
|
# Подчинённость — единственный факт структуры, который из brief не выводится никак,
|
||||||
|
# а без него код записи элемента падает. Про обязательность здесь намеренно молчим:
|
||||||
|
# обязательными бывают и обычные реквизиты, а их brief не разбирает.
|
||||||
|
if md_type == "Catalog":
|
||||||
|
owners_node = find(props, "md:Owners")
|
||||||
|
owner_names = []
|
||||||
|
if owners_node is not None:
|
||||||
|
for it in find_all(owners_node, "xr:Item"):
|
||||||
|
owner_names.append(re.sub(r'^\w+\.', '', inner_text(it)))
|
||||||
|
if owner_names:
|
||||||
|
out(f"Подчинён: {', '.join(owner_names)}")
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
attrs = get_attributes(child_objs) if child_objs is not None else []
|
attrs = get_attributes(child_objs) if child_objs is not None else []
|
||||||
if attrs:
|
if attrs:
|
||||||
@@ -865,48 +1139,39 @@ if not drill_done:
|
|||||||
|
|
||||||
# Document-specific header
|
# Document-specific header
|
||||||
if md_type == "Document":
|
if md_type == "Document":
|
||||||
num_type = find(props, "md:NumberType")
|
|
||||||
num_len = find(props, "md:NumberLength")
|
|
||||||
num_per = find(props, "md:NumberPeriodicity")
|
|
||||||
auto_num = find(props, "md:Autonumbering")
|
|
||||||
posting = find(props, "md:Posting")
|
posting = find(props, "md:Posting")
|
||||||
parts = []
|
parts = []
|
||||||
if num_type is not None and num_len is not None:
|
# Номер уехал в блок стандартных реквизитов — здесь остаются свойства объекта
|
||||||
nt = "Строка" if inner_text(num_type) == "String" else "Число"
|
|
||||||
piece = f"Номер: {nt}({inner_text(num_len)})"
|
|
||||||
if num_per is not None:
|
|
||||||
per_ru = number_period_map.get(inner_text(num_per), inner_text(num_per))
|
|
||||||
piece += f", {per_ru}"
|
|
||||||
if auto_num is not None and inner_text(auto_num) == "true":
|
|
||||||
piece += ", авто"
|
|
||||||
parts.append(piece)
|
|
||||||
if posting is not None:
|
if posting is not None:
|
||||||
parts.append(f"Проведение: {'да' if inner_text(posting) == 'Allow' else 'нет'}")
|
parts.append(f"Проведение: {'да' if inner_text(posting) == 'Allow' else 'нет'}")
|
||||||
if parts:
|
if parts:
|
||||||
out(" | ".join(parts))
|
out(" | ".join(parts))
|
||||||
|
|
||||||
# Catalog-specific header
|
# Свойства иерархии и подчинения. Иерархия — не только у справочников: свойство
|
||||||
|
# Hierarchical есть и у ПВХ (в корпусе acc/erp/ut/unf их 13), а без этой строки
|
||||||
|
# в full появлялся Родитель, происхождение которого было ниоткуда не видно.
|
||||||
|
parts = []
|
||||||
|
hier = find(props, "md:Hierarchical")
|
||||||
|
if hier is not None and inner_text(hier) == "true":
|
||||||
|
ht = find(props, "md:HierarchyType")
|
||||||
|
ht_text = "группы и элементы" if ht is not None and inner_text(ht) == "HierarchyFoldersAndItems" else "элементы"
|
||||||
|
limit_node = find(props, "md:LimitLevelCount")
|
||||||
|
level_node = find(props, "md:LevelCount")
|
||||||
|
if limit_node is not None and inner_text(limit_node) == "true" and level_node is not None:
|
||||||
|
ht_text += f", уровней: {inner_text(level_node)}"
|
||||||
|
else:
|
||||||
|
ht_text += ", без ограничения уровней"
|
||||||
|
parts.append(f"Иерархический: {ht_text}")
|
||||||
|
# Код и Наименование уехали в блок стандартных реквизитов — здесь только свойства объекта.
|
||||||
|
# Подчинение печатаем лишь когда оно отличается от дефолта ToItems.
|
||||||
if md_type == "Catalog":
|
if md_type == "Catalog":
|
||||||
parts = []
|
sub = find(props, "md:SubordinationUse")
|
||||||
hier = find(props, "md:Hierarchical")
|
if sub is not None and inner_text(sub) != "ToItems":
|
||||||
if hier is not None and inner_text(hier) == "true":
|
sub_ru = {"ToFolders": "группам", "ToFoldersAndItems": "группам и элементам"}.get(
|
||||||
ht = find(props, "md:HierarchyType")
|
inner_text(sub), inner_text(sub))
|
||||||
ht_text = "группы и элементы" if ht is not None and inner_text(ht) == "HierarchyFoldersAndItems" else "элементы"
|
parts.append(f"Подчинение: {sub_ru}")
|
||||||
limit_node = find(props, "md:LimitLevelCount")
|
if parts:
|
||||||
level_node = find(props, "md:LevelCount")
|
out(" | ".join(parts))
|
||||||
if limit_node is not None and inner_text(limit_node) == "true" and level_node is not None:
|
|
||||||
ht_text += f", уровней: {inner_text(level_node)}"
|
|
||||||
else:
|
|
||||||
ht_text += ", без ограничения уровней"
|
|
||||||
parts.append(f"Иерархический: {ht_text}")
|
|
||||||
code_len = find(props, "md:CodeLength")
|
|
||||||
desc_len = find(props, "md:DescriptionLength")
|
|
||||||
if code_len is not None and inner_text(code_len).isdigit() and int(inner_text(code_len)) > 0:
|
|
||||||
parts.append(f"Код({inner_text(code_len)})")
|
|
||||||
if desc_len is not None and inner_text(desc_len).isdigit() and int(inner_text(desc_len)) > 0:
|
|
||||||
parts.append(f"Наименование({inner_text(desc_len)})")
|
|
||||||
if parts:
|
|
||||||
out(" | ".join(parts))
|
|
||||||
|
|
||||||
# Register-specific header
|
# Register-specific header
|
||||||
if md_type.endswith("Register"):
|
if md_type.endswith("Register"):
|
||||||
@@ -1067,6 +1332,17 @@ if not drill_done:
|
|||||||
syn_text = f'"{v["Synonym"]}"' if v["Synonym"] and v["Synonym"] != v["Name"] else ""
|
syn_text = f'"{v["Synonym"]}"' if v["Synonym"] and v["Synonym"] != v["Name"] else ""
|
||||||
out(f" {padded} {syn_text}")
|
out(f" {padded} {syn_text}")
|
||||||
|
|
||||||
|
# Standard attributes
|
||||||
|
std_attrs = get_standard_attributes(props, md_type, mode)
|
||||||
|
if mode == "full":
|
||||||
|
std_attrs += get_standard_attributes_full(props, md_type, obj_name)
|
||||||
|
if std_attrs:
|
||||||
|
out("")
|
||||||
|
out("Стандартные реквизиты:")
|
||||||
|
ml = get_max_name_len(std_attrs)
|
||||||
|
for s in std_attrs:
|
||||||
|
out(format_attr_line(s, ml))
|
||||||
|
|
||||||
# Dimensions (registers)
|
# Dimensions (registers)
|
||||||
if md_type.endswith("Register") and child_objs is not None:
|
if md_type.endswith("Register") and child_objs is not None:
|
||||||
dims = get_attributes(child_objs, "Dimension", True)
|
dims = get_attributes(child_objs, "Dimension", True)
|
||||||
@@ -1174,6 +1450,12 @@ if not drill_done:
|
|||||||
if commands:
|
if commands:
|
||||||
out(f"Команды: {', '.join(commands)}")
|
out(f"Команды: {', '.join(commands)}")
|
||||||
|
|
||||||
|
# Единственная подсказка на весь вывод — чем развернуть свёрнутый в счётчик состав
|
||||||
|
if collapsed_names:
|
||||||
|
hints = ", ".join(f"-Name {n}" for n in collapsed_names)
|
||||||
|
out("")
|
||||||
|
out(f"Полный состав типов: {hints}")
|
||||||
|
|
||||||
# ── Pagination and output ────────────────────────────────────
|
# ── Pagination and output ────────────────────────────────────
|
||||||
|
|
||||||
total_lines = len(lines)
|
total_lines = len(lines)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-remove v1.5 — Remove metadata object from 1C configuration dump
|
# meta-remove v1.9 — Remove metadata object from 1C configuration dump
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -493,9 +493,29 @@ if (-not $cfgNode) {
|
|||||||
# Save Configuration.xml
|
# Save Configuration.xml
|
||||||
if ($actions -gt 0 -and -not $DryRun) {
|
if ($actions -gt 0 -and -not $DryRun) {
|
||||||
$enc = New-Object System.Text.UTF8Encoding $true
|
$enc = New-Object System.Text.UTF8Encoding $true
|
||||||
$sw = New-Object System.IO.StreamWriter($configXml, $false, $enc)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$xmlDoc.Save($sw)
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$sw.Close()
|
$settings.Encoding = $enc
|
||||||
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
|
$xmlDoc.Save($writer)
|
||||||
|
$writer.Flush(); $writer.Close()
|
||||||
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $configXml) -and ([System.IO.File]::ReadAllText($configXml) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($configXml, $xmlText, $enc)
|
||||||
Write-Host "[OK] Configuration.xml saved"
|
Write-Host "[OK] Configuration.xml saved"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,9 +579,29 @@ function Remove-FromSubsystems {
|
|||||||
|
|
||||||
if ($modified -and -not $DryRun) {
|
if ($modified -and -not $DryRun) {
|
||||||
$enc = New-Object System.Text.UTF8Encoding $true
|
$enc = New-Object System.Text.UTF8Encoding $true
|
||||||
$sw = New-Object System.IO.StreamWriter($xmlFile.FullName, $false, $enc)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$ssDoc.Save($sw)
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$sw.Close()
|
$settings.Encoding = $enc
|
||||||
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
|
$ssDoc.Save($writer)
|
||||||
|
$writer.Flush(); $writer.Close()
|
||||||
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $xmlFile.FullName) -and ([System.IO.File]::ReadAllText($xmlFile.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($xmlFile.FullName, $xmlText, $enc)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Recurse into child subsystems
|
# Recurse into child subsystems
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# meta-remove v1.5 — Remove metadata object from 1C configuration dump
|
# meta-remove v1.9 — Remove metadata object from 1C configuration dump
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
import shutil
|
import shutil
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -292,21 +314,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -330,7 +353,7 @@ def main():
|
|||||||
parser.add_argument("-DryRun", action="store_true")
|
parser.add_argument("-DryRun", action="store_true")
|
||||||
parser.add_argument("-KeepFiles", action="store_true")
|
parser.add_argument("-KeepFiles", action="store_true")
|
||||||
parser.add_argument("-Force", action="store_true")
|
parser.add_argument("-Force", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
config_dir = args.ConfigDir
|
config_dir = args.ConfigDir
|
||||||
if not os.path.isabs(config_dir):
|
if not os.path.isabs(config_dir):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-validate v1.13 — Validate 1C metadata object structure (+корневой <Type>: скаляр без структуры = ошибка)
|
# meta-validate v1.19 — Validate 1C metadata object structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -153,6 +153,19 @@ $finalize = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
|
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||||
@@ -209,7 +222,7 @@ $standardAttributesByType = @{
|
|||||||
"Enum" = @("Order","Ref")
|
"Enum" = @("Order","Ref")
|
||||||
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
|
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
|
||||||
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType")
|
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType")
|
||||||
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account")
|
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account","PeriodAdjustment","RecordType")
|
||||||
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")
|
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")
|
||||||
"ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance")
|
"ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance")
|
||||||
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType")
|
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType")
|
||||||
@@ -220,6 +233,14 @@ $standardAttributesByType = @{
|
|||||||
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
|
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
|
||||||
|
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
|
||||||
|
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
|
||||||
|
$stdAttrConditionalNames = @{
|
||||||
|
"AccountingRegister" = @("PeriodAdjustment","RecordType")
|
||||||
|
"AccumulationRegister" = @("RecordType")
|
||||||
|
}
|
||||||
|
|
||||||
# Types that have StandardAttributes block
|
# Types that have StandardAttributes block
|
||||||
$typesWithStdAttrs = @(
|
$typesWithStdAttrs = @(
|
||||||
"Catalog","Document","Enum",
|
"Catalog","Document","Enum",
|
||||||
@@ -279,7 +300,7 @@ $validPropertyValues = @{
|
|||||||
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
|
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
|
||||||
"RegisterType" = @("Balance","Turnovers")
|
"RegisterType" = @("Balance","Turnovers")
|
||||||
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
|
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
|
||||||
"ReuseSessions" = @("DontUse","AutoUse")
|
"ReuseSessions" = @("DontUse","Use","AutoUse")
|
||||||
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
|
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
|
||||||
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
|
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
|
||||||
"DataHistory" = @("Use","DontUse")
|
"DataHistory" = @("Use","DontUse")
|
||||||
@@ -342,11 +363,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
|
|
||||||
# Version attribute
|
# Version attribute
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
|
$versionRank = Get-FormatRank $version
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20")) {
|
} elseif ($versionRank -eq 0) {
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
Report-Error "1. Malformed version '$version' (expected N.N)"
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20)"
|
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
|
||||||
|
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
|
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type element — exactly one child element in md namespace
|
# Detect type element — exactly one child element in md namespace
|
||||||
@@ -606,11 +631,9 @@ if ($typesWithStdAttrs -contains $mdType) {
|
|||||||
if ($saName) {
|
if ($saName) {
|
||||||
$foundNames += $saName
|
$foundNames += $saName
|
||||||
if ($expectedStdAttrs -notcontains $saName) {
|
if ($expectedStdAttrs -notcontains $saName) {
|
||||||
# AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} and optional PeriodAdjustment
|
# AccountingRegister: пары субконто, число которых задаётся планом счетов
|
||||||
$isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment"))
|
$isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$'))
|
||||||
# CalculationRegister has conditional period attrs
|
if (-not $isDynamic) {
|
||||||
$isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod"))
|
|
||||||
if (-not $isDynamic -and -not $isCalcDynamic) {
|
|
||||||
Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType"
|
Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,7 +644,8 @@ if ($typesWithStdAttrs -contains $mdType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($expectedStdAttrs) {
|
if ($expectedStdAttrs) {
|
||||||
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ })
|
$condNames = $stdAttrConditionalNames[$mdType]; if (-not $condNames) { $condNames = @() }
|
||||||
|
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ -and $condNames -notcontains $_ })
|
||||||
if ($missingAttrs.Count -gt 0) {
|
if ($missingAttrs.Count -gt 0) {
|
||||||
Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')"
|
Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')"
|
||||||
}
|
}
|
||||||
@@ -1502,13 +1526,12 @@ if ($script:configDir) {
|
|||||||
$versionedProps = @{
|
$versionedProps = @{
|
||||||
"TypeReductionMode" = "2.18" # режим приведения типов (стандартные реквизиты, измерения РС)
|
"TypeReductionMode" = "2.18" # режим приведения типов (стандартные реквизиты, измерения РС)
|
||||||
"LineNumberLength" = "2.20" # длина номера строки ТЧ (5..9)
|
"LineNumberLength" = "2.20" # длина номера строки ТЧ (5..9)
|
||||||
|
# 2.21 (8.5): подтверждено синтетикой — одни исходники, выгрузка с 8.3.27 и с 8.5.1.
|
||||||
|
"Color" = "2.21" # цвет значения перечисления
|
||||||
|
"AuxiliaryVariantForm" = "2.21" # вспомогательная форма варианта отчёта
|
||||||
|
"UseInInterfaceCompatibilityMode" = "2.21" # использование общей формы в режиме совместимости интерфейса
|
||||||
}
|
}
|
||||||
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
$fileRank = $versionRank
|
||||||
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) {
|
if ($fileRank -gt 0) {
|
||||||
foreach ($vp in ($versionedProps.Keys | Sort-Object)) {
|
foreach ($vp in ($versionedProps.Keys | Sort-Object)) {
|
||||||
$nodes = $xmlDoc.SelectNodes("//md:$vp | //xr:$vp", $ns)
|
$nodes = $xmlDoc.SelectNodes("//md:$vp | //xr:$vp", $ns)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# meta-validate v1.13 — Validate 1C metadata object structure (Python port) (+корневой <Type>: скаляр без структуры = ошибка)
|
# meta-validate v1.19 — Validate 1C metadata object structure (Python port)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -11,6 +11,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# ── arg parsing ──────────────────────────────────────────────
|
# ── arg parsing ──────────────────────────────────────────────
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||||
@@ -18,7 +40,7 @@ parser.add_argument("-ObjectPath", "-Path", required=True)
|
|||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
parser.add_argument("-OutFile", default="")
|
parser.add_argument("-OutFile", default="")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
detailed = args.Detailed
|
detailed = args.Detailed
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
@@ -153,6 +175,21 @@ def finalize():
|
|||||||
print(f"Written to: {out_file}")
|
print(f"Written to: {out_file}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
# ── Reference tables ─────────────────────────────────────────
|
# ── Reference tables ─────────────────────────────────────────
|
||||||
|
|
||||||
guid_pattern = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
guid_pattern = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
||||||
@@ -209,7 +246,7 @@ standard_attributes_by_type = {
|
|||||||
"Enum": ["Order", "Ref"],
|
"Enum": ["Order", "Ref"],
|
||||||
"InformationRegister": ["Active", "LineNumber", "Recorder", "Period"],
|
"InformationRegister": ["Active", "LineNumber", "Recorder", "Period"],
|
||||||
"AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"],
|
"AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"],
|
||||||
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account"],
|
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account", "PeriodAdjustment", "RecordType"],
|
||||||
"CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"],
|
"CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"],
|
||||||
"ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"],
|
"ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"],
|
||||||
"ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"],
|
"ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"],
|
||||||
@@ -220,6 +257,14 @@ standard_attributes_by_type = {
|
|||||||
"DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"],
|
"DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
|
||||||
|
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
|
||||||
|
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
|
||||||
|
std_attr_conditional_names = {
|
||||||
|
"AccountingRegister": ("PeriodAdjustment", "RecordType"),
|
||||||
|
"AccumulationRegister": ("RecordType",),
|
||||||
|
}
|
||||||
|
|
||||||
# Types that have StandardAttributes block
|
# Types that have StandardAttributes block
|
||||||
types_with_std_attrs = (
|
types_with_std_attrs = (
|
||||||
"Catalog", "Document", "Enum",
|
"Catalog", "Document", "Enum",
|
||||||
@@ -281,7 +326,7 @@ valid_property_values = {
|
|||||||
"InformationRegisterPeriodicity": ["Nonperiodical", "Second", "Day", "Month", "Quarter", "Year", "RecorderPosition"],
|
"InformationRegisterPeriodicity": ["Nonperiodical", "Second", "Day", "Month", "Quarter", "Year", "RecorderPosition"],
|
||||||
"RegisterType": ["Balance", "Turnovers"],
|
"RegisterType": ["Balance", "Turnovers"],
|
||||||
"ReturnValuesReuse": ["DontUse", "DuringRequest", "DuringSession"],
|
"ReturnValuesReuse": ["DontUse", "DuringRequest", "DuringSession"],
|
||||||
"ReuseSessions": ["DontUse", "AutoUse"],
|
"ReuseSessions": ["DontUse", "Use", "AutoUse"],
|
||||||
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
|
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
|
||||||
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
|
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
|
||||||
"DataHistory": ["Use", "DontUse"],
|
"DataHistory": ["Use", "DontUse"],
|
||||||
@@ -369,11 +414,17 @@ if root_ns != expected_ns:
|
|||||||
|
|
||||||
# Version attribute
|
# Version attribute
|
||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
report_warn("1. Missing version attribute on MetaDataObject")
|
report_warn("1. Missing version attribute on MetaDataObject")
|
||||||
elif version not in ("2.17", "2.18", "2.19", "2.20"):
|
elif version_rank == 0:
|
||||||
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
report_error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20)")
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
report_warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
report_warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Detect type element -- exactly one child element in md namespace
|
# Detect type element -- exactly one child element in md namespace
|
||||||
type_node = None
|
type_node = None
|
||||||
@@ -594,23 +645,19 @@ if md_type in types_with_std_attrs:
|
|||||||
if sa_name:
|
if sa_name:
|
||||||
found_names.append(sa_name)
|
found_names.append(sa_name)
|
||||||
if sa_name not in expected_std_attrs:
|
if sa_name not in expected_std_attrs:
|
||||||
# AccountingRegister has dynamic attrs
|
# AccountingRegister: пары субконто, число которых задаётся планом счетов
|
||||||
is_dynamic = (md_type == "AccountingRegister" and
|
is_dynamic = (md_type == "AccountingRegister" and
|
||||||
(re.match(r'^ExtDimension\d+$', sa_name) or
|
(re.match(r'^ExtDimension\d+$', sa_name) or
|
||||||
re.match(r'^ExtDimensionType\d+$', sa_name) or
|
re.match(r'^ExtDimensionType\d+$', sa_name)))
|
||||||
sa_name == "PeriodAdjustment"))
|
if not is_dynamic:
|
||||||
# CalculationRegister has conditional period attrs
|
|
||||||
is_calc_dynamic = (md_type == "CalculationRegister" and
|
|
||||||
sa_name in ("ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod",
|
|
||||||
"BegOfBasePeriod", "EndOfBasePeriod"))
|
|
||||||
if not is_dynamic and not is_calc_dynamic:
|
|
||||||
report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}")
|
report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}")
|
||||||
else:
|
else:
|
||||||
report_error("5. StandardAttribute without 'name' attribute")
|
report_error("5. StandardAttribute without 'name' attribute")
|
||||||
check5_ok = False
|
check5_ok = False
|
||||||
|
|
||||||
if expected_std_attrs:
|
if expected_std_attrs:
|
||||||
missing_attrs = [a for a in expected_std_attrs if a not in found_names]
|
cond_names = std_attr_conditional_names.get(md_type, ())
|
||||||
|
missing_attrs = [a for a in expected_std_attrs if a not in found_names and a not in cond_names]
|
||||||
if missing_attrs:
|
if missing_attrs:
|
||||||
report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}")
|
report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}")
|
||||||
|
|
||||||
@@ -1404,16 +1451,13 @@ if config_dir:
|
|||||||
versioned_props = {
|
versioned_props = {
|
||||||
"TypeReductionMode": "2.18", # режим приведения типов (стандартные реквизиты, измерения РС)
|
"TypeReductionMode": "2.18", # режим приведения типов (стандартные реквизиты, измерения РС)
|
||||||
"LineNumberLength": "2.20", # длина номера строки ТЧ (5..9)
|
"LineNumberLength": "2.20", # длина номера строки ТЧ (5..9)
|
||||||
|
# 2.21 (8.5): подтверждено синтетикой — одни исходники, выгрузка с 8.3.27 и с 8.5.1.
|
||||||
|
"Color": "2.21", # цвет значения перечисления
|
||||||
|
"AuxiliaryVariantForm": "2.21", # вспомогательная форма варианта отчёта
|
||||||
|
"UseInInterfaceCompatibilityMode": "2.21", # использование общей формы в режиме совместимости интерфейса
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file_rank = version_rank
|
||||||
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:
|
if file_rank > 0:
|
||||||
for vp in sorted(versioned_props):
|
for vp in sorted(versioned_props):
|
||||||
nodes = find_all(root, f"//md:{vp} | //xr:{vp}")
|
nodes = find_all(root, f"//md:{vp} | //xr:{vp}")
|
||||||
|
|||||||
@@ -43,24 +43,49 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
|
|||||||
|
|
||||||
## JSON-схема DSL
|
## JSON-схема DSL
|
||||||
|
|
||||||
Ниже — компактная структура и ключевые правила, достаточные для типового макета. Полные таблицы полей (все свойства шрифтов, стилей, ячеек), развёрнутый пример и ограничения формата — в **`reference/dsl-spec.md`**; нужны не всегда, читать по необходимости.
|
Ниже — компактная структура и ключевые правила, достаточные для типового макета. Подробности читать по необходимости:
|
||||||
|
|
||||||
|
| Что нужно | Файл |
|
||||||
|
|---|---|
|
||||||
|
| Полные таблицы полей, развёрнутый пример, ограничения формата | `reference/dsl-spec.md` |
|
||||||
|
| Шрифты, стили, цвета, рамки, колоночные раскладки и стили колонок | `reference/styles.md` |
|
||||||
|
| Полный перечень свойств стиля — все 44 | `reference/format-properties.md` |
|
||||||
|
|
||||||
Краткая структура:
|
Краткая структура:
|
||||||
|
|
||||||
```
|
```
|
||||||
{ columns, page, defaultWidth, columnWidths,
|
{ columns, page, defaultWidth, columnWidths, columnStyles,
|
||||||
fonts: { name: { face, size, bold, italic, underline, strikeout } },
|
fonts: { name: { face, size, bold, italic, underline, strikeout } | { ref } },
|
||||||
styles: { name: { font, align, valign, border, borderWidth, wrap, format } },
|
styles: { name: { font, horizontalAlignment, verticalAlignment, textPlacement,
|
||||||
areas: [{ name, rows: [{ height, rowStyle, cells: [
|
backColor, textColor, border, borderColor, format, hidden } },
|
||||||
|
areas: [{ name, columnSet, rows: [{ height, hidden, rowStyle, cells: [
|
||||||
{ col, span, rowspan, style, param, detail, text, template }
|
{ col, span, rowspan, style, param, detail, text, template }
|
||||||
]}]}]
|
]}]}],
|
||||||
|
namedAreas: [{ name, rows, cols }],
|
||||||
|
columnSets: { name: { columns, columnWidths, columnStyles } }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключевые правила:
|
Ключевые правила:
|
||||||
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
|
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
|
||||||
- `col` — 1-based позиция колонки
|
- `name` у области в `areas` необязателен: область без имени — просто кусок сетки, именованной она не станет
|
||||||
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
|
- `namedAreas` — области, которые не описываются диапазоном подряд идущих строк: полоса колонок, прямоугольник, ячейка. Тип не указывается, он следует из того, какие оси заданы
|
||||||
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
|
- `columnSet` у области — ссылка на раскладку из `columnSets`, когда группе строк нужны свои ширины колонок; без него действует документная раскладка
|
||||||
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
|
- Ключ стиля — имя свойства как в выгрузке; `columnStyles` вешает стиль на колонку так же, как `style` на ячейку
|
||||||
|
- Рамка — `border` (все стороны) или `leftBorder`/`topBorder`/`rightBorder`/`bottomBorder`; значение `"Solid"` либо `{ style, width }`
|
||||||
|
- `rowStyle` — стиль строки: ложится и на строку, и на все её колонки, заполняя пустоты (рамки по всей ширине)
|
||||||
|
- `height` и `hidden` — собственные свойства строки, у ячейки таких нет
|
||||||
- `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`)
|
- `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`)
|
||||||
|
- Строку можно писать массивом ячеек — позиция из порядка, `col` не нужен: `"текст"`, `"{Имя}"` — параметр, `">"` — продолжить ячейку слева, `"|"` — сверху, `null` — пропуск колонки
|
||||||
|
- `col` — 1-based позиция колонки
|
||||||
|
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
|
||||||
|
- Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]`
|
||||||
|
|
||||||
|
Двухуровневая шапка массивами:
|
||||||
|
```json
|
||||||
|
"rows": [
|
||||||
|
["Вид", "Остаток", ">", "Итог"],
|
||||||
|
["|", "начало", "конец", "|"],
|
||||||
|
["{Вид}", "{Нач}", "{Кон}", "{Итог}"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML).
|
Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML).
|
||||||
|
|
||||||
|
Оформление — шрифты, стили, цвета, рамки, колоночные раскладки — в `styles.md`;
|
||||||
|
полный перечень свойств стиля — в `format-properties.md`.
|
||||||
|
|
||||||
## Пример
|
## Пример
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -18,11 +21,11 @@
|
|||||||
|
|
||||||
"styles": {
|
"styles": {
|
||||||
"default": {},
|
"default": {},
|
||||||
"header": { "font": "header", "align": "center" },
|
"header": { "font": "header", "horizontalAlignment": "Center" },
|
||||||
"label": { "font": "bold" },
|
"label": { "font": "bold" },
|
||||||
"bordered": { "border": "all" },
|
"bordered": { "border": "Solid" },
|
||||||
"bordered-right": { "border": "all", "align": "right" },
|
"bordered-right": { "border": "Solid", "horizontalAlignment": "Right" },
|
||||||
"total-right": { "font": "bold", "border": "top", "align": "right" }
|
"total-right": { "font": "bold", "topBorder": "Solid", "horizontalAlignment": "Right" }
|
||||||
},
|
},
|
||||||
|
|
||||||
"areas": [
|
"areas": [
|
||||||
@@ -73,88 +76,166 @@
|
|||||||
|
|
||||||
| Поле | Обяз. | По умолч. | Описание |
|
| Поле | Обяз. | По умолч. | Описание |
|
||||||
|------|:-----:|-----------|----------|
|
|------|:-----:|-----------|----------|
|
||||||
| `columns` | да | — | Количество колонок |
|
| `columns` | да | — | Количество колонок в раскладке по умолчанию. `0` допустимо: значит, все строки живут в раскладках из `columnSets` |
|
||||||
| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` |
|
| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` |
|
||||||
| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` |
|
| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` |
|
||||||
| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
|
| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
|
||||||
|
| `columnStyles` | нет | — | Оформление колонок: те же ключи, значение — имя стиля (см. `styles.md`) |
|
||||||
|
| `textLanguages` | нет | `["ru"]` | Языки, на которых пишется текст, заданный строкой (см. ниже) |
|
||||||
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
|
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
|
||||||
| `styles` | нет | `{}` | Именованные стили |
|
| `styles` | нет | `{}` | Именованные стили (см. `styles.md`) |
|
||||||
| `areas` | да | — | Массив именованных областей (порядок = порядок в документе) |
|
| `areas` | да | — | Массив областей — диапазонов подряд идущих строк (порядок = порядок в документе); имя необязательно |
|
||||||
|
| `namedAreas` | нет | — | Именованные области, заданные координатами (см. ниже) |
|
||||||
## Шрифты (`fonts.<name>`)
|
| `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `styles.md`) |
|
||||||
|
|
||||||
| Поле | По умолч. | Описание |
|
|
||||||
|------|-----------|----------|
|
|
||||||
| `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[]`)
|
## Области (`areas[]`)
|
||||||
|
|
||||||
| Поле | Обяз. | Описание |
|
| Поле | Обяз. | Описание |
|
||||||
|------|:-----:|----------|
|
|------|:-----:|----------|
|
||||||
| `name` | да | Имя области для `Макет.ПолучитьОбласть("Имя")` |
|
| `name` | нет | Имя области для `Макет.ПолучитьОбласть("Имя")` |
|
||||||
|
| `columnSet` | нет | Ссылка на раскладку из `columnSets` |
|
||||||
| `rows` | да | Массив строк |
|
| `rows` | да | Массив строк |
|
||||||
|
|
||||||
|
Макет собирается из областей — диапазонов подряд идущих строк. Имя делает область именованной: она доступна в коде как `Макет.ПолучитьОбласть("Имя")` и занимает строки своего диапазона. **Область без имени** — просто кусок сетки: так описываются строки, не принадлежащие ни одной именованной области.
|
||||||
|
|
||||||
|
## Именованные области координатами (`namedAreas[]`)
|
||||||
|
|
||||||
|
Для областей, которые диапазоном подряд идущих строк не описываются: полоса колонок, прямоугольник, ячейка, а также пересекающиеся с другими.
|
||||||
|
|
||||||
|
| Поле | Обяз. | Описание |
|
||||||
|
|------|:-----:|----------|
|
||||||
|
| `name` | да | Имя области |
|
||||||
|
| `rows` | \* | Строки: число или диапазон `"N-M"`, 1-based |
|
||||||
|
| `cols` | \* | Колонки: число или диапазон `"N-M"`, 1-based |
|
||||||
|
|
||||||
|
\* Обязательна хотя бы одна из осей.
|
||||||
|
|
||||||
|
**Тип области не указывается** — он следует из того, какие оси заданы, как в `ТабличныйДокумент.Область()`: только строки → полоса строк, только колонки → полоса колонок, обе оси → прямоугольник, одиночные значения по обеим осям → одна ячейка.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"namedAreas": [
|
||||||
|
{ "name": "ОбластьПечатиПоВысоте", "rows": "1-48" },
|
||||||
|
{ "name": "ОбластьПечатиПоШирине", "cols": "1-35" },
|
||||||
|
{ "name": "HZY", "rows": 9, "cols": "16-17" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Диапазон — та же грамматика, что у `columnWidths`, но **только** число или `"N-M"`: список через запятую запрещён, область непрерывна. Имя обязательно, и хотя бы одна ось должна быть задана; нарушение любого из этих правил → ненулевой код выхода и сообщение в stderr.
|
||||||
|
|
||||||
## Строки (`rows[]`)
|
## Строки (`rows[]`)
|
||||||
|
|
||||||
| Поле | По умолч. | Описание |
|
| Поле | По умолч. | Описание |
|
||||||
|------|-----------|----------|
|
|------|-----------|----------|
|
||||||
| `height` | — | Высота строки (если не задана, используется авто) |
|
| `height` | — | Высота строки (если не задана, используется авто) |
|
||||||
| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) |
|
| `hidden` | `false` | Скрыть строку |
|
||||||
|
| `rowStyle` | — | Стиль строки: ложится и на саму строку, и на ВСЕ её колонки (заполняет пустоты рамками) |
|
||||||
| `cells` | `[]` | Массив ячеек |
|
| `cells` | `[]` | Массив ячеек |
|
||||||
| `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) |
|
| `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) |
|
||||||
|
|
||||||
Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`.
|
Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`.
|
||||||
|
|
||||||
|
`height` и `hidden` — собственные свойства строки: у ячейки таких нет, и в её оформление они
|
||||||
|
не попадают. Всё остальное оформление строки задаётся через `rowStyle`.
|
||||||
|
|
||||||
|
### Короткая форма: строка массивом
|
||||||
|
|
||||||
|
Вместо объекта строка может быть массивом ячеек — позиция определяется порядком, `col` не указывается.
|
||||||
|
|
||||||
|
| Элемент | Значение |
|
||||||
|
|---------|----------|
|
||||||
|
| `"текст"` | Статический текст (`text`) |
|
||||||
|
| `{ "ru": "…", "en": "…" }` | Тот же текст на нескольких языках |
|
||||||
|
| `"{Имя}"` | Параметр (`param`) |
|
||||||
|
| `">"` | Продолжение ячейки слева — увеличивает её `span` |
|
||||||
|
| `"|"` | Продолжение ячейки сверху — увеличивает её `rowspan` |
|
||||||
|
| `null` | Пустая колонка: позиция занята, ячейка не создаётся |
|
||||||
|
| `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` |
|
||||||
|
|
||||||
|
Объект-элемент трактуется по его ключам: если среди них есть ключ ячейки (`span`, `rowspan`,
|
||||||
|
`style`, `param`, `detail`, `text`, `template`) — объект описывает свойства ячейки. Иначе он
|
||||||
|
целиком считается её текстом, а его ключи — идентификаторами языков.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"rows": [
|
||||||
|
["Вид", "Остаток", ">", "Итог"],
|
||||||
|
["|", "начало", "конец", "|"],
|
||||||
|
["{Вид}", "{Нач}", "{Кон}", "{Итог}"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
Здесь «Вид» и «Итог» объединены по вертикали на две строки, «Остаток» — по горизонтали на две колонки.
|
||||||
|
|
||||||
|
Ограничения короткой формы:
|
||||||
|
- не задать `height` и `rowStyle` — это свойства строки, а не ячейки;
|
||||||
|
- не выразить текст, совпадающий с `">"`, `"|"` или с шаблоном `"{...}"`.
|
||||||
|
|
||||||
|
Маркеру нужно, что продолжать: `">"` требует ячейку слева в той же строке, `"|"` — ячейку сверху. Объектный элемент не должен нести `col`: позиция уже задана порядком. Число элементов не может превышать `columns`. Нарушение любого из этих правил → ненулевой код выхода и сообщение в stderr.
|
||||||
|
|
||||||
## Ячейки (`cells[]`)
|
## Ячейки (`cells[]`)
|
||||||
|
|
||||||
| Поле | Обяз. | По умолч. | Описание |
|
| Поле | Обяз. | По умолч. | Описание |
|
||||||
|------|:-----:|-----------|----------|
|
|------|:-----:|-----------|----------|
|
||||||
| `col` | да | — | Позиция колонки (1-based) |
|
| `col` | да | — | Позиция колонки (1-based). В короткой форме строки не указывается — позиция берётся из порядка |
|
||||||
| `span` | нет | `1` | Объединение по горизонтали (количество колонок) |
|
| `span` | нет | `1` | Объединение по горизонтали (количество колонок) |
|
||||||
| `rowspan` | нет | `1` | Объединение по вертикали (количество строк) |
|
| `rowspan` | нет | `1` | Объединение по вертикали (количество строк) |
|
||||||
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
|
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
|
||||||
| `param` | нет | — | Параметр заполнения |
|
| `param` | нет | — | Параметр заполнения |
|
||||||
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
|
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
|
||||||
| `text` | нет | — | Статический текст |
|
| `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже |
|
||||||
| `template` | нет | — | Шаблонный текст с `[Параметр]` |
|
| `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` |
|
||||||
|
|
||||||
### Тип заполнения
|
### Содержимое ячейки
|
||||||
|
|
||||||
Определяется автоматически по содержимому ячейки:
|
Задаётся ровно одним из ключей, объявлять способ заполнения отдельно не нужно:
|
||||||
- `param` → fillType=Parameter
|
- `param` — параметр заполнения;
|
||||||
- `template` → fillType=Template
|
- `template` — текст со вставками `[Параметр]`;
|
||||||
- `text` → fillType=Text
|
- `text` — статический текст;
|
||||||
- ничего → без fillType (пустая ячейка или рамка)
|
- ничего — пустая ячейка (нужна, например, ради рамки).
|
||||||
|
|
||||||
## `rowStyle` — автозаполнение
|
### Текст на нескольких языках
|
||||||
|
|
||||||
Когда задан `rowStyle`, компилятор создаёт ячейки для ВСЕХ колонок строки. Позиции без явных ячеек заполняются пустыми ячейками с указанным стилем. Это обеспечивает сплошные рамки в табличных строках.
|
`text` и `template` принимают строку или объект «язык → текст». Объект даёт по надписи на каждый язык, в порядке ключей. Строка означает один и тот же текст на всех языках макета — по умолчанию только русский.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "col": 1, "text": "Наименование" }
|
||||||
|
{ "col": 2, "text": { "ru": "Поставщик", "en": "Supplier" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Набор языков задаётся документным ключом `textLanguages`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "columns": 3, "textLanguages": ["ru", "en"], "areas": [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
С таким объявлением `"Наименование"` из примера выше даст надпись и под `ru`, и под `en`.
|
||||||
|
|
||||||
|
Ключ ни на что в конфигурации не смотрит — это просто список языков, на которые разворачивается строка.
|
||||||
|
|
||||||
|
Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста.
|
||||||
|
|
||||||
|
## `rowStyle` — оформление строки
|
||||||
|
|
||||||
|
Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком.
|
||||||
|
|
||||||
|
Стиль конкретной ячейки (`style`) перекрывает `rowStyle` для этой ячейки.
|
||||||
|
|
||||||
Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются.
|
Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются.
|
||||||
|
|
||||||
## Ограничения
|
## Ограничения
|
||||||
|
|
||||||
Текущая версия не поддерживает:
|
DSL описывает не все конструкции табличного документа. Перечисленное ниже **теряется при
|
||||||
- Множественные наборы колонок (`columnsID`)
|
round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попадает, в сгенерированный
|
||||||
- Области типа Columns / Rectangle
|
XML не возвращается.
|
||||||
- Рисунки (штрихкоды, картинки)
|
|
||||||
- Фон ячеек
|
- ячейки-поля ввода (`containsValue` / `valueType` / `controlType`);
|
||||||
|
- объединения, не привязанные к ячейке (по всей высоте или ширине документа);
|
||||||
|
- рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам;
|
||||||
|
- группировки строк и колонок;
|
||||||
|
- колонтитулы, параметры печати, область печати.
|
||||||
|
|
||||||
|
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
|
||||||
|
diff после round-trip обычно шире фактической доработки.
|
||||||
|
|
||||||
|
Отдельно про побайтовое совпадение. В макетах, которые долго правили в Конфигураторе,
|
||||||
|
встречаются следы прежних состояний: формат ячейки может нести ширину колонки, которая с тех
|
||||||
|
пор изменилась. Такие значения не описывают итоговый документ и из него не выводятся, поэтому
|
||||||
|
собранный XML совпадёт с исходным не всегда — при полностью сохранённом содержании.
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Полный список свойств стиля
|
||||||
|
|
||||||
|
Имя ключа совпадает с именем свойства в выгрузке — исключений нет. Частые свойства
|
||||||
|
с примерами — в `styles.md`, здесь полный перечень.
|
||||||
|
|
||||||
|
Тип значения:
|
||||||
|
|
||||||
|
- **число** — целое;
|
||||||
|
- **да/нет** — `true` / `false`;
|
||||||
|
- **перечисление** — одно из указанных, регистр не важен;
|
||||||
|
- **цвет** — `#RRGGBB`, `style:Имя`, `web:Имя`, `win:Имя`;
|
||||||
|
- **линия** — `"Solid"` либо `{ style, width }`;
|
||||||
|
- **текст** — строка (разворачивается на языки макета).
|
||||||
|
|
||||||
|
## Текст и выравнивание
|
||||||
|
|
||||||
|
| Ключ | Тип | Значение |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `font` | имя | Ссылка на имя из `fonts` |
|
||||||
|
| `horizontalAlignment` | перечисление | `Left`, `Center`, `Right`, `Justify`, `Auto` |
|
||||||
|
| `verticalAlignment` | перечисление | `Top`, `Center`, `Bottom` |
|
||||||
|
| `textPlacement` | перечисление | `Wrap`, `Cut`, `Block`, `Auto` |
|
||||||
|
| `textOrientation` | число | Поворот в десятых долях градуса: `900` = 90° |
|
||||||
|
| `textColor` | цвет | Цвет текста |
|
||||||
|
| `indent` | число | Отступ текста |
|
||||||
|
| `autoIndent` | число | Автоматический отступ |
|
||||||
|
| `format` | текст | Формат данных: `"ЧЦ=15; ЧДЦ=2"` |
|
||||||
|
| `editFormat` | текст | Формат редактирования |
|
||||||
|
| `mask` | текст | Маска ввода |
|
||||||
|
| `markNegatives` | да/нет | Выделять отрицательные |
|
||||||
|
|
||||||
|
## Фон и рамка
|
||||||
|
|
||||||
|
| Ключ | Тип | Значение |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `backColor` | цвет | Цвет фона |
|
||||||
|
| `pattern` | перечисление | `Solid`, `WithoutPattern`, `Pattern7`, `Pattern10`, `Pattern12`, `Pattern13`, `Pattern14`, `Pattern16` |
|
||||||
|
| `patternColor` | цвет | Цвет узора |
|
||||||
|
| `border` | линия | Все четыре стороны |
|
||||||
|
| `leftBorder`, `topBorder`, `rightBorder`, `bottomBorder` | линия | Отдельная сторона |
|
||||||
|
| `borderColor` | цвет | Цвет рамки |
|
||||||
|
|
||||||
|
## Поведение
|
||||||
|
|
||||||
|
| Ключ | Тип | Значение |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `hidden` | да/нет | Скрыть |
|
||||||
|
| `protection` | да/нет | Защита от редактирования |
|
||||||
|
| `print` | да/нет | Выводить на печать |
|
||||||
|
| `hyperLink` | да/нет | Гиперссылка |
|
||||||
|
| `detailsUse` | перечисление | Использование расшифровки: `Cell`, `Row`, `WithoutProcessing` |
|
||||||
|
| `autoMarkIncomplete` | да/нет | Автоотметка незаполненного |
|
||||||
|
| `bySelectedColumns` | да/нет | По выделенным колонкам |
|
||||||
|
| `columnSizeChange` | перечисление | `Normal`, `QuickChange` |
|
||||||
|
| `autoWidthCalculation` | да/нет | Автоматический расчёт ширины |
|
||||||
|
| `widthWeightFactor` | число | Весовой коэффициент ширины |
|
||||||
|
|
||||||
|
## Картинка в ячейке
|
||||||
|
|
||||||
|
| Ключ | Тип | Значение |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `picIndex` | число | Номер картинки |
|
||||||
|
| `pictureSizeMode` | перечисление | `AutoSize`, `Proportionally`, `RealSize` |
|
||||||
|
| `picHorizontalAlignment` | перечисление | `Auto`, `Center`, `Left`, `Right` |
|
||||||
|
| `picVerticalAlignment` | перечисление | `Top`, `Center`, `Bottom` |
|
||||||
|
| `textPosition` | перечисление | Положение текста относительно картинки: `Auto`, `Top`, `Right`, `Bottom` |
|
||||||
|
| `drawingBorder` | число | Рамка рисунка |
|
||||||
|
| `drawingHaveLeftBorder`, `drawingHaveTopBorder`, `drawingHaveRightBorder`, `drawingHaveBottomBorder` | да/нет | Наличие стороны рамки рисунка |
|
||||||
|
|
||||||
|
## Чего в стиле нет
|
||||||
|
|
||||||
|
- `width` — свойство колонки, задаётся через `columnWidths`;
|
||||||
|
- `height` — свойство строки, задаётся ключом `height` у строки;
|
||||||
|
- `fillType` — выводится из того, каким ключом задано содержимое ячейки
|
||||||
|
(`text` / `param` / `template`);
|
||||||
|
- `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего
|
||||||
|
оформления; сейчас не поддерживаются.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Оформление: шрифты, стили, колонки
|
||||||
|
|
||||||
|
Оформление в табличном документе — одна сущность на всех: ячейка, строка и колонка ссылаются
|
||||||
|
на один и тот же именованный стиль. Ячейка — ключом `style`, строка — `rowStyle`, колонка —
|
||||||
|
через `columnStyles`.
|
||||||
|
|
||||||
|
## Шрифты (`fonts.<name>`)
|
||||||
|
|
||||||
|
| Поле | По умолч. | Описание |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `face` | `"Arial"` | Имя шрифта |
|
||||||
|
| `size` | `10` | Размер (бывает дробным: `8.3`) |
|
||||||
|
| `bold` | `false` | Жирный |
|
||||||
|
| `italic` | `false` | Курсив |
|
||||||
|
| `underline` | `false` | Подчёркнутый |
|
||||||
|
| `strikeout` | `false` | Зачёркнутый |
|
||||||
|
|
||||||
|
Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён,
|
||||||
|
создаётся автоматически (Arial 10).
|
||||||
|
|
||||||
|
Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации
|
||||||
|
или на системный шрифт. Тогда у него единственное поле:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"fonts": {
|
||||||
|
"основной": { "ref": "style:TextFont" },
|
||||||
|
"системный": { "ref": "sys:DefaultGUIFont" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Это та же запись, что у шрифта в описании формы.
|
||||||
|
|
||||||
|
## Стили (`styles.<name>`)
|
||||||
|
|
||||||
|
Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список
|
||||||
|
из 44 свойств — в `format-properties.md`.
|
||||||
|
|
||||||
|
| Поле | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `font` | Ссылка на имя из `fonts` |
|
||||||
|
| `horizontalAlignment` | `Left`, `Center`, `Right`, `Justify`, `Auto` |
|
||||||
|
| `verticalAlignment` | `Top`, `Center`, `Bottom` |
|
||||||
|
| `textPlacement` | Что делать с длинным текстом: `Wrap` (перенос), `Cut` (обрезать), `Block`, `Auto` |
|
||||||
|
| `backColor` | Цвет фона (см. «Цвет») |
|
||||||
|
| `textColor` | Цвет текста |
|
||||||
|
| `border`, `leftBorder`, `topBorder`, `rightBorder`, `bottomBorder` | Рамка (см. «Рамка») |
|
||||||
|
| `borderColor` | Цвет рамки |
|
||||||
|
| `format` | Формат данных 1С: `"ЧЦ=15; ЧДЦ=2"`, `"ДФ=dd.MM.yyyy"` |
|
||||||
|
| `hidden` | Скрыть |
|
||||||
|
| `protection` | Защита от редактирования |
|
||||||
|
| `indent` | Отступ |
|
||||||
|
| `textOrientation` | Поворот текста, в десятых долях градуса (`900` = 90°) |
|
||||||
|
|
||||||
|
Значения перечислений регистр не различают: `"center"` и `"Center"` равнозначны.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"styles": {
|
||||||
|
"шапка": {
|
||||||
|
"font": "жирный",
|
||||||
|
"horizontalAlignment": "Center",
|
||||||
|
"verticalAlignment": "Center",
|
||||||
|
"textPlacement": "Wrap",
|
||||||
|
"backColor": "#EBEBEB"
|
||||||
|
},
|
||||||
|
"итог": { "font": "жирный", "topBorder": "Solid", "horizontalAlignment": "Right" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Цвет
|
||||||
|
|
||||||
|
Строка в одной из четырёх форм — это нотация самой платформы:
|
||||||
|
|
||||||
|
| Форма | Значение |
|
||||||
|
|-------|----------|
|
||||||
|
| `#RRGGBB` | RGB-hex, напр. `#FFFFC0` |
|
||||||
|
| `style:ИмяСтиля` | Элемент стиля конфигурации или платформы, напр. `style:FormBackColor` |
|
||||||
|
| `web:Имя` | Цвет из web-палитры, напр. `web:Gainsboro`, `web:FireBrick` |
|
||||||
|
| `win:Имя` | Системный цвет Windows, напр. `win:ButtonText` |
|
||||||
|
|
||||||
|
Имя должно существовать в своей палитре — несуществующее платформа отвергнет при загрузке.
|
||||||
|
|
||||||
|
## Рамка
|
||||||
|
|
||||||
|
Пять ключей: `border` — все четыре стороны сразу, `leftBorder` / `topBorder` / `rightBorder` /
|
||||||
|
`bottomBorder` — по отдельности. Значение одинаковое у всех:
|
||||||
|
|
||||||
|
| Запись | Значение |
|
||||||
|
|--------|----------|
|
||||||
|
| `"Solid"` | Стиль линии, ширина 1 |
|
||||||
|
| `{ "style": "Solid", "width": 2 }` | Стиль и ширина |
|
||||||
|
|
||||||
|
Стили линии: `Solid`, `None`, `Dotted`, `ThinDashed`, `LargeDashed`, `ThickDashed`, `Double`.
|
||||||
|
|
||||||
|
Задавать стороны по отдельности можно всегда: если все четыре совпали, компилятор сам свернёт
|
||||||
|
их в один `border` — так это хранит платформа.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"рамка-снизу": { "bottomBorder": "Dotted" },
|
||||||
|
"рамка-вокруг": { "border": { "style": "Solid", "width": 2 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Колоночные раскладки (`columnSets`)
|
||||||
|
|
||||||
|
Группа строк может иметь собственные ширины колонок — в 1С это «индивидуальная ширина колонок».
|
||||||
|
Документные `columns` и `columnWidths` описывают раскладку по умолчанию; дополнительные
|
||||||
|
объявляются в `columnSets`, а область ссылается на нужную ключом `columnSet` — так же, как
|
||||||
|
ячейка ссылается на `styles` через `style`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"columns": 52,
|
||||||
|
"columnWidths": { "1": 8 },
|
||||||
|
"columnSets": {
|
||||||
|
"таблица": { "columns": 52, "columnWidths": { "1": 7, "2-52": 24 } }
|
||||||
|
},
|
||||||
|
"areas": [
|
||||||
|
{ "name": "Шапка", "rows": [ ] },
|
||||||
|
{ "name": "ТабличнаяЧасть", "columnSet": "таблица", "rows": [ ] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Раскладка описывается той же парой полей, что и документная: `columns` — количество колонок
|
||||||
|
(у раскладок оно обычно разное), `columnWidths` — ширины. Ключ словаря — имя раскладки;
|
||||||
|
в макетах, полученных декомпиляцией, это идентификатор из исходного файла, при описании
|
||||||
|
с нуля — любая строка.
|
||||||
|
|
||||||
|
Все строки области получают раскладку области, поэтому одна область не может смешивать
|
||||||
|
раскладки. Позиции колонок (`col`, `span`) проверяются по ширине раскладки СВОЕЙ области,
|
||||||
|
а не документной.
|
||||||
|
|
||||||
|
Ссылка на необъявленную раскладку → ненулевой код выхода и сообщение в stderr.
|
||||||
|
|
||||||
|
## Стиль колонки (`columnStyles`)
|
||||||
|
|
||||||
|
Колонка несёт то же оформление, что ячейка и строка. Ключи — та же грамматика диапазонов,
|
||||||
|
что у `columnWidths`; значение — имя стиля.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"columnWidths": { "1": 30, "2-3": 15 },
|
||||||
|
"columnStyles": { "1": "по-центру", "4": "скрытая" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Внутри `columnSets` работает тот же ключ. Ширина и стиль независимы: колонка может иметь
|
||||||
|
только ширину, только стиль или и то, и другое.
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user