mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 01:14:56 +03:00
2846 lines
118 KiB
Python
2846 lines
118 KiB
Python
# skd-edit v1.18 — Atomic 1C DCS editor (Python port)
|
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
import uuid
|
|
|
|
from lxml import etree
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
# ── arg parsing ──────────────────────────────────────────────
|
|
|
|
VALID_OPS = [
|
|
"add-field", "add-total", "add-calculated-field", "add-parameter", "add-filter",
|
|
"add-dataParameter", "add-order", "add-selection", "add-dataSetLink",
|
|
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
|
|
"set-query", "patch-query", "set-outputParameter", "set-structure",
|
|
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter", "modify-structure", "set-field-role",
|
|
"rename-parameter", "reorder-parameters",
|
|
"clear-selection", "clear-order", "clear-filter", "clear-conditionalAppearance",
|
|
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
|
|
]
|
|
|
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
parser.add_argument("-TemplatePath", "-Path", required=True)
|
|
parser.add_argument("-Operation", required=True, choices=VALID_OPS)
|
|
parser.add_argument("-Value", required=True)
|
|
parser.add_argument("-DataSet", default="")
|
|
parser.add_argument("-Variant", default="")
|
|
parser.add_argument("-NoSelection", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
template_path = args.TemplatePath
|
|
operation = args.Operation
|
|
value_arg = args.Value
|
|
data_set_arg = args.DataSet
|
|
variant_arg = args.Variant
|
|
no_selection = args.NoSelection
|
|
|
|
# ── namespaces ───────────────────────────────────────────────
|
|
|
|
SCH_NS = "http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
SET_NS = "http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
COR_NS = "http://v8.1c.ru/8.1/data-composition-system/core"
|
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|
|
|
NS_MAP = {
|
|
"sch": SCH_NS,
|
|
"dcsset": SET_NS,
|
|
"dcscor": COR_NS,
|
|
"xsi": XSI_NS,
|
|
"v8": V8_NS,
|
|
}
|
|
|
|
WRAPPER_NS = (
|
|
f'xmlns="{SCH_NS}"'
|
|
f' xmlns:xsi="{XSI_NS}"'
|
|
f' xmlns:v8="{V8_NS}"'
|
|
' xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"'
|
|
f' xmlns:dcscor="{COR_NS}"'
|
|
f' xmlns:dcsset="{SET_NS}"'
|
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
)
|
|
|
|
XSI_TYPE = f"{{{XSI_NS}}}type"
|
|
|
|
|
|
def local_name(node):
|
|
return etree.QName(node.tag).localname
|
|
|
|
|
|
# ── helpers ──────────────────────────────────────────────────
|
|
|
|
def esc_xml(s):
|
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
|
|
|
|
def resolve_query_value(val, base_dir):
|
|
if not val.startswith("@"):
|
|
return val
|
|
file_path = val[1:]
|
|
if os.path.isabs(file_path):
|
|
candidates = [file_path]
|
|
else:
|
|
candidates = [
|
|
os.path.join(base_dir, file_path),
|
|
os.path.join(os.getcwd(), file_path),
|
|
]
|
|
for c in candidates:
|
|
if os.path.exists(c):
|
|
with open(c, 'r', encoding='utf-8-sig') as f:
|
|
return f.read().rstrip()
|
|
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def new_uuid():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
# ── 1. Resolve path ─────────────────────────────────────────
|
|
|
|
if not template_path.endswith(".xml"):
|
|
candidate = os.path.join(template_path, "Ext", "Template.xml")
|
|
if os.path.exists(candidate):
|
|
template_path = candidate
|
|
|
|
if not os.path.exists(template_path):
|
|
print(f"File not found: {template_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
resolved_path = os.path.abspath(template_path)
|
|
query_base_dir = os.path.dirname(resolved_path)
|
|
|
|
# ── 2. Type system ──────────────────────────────────────────
|
|
|
|
type_synonyms = {
|
|
"\u0447\u0438\u0441\u043b\u043e": "decimal",
|
|
"\u0441\u0442\u0440\u043e\u043a\u0430": "string",
|
|
"\u0431\u0443\u043b\u0435\u0432\u043e": "boolean",
|
|
"\u0434\u0430\u0442\u0430": "date",
|
|
"\u0434\u0430\u0442\u0430\u0432\u0440\u0435\u043c\u044f": "dateTime",
|
|
"\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439\u043f\u0435\u0440\u0438\u043e\u0434": "StandardPeriod",
|
|
"bool": "boolean",
|
|
"str": "string",
|
|
"int": "decimal",
|
|
"integer": "decimal",
|
|
"number": "decimal",
|
|
"num": "decimal",
|
|
"\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "CatalogRef",
|
|
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0441\u0441\u044b\u043b\u043a\u0430": "DocumentRef",
|
|
"\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435\u0441\u0441\u044b\u043b\u043a\u0430": "EnumRef",
|
|
"\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfAccountsRef",
|
|
"\u043f\u043b\u0430\u043d\u0432\u0438\u0434\u043e\u0432\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfCharacteristicTypesRef",
|
|
}
|
|
|
|
output_param_types = {
|
|
"\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "mltext",
|
|
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "dcsset:DataCompositionTextOutputType",
|
|
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0414\u0430\u043d\u043d\u044b\u0445": "dcsset:DataCompositionTextOutputType",
|
|
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041e\u0442\u0431\u043e\u0440": "dcsset:DataCompositionTextOutputType",
|
|
"\u041c\u0430\u043a\u0435\u0442\u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f": "xs:string",
|
|
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041f\u043e\u043b\u0435\u0439\u0413\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438": "dcsset:DataCompositionGroupFieldsPlacement",
|
|
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432": "dcsset:DataCompositionAttributesPlacement",
|
|
"\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
|
|
"\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
|
|
}
|
|
|
|
|
|
def resolve_type_str(type_str):
|
|
if not type_str:
|
|
return type_str
|
|
|
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
|
if m:
|
|
base_name = m.group(1).strip()
|
|
params = m.group(2)
|
|
resolved = type_synonyms.get(base_name.lower())
|
|
if resolved:
|
|
return f"{resolved}({params})"
|
|
return type_str
|
|
|
|
if "." in type_str:
|
|
dot_idx = type_str.index(".")
|
|
prefix = type_str[:dot_idx]
|
|
suffix = type_str[dot_idx:]
|
|
resolved = type_synonyms.get(prefix.lower())
|
|
if resolved:
|
|
return f"{resolved}{suffix}"
|
|
return type_str
|
|
|
|
resolved = type_synonyms.get(type_str.lower())
|
|
if resolved:
|
|
return resolved
|
|
return type_str
|
|
|
|
|
|
# ── 3. Parsers ──────────────────────────────────────────────
|
|
|
|
def parse_field_shorthand(s):
|
|
result = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": []}
|
|
|
|
m = re.search(r'\[([^\]]+)\]', s)
|
|
if m:
|
|
result["title"] = m.group(1)
|
|
s = re.sub(r'\s*\[[^\]]+\]', '', s)
|
|
|
|
role_matches = re.findall(r'@(\w+)', s)
|
|
result["roles"] = role_matches
|
|
s = re.sub(r'\s*@\w+', '', s)
|
|
|
|
restrict_matches = re.findall(r'#(\w+)', s)
|
|
result["restrict"] = restrict_matches
|
|
s = re.sub(r'\s*#\w+', '', s)
|
|
|
|
s = s.strip()
|
|
if ":" in s:
|
|
parts = s.split(":", 1)
|
|
result["dataPath"] = parts[0].strip()
|
|
result["type"] = resolve_type_str(parts[1].strip())
|
|
else:
|
|
result["dataPath"] = s
|
|
|
|
result["field"] = result["dataPath"]
|
|
return result
|
|
|
|
|
|
def read_field_properties(field_el):
|
|
props = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": [], "_rawTypeText": ""}
|
|
|
|
for ch in field_el:
|
|
if not isinstance(ch.tag, str):
|
|
continue
|
|
ln = local_name(ch)
|
|
if ln == "dataPath":
|
|
props["dataPath"] = (ch.text or "").strip()
|
|
elif ln == "field":
|
|
props["field"] = (ch.text or "").strip()
|
|
elif ln == "title":
|
|
for item in ch:
|
|
if isinstance(item.tag, str) and local_name(item) == "item":
|
|
for gc in item:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "content":
|
|
props["title"] = (gc.text or "").strip()
|
|
elif ln == "valueType":
|
|
for gc in ch:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "Type":
|
|
props["_rawTypeText"] = (gc.text or "").strip()
|
|
break
|
|
elif ln == "role":
|
|
for gc in ch:
|
|
if isinstance(gc.tag, str):
|
|
gcn = local_name(gc)
|
|
if gcn == "periodNumber":
|
|
props["roles"].append("period")
|
|
elif (gc.text or "").strip() == "true":
|
|
props["roles"].append(gcn)
|
|
elif ln == "useRestriction":
|
|
rev_map = {"field": "noField", "condition": "noFilter", "group": "noGroup", "order": "noOrder"}
|
|
for gc in ch:
|
|
if isinstance(gc.tag, str) and (gc.text or "").strip() == "true":
|
|
mapped = rev_map.get(local_name(gc))
|
|
if mapped:
|
|
props["restrict"].append(mapped)
|
|
return props
|
|
|
|
|
|
def parse_total_shorthand(s):
|
|
# "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity)
|
|
parts = s.split(":", 1)
|
|
data_path = parts[0].strip()
|
|
func_part = parts[1].strip()
|
|
|
|
agg_funcs = {'Сумма', 'Количество', 'Минимум', 'Максимум', 'Среднее',
|
|
'Sum', 'Count', 'Min', 'Max', 'Avg',
|
|
'Minimum', 'Maximum', 'Average'}
|
|
|
|
if re.match(r'^\w+\(', func_part):
|
|
return {"dataPath": data_path, "expression": func_part}
|
|
elif func_part in agg_funcs:
|
|
return {"dataPath": data_path, "expression": f"{func_part}({data_path})"}
|
|
else:
|
|
return {"dataPath": data_path, "expression": func_part}
|
|
|
|
|
|
def parse_calc_shorthand(s):
|
|
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
|
|
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
|
|
# an expression (e.g. index access) isn't interpreted as a title.
|
|
# - `#restrict` flags use a known-names pattern and are extracted globally —
|
|
# the docs put them after `=`, and the closed flag set avoids matching
|
|
# `#word` that happens to appear inside a string literal.
|
|
restrict_pattern = r'#(noField|noFilter|noCondition|noGroup|noOrder)\b'
|
|
|
|
restrict_matches = re.findall(restrict_pattern, s)
|
|
s = re.sub(r'\s*' + restrict_pattern, '', s)
|
|
|
|
eq_idx = s.find("=")
|
|
if eq_idx > 0:
|
|
lhs = s[:eq_idx]
|
|
rhs = s[eq_idx + 1:].strip()
|
|
has_rhs = True
|
|
else:
|
|
lhs = s
|
|
rhs = ""
|
|
has_rhs = False
|
|
|
|
title = ""
|
|
m = re.search(r'\[([^\]]+)\]', lhs)
|
|
if m:
|
|
title = m.group(1)
|
|
lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
|
|
lhs = lhs.strip()
|
|
|
|
if has_rhs:
|
|
if ":" in lhs:
|
|
colon_idx = lhs.index(":")
|
|
data_path = lhs[:colon_idx].strip()
|
|
type_str = resolve_type_str(lhs[colon_idx + 1:].strip())
|
|
return {"dataPath": data_path, "expression": rhs, "type": type_str, "title": title, "restrict": restrict_matches}
|
|
return {"dataPath": lhs, "expression": rhs, "type": "", "title": title, "restrict": restrict_matches}
|
|
return {"dataPath": lhs, "expression": "", "type": "", "title": title, "restrict": restrict_matches}
|
|
|
|
|
|
def parse_param_shorthand(s):
|
|
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None, "hidden": False, "always": False, "availableValues": []}
|
|
|
|
# Extract availableValue=... (must be before main parse — captures to end of string)
|
|
m_av = re.search(r'\s*availableValue=(.+)$', s)
|
|
if m_av:
|
|
result["availableValues"] = parse_available_value_list(m_av.group(1).strip())
|
|
s = re.sub(r'\s*availableValue=.+$', '', s).strip()
|
|
|
|
if re.search(r'@autoDates', s):
|
|
result["autoDates"] = True
|
|
s = re.sub(r'\s*@autoDates', '', s)
|
|
|
|
if re.search(r'@hidden\b', s):
|
|
result["hidden"] = True
|
|
s = re.sub(r'\s*@hidden\b', '', s)
|
|
|
|
if re.search(r'@always\b', s):
|
|
result["always"] = True
|
|
s = re.sub(r'\s*@always\b', '', s)
|
|
|
|
# Extract optional [Title] (mirrors parse_field_shorthand)
|
|
m = re.search(r'\[([^\]]*)\]', s)
|
|
if m:
|
|
result["title"] = m.group(1).strip()
|
|
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
|
|
|
|
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
|
|
if m:
|
|
result["name"] = m.group(1).strip()
|
|
result["type"] = resolve_type_str(m.group(2).strip())
|
|
if m.group(4):
|
|
result["value"] = m.group(4).strip()
|
|
else:
|
|
result["name"] = s.strip()
|
|
|
|
return result
|
|
|
|
|
|
def parse_filter_shorthand(s):
|
|
# use is tristate: None = not specified (modify-* won't touch),
|
|
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
|
|
result = {"field": "", "op": "Equal", "value": None, "use": None, "userSettingID": None, "viewMode": None}
|
|
|
|
if re.search(r'@user', s):
|
|
result["userSettingID"] = "auto"
|
|
s = re.sub(r'\s*@user', '', s)
|
|
if re.search(r'@off', s):
|
|
result["use"] = False
|
|
s = re.sub(r'\s*@off', '', s)
|
|
if re.search(r'@on\b', s):
|
|
result["use"] = True
|
|
s = re.sub(r'\s*@on\b', '', s)
|
|
if re.search(r'@quickAccess', s):
|
|
result["viewMode"] = "QuickAccess"
|
|
s = re.sub(r'\s*@quickAccess', '', s)
|
|
if re.search(r'@normal', s):
|
|
result["viewMode"] = "Normal"
|
|
s = re.sub(r'\s*@normal', '', s)
|
|
if re.search(r'@inaccessible', s):
|
|
result["viewMode"] = "Inaccessible"
|
|
s = re.sub(r'\s*@inaccessible', '', s)
|
|
|
|
s = s.strip()
|
|
|
|
op_patterns = [r'<>', r'>=', r'<=', r'=', r'>', r'<',
|
|
r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b',
|
|
r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b',
|
|
r'notFilled\b', r'filled\b']
|
|
op_joined = "|".join(op_patterns)
|
|
|
|
m = re.match(rf'^(.+?)\s+({op_joined})\s*(.*)?$', s)
|
|
if m:
|
|
result["field"] = m.group(1).strip()
|
|
op_raw = m.group(2).strip()
|
|
val_part = (m.group(3) or "").strip()
|
|
|
|
op_map = {
|
|
"=": "Equal", "<>": "NotEqual", ">": "Greater", ">=": "GreaterOrEqual",
|
|
"<": "Less", "<=": "LessOrEqual", "in": "InList", "notIn": "NotInList",
|
|
"inHierarchy": "InHierarchy", "inListByHierarchy": "InListByHierarchy",
|
|
"contains": "Contains", "notContains": "NotContains",
|
|
"beginsWith": "BeginsWith", "notBeginsWith": "NotBeginsWith",
|
|
"filled": "Filled", "notFilled": "NotFilled",
|
|
}
|
|
result["op"] = op_map.get(op_raw, op_raw)
|
|
|
|
if val_part and val_part != "_":
|
|
if val_part in ("true", "false"):
|
|
result["value"] = val_part
|
|
result["valueType"] = "xs:boolean"
|
|
elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part):
|
|
result["value"] = val_part
|
|
result["valueType"] = "xs:dateTime"
|
|
elif re.match(r'^\d+(\.\d+)?$', val_part):
|
|
result["value"] = val_part
|
|
result["valueType"] = "xs:decimal"
|
|
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
|
|
result["value"] = val_part
|
|
result["valueType"] = "dcscor:DesignTimeValue"
|
|
else:
|
|
result["value"] = val_part
|
|
result["valueType"] = "xs:string"
|
|
else:
|
|
result["field"] = s
|
|
|
|
return result
|
|
|
|
|
|
def parse_data_param_shorthand(s):
|
|
# use is tristate: None = not specified (modify-* won't touch),
|
|
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
|
|
result = {"parameter": "", "value": None, "use": None, "userSettingID": None, "viewMode": None}
|
|
|
|
if re.search(r'@user', s):
|
|
result["userSettingID"] = "auto"
|
|
s = re.sub(r'\s*@user', '', s)
|
|
if re.search(r'@off', s):
|
|
result["use"] = False
|
|
s = re.sub(r'\s*@off', '', s)
|
|
if re.search(r'@on\b', s):
|
|
result["use"] = True
|
|
s = re.sub(r'\s*@on\b', '', s)
|
|
if re.search(r'@quickAccess', s):
|
|
result["viewMode"] = "QuickAccess"
|
|
s = re.sub(r'\s*@quickAccess', '', s)
|
|
if re.search(r'@normal', s):
|
|
result["viewMode"] = "Normal"
|
|
s = re.sub(r'\s*@normal', '', s)
|
|
|
|
s = s.strip()
|
|
|
|
m = re.match(r'^([^=]+)=\s*(.+)$', s)
|
|
if m:
|
|
result["parameter"] = m.group(1).strip()
|
|
val_str = m.group(2).strip()
|
|
|
|
period_variants = [
|
|
"Custom", "Today", "ThisWeek", "ThisTenDays", "ThisMonth", "ThisQuarter", "ThisHalfYear", "ThisYear",
|
|
"FromBeginningOfThisWeek", "FromBeginningOfThisTenDays", "FromBeginningOfThisMonth",
|
|
"FromBeginningOfThisQuarter", "FromBeginningOfThisHalfYear", "FromBeginningOfThisYear",
|
|
"LastWeek", "LastTenDays", "LastMonth", "LastQuarter", "LastHalfYear", "LastYear",
|
|
"NextDay", "NextWeek", "NextTenDays", "NextMonth", "NextQuarter", "NextHalfYear", "NextYear",
|
|
"TillEndOfThisWeek", "TillEndOfThisTenDays", "TillEndOfThisMonth",
|
|
"TillEndOfThisQuarter", "TillEndOfThisHalfYear", "TillEndOfThisYear",
|
|
]
|
|
if val_str in period_variants:
|
|
result["value"] = {"variant": val_str}
|
|
else:
|
|
result["value"] = val_str
|
|
else:
|
|
result["parameter"] = s
|
|
|
|
return result
|
|
|
|
|
|
def parse_order_shorthand(s):
|
|
s = s.strip()
|
|
if s == "Auto":
|
|
return {"field": "Auto", "direction": ""}
|
|
parts = s.split(None, 1)
|
|
field = parts[0]
|
|
direction = "Asc"
|
|
if len(parts) > 1 and re.match(r'^desc$', parts[1], re.IGNORECASE):
|
|
direction = "Desc"
|
|
return {"field": field, "direction": direction}
|
|
|
|
|
|
def parse_data_set_link_shorthand(s):
|
|
result = {"source": "", "dest": "", "sourceExpr": "", "destExpr": "", "parameter": ""}
|
|
|
|
m = re.search(r'\[param\s+([^\]]+)\]', s)
|
|
if m:
|
|
result["parameter"] = m.group(1).strip()
|
|
s = re.sub(r'\s*\[param\s+[^\]]+\]', '', s)
|
|
|
|
m = re.match(r'^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$', s)
|
|
if m:
|
|
result["source"] = m.group(1).strip()
|
|
result["dest"] = m.group(2).strip()
|
|
result["sourceExpr"] = m.group(3).strip()
|
|
result["destExpr"] = m.group(4).strip()
|
|
else:
|
|
print(f"Invalid dataSetLink shorthand: {s}. Expected: 'Source > Dest on FieldA = FieldB [param Name]'", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
return result
|
|
|
|
|
|
def parse_data_set_shorthand(s):
|
|
s = s.strip()
|
|
m = re.match(r'^(\S+):\s(.+)$', s)
|
|
if m:
|
|
return {"name": m.group(1), "query": m.group(2)}
|
|
return {"name": "", "query": s}
|
|
|
|
|
|
def parse_variant_shorthand(s):
|
|
presentation = ""
|
|
m = re.search(r'\[([^\]]+)\]', s)
|
|
if m:
|
|
presentation = m.group(1)
|
|
s = re.sub(r'\s*\[[^\]]+\]', '', s)
|
|
name = s.strip()
|
|
if not presentation:
|
|
presentation = name
|
|
return {"name": name, "presentation": presentation}
|
|
|
|
|
|
def parse_conditional_appearance_shorthand(s):
|
|
result = {"param": "", "value": "", "filter": None, "fields": []}
|
|
|
|
when_idx = s.find(" when ")
|
|
for_idx = s.find(" for ")
|
|
|
|
main_end = len(s)
|
|
if when_idx >= 0 and for_idx >= 0:
|
|
main_end = min(when_idx, for_idx)
|
|
elif when_idx >= 0:
|
|
main_end = when_idx
|
|
elif for_idx >= 0:
|
|
main_end = for_idx
|
|
|
|
if for_idx >= 0:
|
|
for_end = len(s)
|
|
if when_idx > for_idx:
|
|
for_end = when_idx
|
|
for_part = s[for_idx + 5:for_end].strip()
|
|
result["fields"] = [f.strip() for f in for_part.split(",") if f.strip()]
|
|
|
|
if when_idx >= 0:
|
|
when_end = len(s)
|
|
if for_idx > when_idx:
|
|
when_end = for_idx
|
|
when_part = s[when_idx + 6:when_end].strip()
|
|
or_parts = re.split(r'\s+or\s+', when_part)
|
|
if len(or_parts) > 1:
|
|
result["filter"] = [parse_filter_shorthand(p.strip()) for p in or_parts]
|
|
else:
|
|
result["filter"] = parse_filter_shorthand(when_part)
|
|
|
|
main_part = s[:main_end].strip()
|
|
eq_idx = main_part.find("=")
|
|
if eq_idx > 0:
|
|
result["param"] = main_part[:eq_idx].strip()
|
|
result["value"] = main_part[eq_idx + 1:].strip()
|
|
else:
|
|
result["param"] = main_part
|
|
|
|
return result
|
|
|
|
|
|
def parse_structure_shorthand(s):
|
|
segments = [seg.strip() for seg in s.split(">")]
|
|
result = []
|
|
innermost = None
|
|
|
|
for i in range(len(segments) - 1, -1, -1):
|
|
seg = segments[i].strip()
|
|
group = {"type": "group"}
|
|
|
|
name_m = re.search(r'@name=(?:"([^"]+)"|\'([^\']+)\'|(\S+))', seg)
|
|
if name_m:
|
|
raw_name = name_m.group(1) or name_m.group(2) or name_m.group(3)
|
|
group["name"] = raw_name.strip()
|
|
seg = re.sub(r'\s*@name=(?:"[^"]+"|\'[^\']+\'|\S+)', '', seg).strip()
|
|
|
|
if re.match(r'^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg, re.IGNORECASE):
|
|
group["groupBy"] = []
|
|
else:
|
|
fields = [f.strip() for f in re.split(r'\s*,\s*', seg) if f.strip()]
|
|
group["groupBy"] = fields
|
|
|
|
if innermost is not None:
|
|
group["children"] = [innermost]
|
|
innermost = group
|
|
|
|
if innermost:
|
|
result.append(innermost)
|
|
return result
|
|
|
|
|
|
def parse_output_param_shorthand(s):
|
|
idx = s.find("=")
|
|
if idx > 0:
|
|
return {"key": s[:idx].strip(), "value": s[idx + 1:].strip()}
|
|
return {"key": s.strip(), "value": ""}
|
|
|
|
|
|
def parse_available_value_list(s):
|
|
"""Returns list of {value, presentation} from comma-separated list.
|
|
Items can use single/double quotes (stripped). Quoted spans preserve commas/colons."""
|
|
if not s:
|
|
return []
|
|
|
|
# Tokenize by ',' respecting quoted spans
|
|
items = []
|
|
buf = []
|
|
in_quote = None
|
|
for ch in s:
|
|
if in_quote:
|
|
buf.append(ch)
|
|
if ch == in_quote:
|
|
in_quote = None
|
|
elif ch in ("'", '"'):
|
|
in_quote = ch
|
|
buf.append(ch)
|
|
elif ch == ',':
|
|
items.append("".join(buf))
|
|
buf = []
|
|
else:
|
|
buf.append(ch)
|
|
if buf:
|
|
items.append("".join(buf))
|
|
|
|
def strip_quotes(t):
|
|
t = t.strip()
|
|
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
|
|
return t[1:-1]
|
|
return t
|
|
|
|
result = []
|
|
for raw in items:
|
|
item = raw.strip()
|
|
if not item:
|
|
continue
|
|
# Find first ':' outside quotes
|
|
colon_idx = -1
|
|
q = None
|
|
for j, c in enumerate(item):
|
|
if q:
|
|
if c == q:
|
|
q = None
|
|
elif c in ("'", '"'):
|
|
q = c
|
|
elif c == ':':
|
|
colon_idx = j
|
|
break
|
|
if colon_idx >= 0:
|
|
val_part = item[:colon_idx]
|
|
pres_part = item[colon_idx + 1:]
|
|
result.append({"value": strip_quotes(val_part), "presentation": strip_quotes(pres_part)})
|
|
else:
|
|
result.append({"value": strip_quotes(item), "presentation": ""})
|
|
return result
|
|
|
|
|
|
def build_available_value_fragment(item, declared_type, indent):
|
|
"""Return XML lines for a single <availableValue> block."""
|
|
lines = [f"{indent}<availableValue>"]
|
|
for vl in build_param_value_xml(declared_type, item["value"], f"{indent}\t"):
|
|
lines.append(vl)
|
|
if item.get("presentation"):
|
|
lines.append(f'{indent}\t<presentation xsi:type="v8:LocalStringType">')
|
|
lines.append(f"{indent}\t\t<v8:item>")
|
|
lines.append(f"{indent}\t\t\t<v8:lang>ru</v8:lang>")
|
|
lines.append(f"{indent}\t\t\t<v8:content>{esc_xml(item['presentation'])}</v8:content>")
|
|
lines.append(f"{indent}\t\t</v8:item>")
|
|
lines.append(f"{indent}\t</presentation>")
|
|
lines.append(f"{indent}</availableValue>")
|
|
return lines
|
|
|
|
|
|
# ── 4. Build-* functions (XML fragment generators) ──────────
|
|
|
|
def build_value_type_xml(type_str, indent):
|
|
if not type_str:
|
|
return ""
|
|
type_str = resolve_type_str(type_str)
|
|
lines = []
|
|
|
|
if type_str == "boolean":
|
|
lines.append(f"{indent}<v8:Type>xs:boolean</v8:Type>")
|
|
return "\r\n".join(lines)
|
|
|
|
m = re.match(r'^string(\((\d+)\))?$', type_str)
|
|
if m:
|
|
length = m.group(2) if m.group(2) else "0"
|
|
lines.append(f"{indent}<v8:Type>xs:string</v8:Type>")
|
|
lines.append(f"{indent}<v8:StringQualifiers>")
|
|
lines.append(f"{indent}\t<v8:Length>{length}</v8:Length>")
|
|
lines.append(f"{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>")
|
|
lines.append(f"{indent}</v8:StringQualifiers>")
|
|
return "\r\n".join(lines)
|
|
|
|
m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
|
|
if m:
|
|
digits, fraction = m.group(1), m.group(2)
|
|
sign = "Nonnegative" if m.group(3) else "Any"
|
|
lines.append(f"{indent}<v8:Type>xs:decimal</v8:Type>")
|
|
lines.append(f"{indent}<v8:NumberQualifiers>")
|
|
lines.append(f"{indent}\t<v8:Digits>{digits}</v8:Digits>")
|
|
lines.append(f"{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>")
|
|
lines.append(f"{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>")
|
|
lines.append(f"{indent}</v8:NumberQualifiers>")
|
|
return "\r\n".join(lines)
|
|
|
|
m = re.match(r'^(date|dateTime)$', type_str)
|
|
if m:
|
|
fractions = "Date" if type_str == "date" else "DateTime"
|
|
lines.append(f"{indent}<v8:Type>xs:dateTime</v8:Type>")
|
|
lines.append(f"{indent}<v8:DateQualifiers>")
|
|
lines.append(f"{indent}\t<v8:DateFractions>{fractions}</v8:DateFractions>")
|
|
lines.append(f"{indent}</v8:DateQualifiers>")
|
|
return "\r\n".join(lines)
|
|
|
|
if type_str == "StandardPeriod":
|
|
lines.append(f"{indent}<v8:Type>v8:StandardPeriod</v8:Type>")
|
|
return "\r\n".join(lines)
|
|
|
|
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.', type_str):
|
|
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
|
|
return "\r\n".join(lines)
|
|
|
|
if "." in type_str:
|
|
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
|
|
return "\r\n".join(lines)
|
|
|
|
lines.append(f"{indent}<v8:Type>{esc_xml(type_str)}</v8:Type>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_mltext_xml(tag, text, indent):
|
|
lines = [
|
|
f'{indent}<{tag} xsi:type="v8:LocalStringType">',
|
|
f"{indent}\t<v8:item>",
|
|
f"{indent}\t\t<v8:lang>ru</v8:lang>",
|
|
f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>",
|
|
f"{indent}\t</v8:item>",
|
|
f"{indent}</{tag}>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_role_xml(roles, indent):
|
|
if not roles:
|
|
return ""
|
|
lines = [f"{indent}<role>"]
|
|
for role in roles:
|
|
if role == "period":
|
|
lines.append(f"{indent}\t<dcscom:periodNumber>1</dcscom:periodNumber>")
|
|
lines.append(f"{indent}\t<dcscom:periodType>Main</dcscom:periodType>")
|
|
else:
|
|
lines.append(f"{indent}\t<dcscom:{role}>true</dcscom:{role}>")
|
|
lines.append(f"{indent}</role>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_restriction_xml(restrict, indent):
|
|
if not restrict:
|
|
return ""
|
|
restrict_map = {"noField": "field", "noFilter": "condition", "noCondition": "condition", "noGroup": "group", "noOrder": "order"}
|
|
lines = [f"{indent}<useRestriction>"]
|
|
for r in restrict:
|
|
xml_name = restrict_map.get(r)
|
|
if xml_name:
|
|
lines.append(f"{indent}\t<{xml_name}>true</{xml_name}>")
|
|
lines.append(f"{indent}</useRestriction>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_field_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [f'{i}<field xsi:type="DataSetFieldField">']
|
|
lines.append(f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>")
|
|
lines.append(f"{i}\t<field>{esc_xml(parsed['field'])}</field>")
|
|
|
|
if parsed.get("title"):
|
|
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
|
|
|
|
if parsed.get("restrict"):
|
|
lines.append(build_restriction_xml(parsed["restrict"], f"{i}\t"))
|
|
|
|
role_xml = build_role_xml(parsed.get("roles"), f"{i}\t")
|
|
if role_xml:
|
|
lines.append(role_xml)
|
|
|
|
if parsed.get("type"):
|
|
lines.append(f"{i}\t<valueType>")
|
|
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
|
|
lines.append(f"{i}\t</valueType>")
|
|
|
|
lines.append(f"{i}</field>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_total_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [
|
|
f"{i}<totalField>",
|
|
f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>",
|
|
f"{i}\t<expression>{esc_xml(parsed['expression'])}</expression>",
|
|
f"{i}</totalField>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_calc_field_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [
|
|
f"{i}<calculatedField>",
|
|
f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>",
|
|
f"{i}\t<expression>{esc_xml(parsed['expression'])}</expression>",
|
|
]
|
|
if parsed.get("title"):
|
|
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
|
|
if parsed.get("restrict"):
|
|
lines.append(build_restriction_xml(parsed["restrict"], f"{i}\t"))
|
|
if parsed.get("type"):
|
|
lines.append(f"{i}\t<valueType>")
|
|
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
|
|
lines.append(f"{i}\t</valueType>")
|
|
lines.append(f"{i}</calculatedField>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_param_value_xml(type_str, value, indent, tag_name="value", tag_ns=""):
|
|
"""Return list of XML lines for <value xsi:type=...>...</value>."""
|
|
val_str = "" if value is None else str(value)
|
|
open_tag = f"{tag_ns}:{tag_name}" if tag_ns else tag_name
|
|
lines = []
|
|
|
|
if type_str == "StandardPeriod":
|
|
lines.append(f'{indent}<{open_tag} xsi:type="v8:StandardPeriod">')
|
|
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
|
|
lines.append(f"{indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
|
lines.append(f"{indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
|
lines.append(f"{indent}</{open_tag}>")
|
|
return lines
|
|
|
|
t = type_str or ""
|
|
xsi = None
|
|
if t.startswith("date"):
|
|
xsi = "xs:dateTime"
|
|
elif t == "boolean":
|
|
xsi = "xs:boolean"
|
|
elif t.startswith("decimal"):
|
|
xsi = "xs:decimal"
|
|
elif t.startswith("string"):
|
|
xsi = "xs:string"
|
|
elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t):
|
|
xsi = "dcscor:DesignTimeValue"
|
|
else:
|
|
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
|
|
xsi = "xs:dateTime"
|
|
elif val_str in ("true", "false"):
|
|
xsi = "xs:boolean"
|
|
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or \
|
|
re.match(r'^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
|
|
xsi = "dcscor:DesignTimeValue"
|
|
else:
|
|
xsi = "xs:string"
|
|
|
|
lines.append(f'{indent}<{open_tag} xsi:type="{xsi}">{esc_xml(val_str)}</{open_tag}>')
|
|
return lines
|
|
|
|
|
|
def build_param_fragment(parsed, indent):
|
|
i = indent
|
|
fragments = []
|
|
|
|
lines = [f"{i}<parameter>", f"{i}\t<name>{esc_xml(parsed['name'])}</name>"]
|
|
|
|
if parsed.get("title"):
|
|
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
|
|
|
|
if parsed.get("type"):
|
|
lines.append(f"{i}\t<valueType>")
|
|
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
|
|
lines.append(f"{i}\t</valueType>")
|
|
|
|
if parsed["value"] is not None:
|
|
for vl in build_param_value_xml(parsed.get("type", ""), parsed["value"], f"{i}\t"):
|
|
lines.append(vl)
|
|
|
|
if parsed.get("hidden"):
|
|
lines.append(f"{i}\t<useRestriction>true</useRestriction>")
|
|
lines.append(f"{i}\t<availableAsField>false</availableAsField>")
|
|
|
|
for av in parsed.get("availableValues", []) or []:
|
|
for l in build_available_value_fragment(av, parsed.get("type", ""), f"{i}\t"):
|
|
lines.append(l)
|
|
|
|
if parsed.get("always"):
|
|
lines.append(f"{i}\t<use>Always</use>")
|
|
|
|
lines.append(f"{i}</parameter>")
|
|
fragments.append("\r\n".join(lines))
|
|
|
|
if parsed.get("autoDates"):
|
|
param_name = parsed["name"]
|
|
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
|
|
b_lines = [
|
|
f"{i}<parameter>",
|
|
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430</name>",
|
|
build_mltext_xml("title", "\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
|
|
f"{i}\t<valueType>",
|
|
build_value_type_xml("date", f"{i}\t\t"),
|
|
f"{i}\t</valueType>",
|
|
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
|
|
f"{i}\t<useRestriction>true</useRestriction>",
|
|
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}</expression>",
|
|
f"{i}</parameter>",
|
|
]
|
|
fragments.append("\r\n".join(b_lines))
|
|
|
|
e_lines = [
|
|
f"{i}<parameter>",
|
|
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f</name>",
|
|
build_mltext_xml("title", "\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
|
|
f"{i}\t<valueType>",
|
|
build_value_type_xml("date", f"{i}\t\t"),
|
|
f"{i}\t</valueType>",
|
|
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
|
|
f"{i}\t<useRestriction>true</useRestriction>",
|
|
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}</expression>",
|
|
f"{i}</parameter>",
|
|
]
|
|
fragments.append("\r\n".join(e_lines))
|
|
|
|
return fragments
|
|
|
|
|
|
def build_filter_item_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [f'{i}<dcsset:item xsi:type="dcsset:FilterItemComparison">']
|
|
|
|
if parsed.get("use") is False:
|
|
lines.append(f"{i}\t<dcsset:use>false</dcsset:use>")
|
|
|
|
lines.append(f'{i}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(parsed["field"])}</dcsset:left>')
|
|
lines.append(f"{i}\t<dcsset:comparisonType>{esc_xml(parsed['op'])}</dcsset:comparisonType>")
|
|
|
|
if parsed.get("value") is not None:
|
|
vt = parsed.get("valueType", "xs:string")
|
|
lines.append(f'{i}\t<dcsset:right xsi:type="{vt}">{esc_xml(str(parsed["value"]))}</dcsset:right>')
|
|
|
|
if parsed.get("viewMode"):
|
|
lines.append(f"{i}\t<dcsset:viewMode>{esc_xml(parsed['viewMode'])}</dcsset:viewMode>")
|
|
|
|
if parsed.get("userSettingID"):
|
|
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
|
|
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
|
|
|
|
lines.append(f"{i}</dcsset:item>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_selection_item_fragment(field_name, indent):
|
|
i = indent
|
|
if field_name == "Auto":
|
|
return f'{i}<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>'
|
|
m = re.match(r'^Folder\((.+)\)$', field_name)
|
|
if m:
|
|
inner = m.group(1)
|
|
colon_idx = inner.find(':')
|
|
if colon_idx > 0:
|
|
title = inner[:colon_idx].strip()
|
|
items = [x.strip() for x in inner[colon_idx + 1:].split(',') if x.strip()]
|
|
else:
|
|
title = ""
|
|
items = [x.strip() for x in inner.split(',') if x.strip()]
|
|
lines = [f'{i}<dcsset:item xsi:type="dcsset:SelectedItemFolder">']
|
|
if title:
|
|
lines.append(f"{i}\t<dcsset:lwsTitle>")
|
|
lines.append(f"{i}\t\t<v8:item>")
|
|
lines.append(f"{i}\t\t\t<v8:lang>ru</v8:lang>")
|
|
lines.append(f"{i}\t\t\t<v8:content>{esc_xml(title)}</v8:content>")
|
|
lines.append(f"{i}\t\t</v8:item>")
|
|
lines.append(f"{i}\t</dcsset:lwsTitle>")
|
|
for item in items:
|
|
lines.append(f'{i}\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
|
|
lines.append(f"{i}\t\t<dcsset:field>{esc_xml(item)}</dcsset:field>")
|
|
lines.append(f"{i}\t</dcsset:item>")
|
|
lines.append(f"{i}\t<dcsset:placement>Auto</dcsset:placement>")
|
|
lines.append(f"{i}</dcsset:item>")
|
|
return "\r\n".join(lines)
|
|
lines = [
|
|
f'{i}<dcsset:item xsi:type="dcsset:SelectedItemField">',
|
|
f"{i}\t<dcsset:field>{esc_xml(field_name)}</dcsset:field>",
|
|
f"{i}</dcsset:item>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_data_param_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [f'{i}<dcscor:item xsi:type="dcsset:SettingsParameterValue">']
|
|
|
|
if parsed.get("use") is False:
|
|
lines.append(f"{i}\t<dcscor:use>false</dcscor:use>")
|
|
|
|
lines.append(f"{i}\t<dcscor:parameter>{esc_xml(parsed['parameter'])}</dcscor:parameter>")
|
|
|
|
if parsed.get("value") is not None:
|
|
val = parsed["value"]
|
|
if isinstance(val, dict) and val.get("variant"):
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="v8:StandardPeriod">')
|
|
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val["variant"])}</v8:variant>')
|
|
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
|
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
|
lines.append(f"{i}\t</dcscor:value>")
|
|
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
|
|
elif str(val) in ("true", "false"):
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="xs:boolean">{esc_xml(str(val))}</dcscor:value>')
|
|
else:
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
|
|
|
|
if parsed.get("viewMode"):
|
|
lines.append(f"{i}\t<dcsset:viewMode>{esc_xml(parsed['viewMode'])}</dcsset:viewMode>")
|
|
|
|
if parsed.get("userSettingID"):
|
|
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
|
|
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
|
|
|
|
lines.append(f"{i}</dcscor:item>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_order_item_fragment(parsed, indent):
|
|
i = indent
|
|
if parsed["field"] == "Auto":
|
|
return f'{i}<dcsset:item xsi:type="dcsset:OrderItemAuto"/>'
|
|
lines = [
|
|
f'{i}<dcsset:item xsi:type="dcsset:OrderItemField">',
|
|
f"{i}\t<dcsset:field>{esc_xml(parsed['field'])}</dcsset:field>",
|
|
f"{i}\t<dcsset:orderType>{parsed['direction']}</dcsset:orderType>",
|
|
f"{i}</dcsset:item>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_data_set_link_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [
|
|
f"{i}<dataSetLink>",
|
|
f"{i}\t<sourceDataSet>{esc_xml(parsed['source'])}</sourceDataSet>",
|
|
f"{i}\t<destinationDataSet>{esc_xml(parsed['dest'])}</destinationDataSet>",
|
|
f"{i}\t<sourceExpression>{esc_xml(parsed['sourceExpr'])}</sourceExpression>",
|
|
f"{i}\t<destinationExpression>{esc_xml(parsed['destExpr'])}</destinationExpression>",
|
|
]
|
|
if parsed.get("parameter"):
|
|
lines.append(f"{i}\t<parameter>{esc_xml(parsed['parameter'])}</parameter>")
|
|
lines.append(f"{i}</dataSetLink>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_data_set_query_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [
|
|
f'{i}<dataSet xsi:type="DataSetQuery">',
|
|
f"{i}\t<name>{esc_xml(parsed['name'])}</name>",
|
|
f"{i}\t<dataSource>{esc_xml(parsed['dataSource'])}</dataSource>",
|
|
f"{i}\t<query>{esc_xml(parsed['query'])}</query>",
|
|
f"{i}</dataSet>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_variant_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [
|
|
f"{i}<settingsVariant>",
|
|
f"{i}\t<dcsset:name>{esc_xml(parsed['name'])}</dcsset:name>",
|
|
build_mltext_xml("dcsset:presentation", parsed["presentation"], f"{i}\t"),
|
|
f'{i}\t<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">',
|
|
f"{i}\t\t<dcsset:selection>",
|
|
f'{i}\t\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>',
|
|
f"{i}\t\t</dcsset:selection>",
|
|
f'{i}\t\t<dcsset:item xsi:type="dcsset:StructureItemGroup">',
|
|
f"{i}\t\t\t<dcsset:groupItems/>",
|
|
f"{i}\t\t\t<dcsset:order>",
|
|
f'{i}\t\t\t\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>',
|
|
f"{i}\t\t\t</dcsset:order>",
|
|
f"{i}\t\t\t<dcsset:selection>",
|
|
f'{i}\t\t\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>',
|
|
f"{i}\t\t\t</dcsset:selection>",
|
|
f"{i}\t\t</dcsset:item>",
|
|
f"{i}\t</dcsset:settings>",
|
|
f"{i}</settingsVariant>",
|
|
]
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def _emit_filter_comparison(lines, f, indent):
|
|
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">')
|
|
lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(f["field"])}</dcsset:left>')
|
|
lines.append(f"{indent}\t<dcsset:comparisonType>{esc_xml(f['op'])}</dcsset:comparisonType>")
|
|
if f.get("value") is not None:
|
|
vt = f.get("valueType", "xs:string")
|
|
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{esc_xml(str(f["value"]))}</dcsset:right>')
|
|
lines.append(f"{indent}</dcsset:item>")
|
|
|
|
|
|
def build_conditional_appearance_item_fragment(parsed, indent):
|
|
i = indent
|
|
lines = [f"{i}<dcsset:item>"]
|
|
|
|
if parsed.get("fields"):
|
|
lines.append(f"{i}\t<dcsset:selection>")
|
|
for fld in parsed["fields"]:
|
|
lines.append(f"{i}\t\t<dcsset:item>")
|
|
lines.append(f"{i}\t\t\t<dcsset:field>{esc_xml(fld)}</dcsset:field>")
|
|
lines.append(f"{i}\t\t</dcsset:item>")
|
|
lines.append(f"{i}\t</dcsset:selection>")
|
|
else:
|
|
lines.append(f"{i}\t<dcsset:selection/>")
|
|
|
|
if parsed.get("filter"):
|
|
flt = parsed["filter"]
|
|
lines.append(f"{i}\t<dcsset:filter>")
|
|
if isinstance(flt, list):
|
|
# OrGroup
|
|
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:FilterItemGroup">')
|
|
lines.append(f"{i}\t\t\t<dcsset:groupType>OrGroup</dcsset:groupType>")
|
|
for f in flt:
|
|
_emit_filter_comparison(lines, f, f"{i}\t\t\t")
|
|
lines.append(f"{i}\t\t</dcsset:item>")
|
|
else:
|
|
_emit_filter_comparison(lines, flt, f"{i}\t\t")
|
|
lines.append(f"{i}\t</dcsset:filter>")
|
|
else:
|
|
lines.append(f"{i}\t<dcsset:filter/>")
|
|
|
|
# appearance
|
|
lines.append(f"{i}\t<dcsset:appearance>")
|
|
val = parsed["value"]
|
|
lines.append(f'{i}\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
|
|
lines.append(f"{i}\t\t\t<dcscor:parameter>{esc_xml(parsed['param'])}</dcscor:parameter>")
|
|
|
|
if re.match(r'^(web|style|win):', val):
|
|
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(val)}</dcscor:value>')
|
|
elif val in ("true", "false"):
|
|
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:boolean">{esc_xml(val)}</dcscor:value>')
|
|
elif parsed["param"] in ("Формат", "Текст", "Заголовок"):
|
|
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8:LocalStringType">')
|
|
lines.append(f"{i}\t\t\t\t<v8:item>")
|
|
lines.append(f"{i}\t\t\t\t\t<v8:lang>ru</v8:lang>")
|
|
lines.append(f"{i}\t\t\t\t\t<v8:content>{esc_xml(val)}</v8:content>")
|
|
lines.append(f"{i}\t\t\t\t</v8:item>")
|
|
lines.append(f"{i}\t\t\t</dcscor:value>")
|
|
else:
|
|
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:string">{esc_xml(val)}</dcscor:value>')
|
|
|
|
lines.append(f"{i}\t\t</dcscor:item>")
|
|
lines.append(f"{i}\t</dcsset:appearance>")
|
|
|
|
lines.append(f"{i}</dcsset:item>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_structure_item_fragment(item, indent):
|
|
i = indent
|
|
lines = [f'{i}<dcsset:item xsi:type="dcsset:StructureItemGroup">']
|
|
|
|
if item.get("name"):
|
|
lines.append(f"{i}\t<dcsset:name>{esc_xml(item['name'])}</dcsset:name>")
|
|
|
|
group_by = item.get("groupBy", [])
|
|
if not group_by:
|
|
lines.append(f"{i}\t<dcsset:groupItems/>")
|
|
else:
|
|
lines.append(f"{i}\t<dcsset:groupItems>")
|
|
for field in group_by:
|
|
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:GroupItemField">')
|
|
lines.append(f"{i}\t\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>")
|
|
lines.append(f"{i}\t\t\t<dcsset:groupType>Items</dcsset:groupType>")
|
|
lines.append(f"{i}\t\t\t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>")
|
|
lines.append(f'{i}\t\t\t<dcsset:periodAdditionBegin xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionBegin>')
|
|
lines.append(f'{i}\t\t\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>')
|
|
lines.append(f"{i}\t\t</dcsset:item>")
|
|
lines.append(f"{i}\t</dcsset:groupItems>")
|
|
|
|
lines.append(f"{i}\t<dcsset:order>")
|
|
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
|
|
lines.append(f"{i}\t</dcsset:order>")
|
|
lines.append(f"{i}\t<dcsset:selection>")
|
|
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>')
|
|
lines.append(f"{i}\t</dcsset:selection>")
|
|
|
|
if item.get("children"):
|
|
for child in item["children"]:
|
|
lines.append(build_structure_item_fragment(child, f"{i}\t"))
|
|
|
|
lines.append(f"{i}</dcsset:item>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
def build_output_param_fragment(parsed, indent):
|
|
i = indent
|
|
key = parsed["key"]
|
|
val = parsed["value"]
|
|
ptype = output_param_types.get(key, "xs:string")
|
|
|
|
lines = [f'{i}<dcscor:item xsi:type="dcsset:SettingsParameterValue">']
|
|
lines.append(f"{i}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>")
|
|
|
|
if ptype == "mltext":
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="v8:LocalStringType">')
|
|
lines.append(f"{i}\t\t<v8:item>")
|
|
lines.append(f"{i}\t\t\t<v8:lang>ru</v8:lang>")
|
|
lines.append(f"{i}\t\t\t<v8:content>{esc_xml(val)}</v8:content>")
|
|
lines.append(f"{i}\t\t</v8:item>")
|
|
lines.append(f"{i}\t</dcscor:value>")
|
|
else:
|
|
lines.append(f'{i}\t<dcscor:value xsi:type="{ptype}">{esc_xml(val)}</dcscor:value>')
|
|
|
|
lines.append(f"{i}</dcscor:item>")
|
|
return "\r\n".join(lines)
|
|
|
|
|
|
# ── 5. XML helpers ──────────────────────────────────────────
|
|
|
|
def import_fragment(doc_root, xml_string):
|
|
wrapper = f"<_W {WRAPPER_NS}>{xml_string}</_W>"
|
|
frag_parser = etree.XMLParser(remove_blank_text=False)
|
|
frag = etree.fromstring(wrapper.encode("utf-8"), frag_parser)
|
|
nodes = []
|
|
for child in frag:
|
|
if isinstance(child.tag, str):
|
|
nodes.append(child)
|
|
return nodes
|
|
|
|
|
|
def get_child_indent(container):
|
|
for i, child in enumerate(container):
|
|
txt = container.text if i == 0 else container[i - 1].tail
|
|
if txt:
|
|
m = re.search(r'\n(\t+)$', txt)
|
|
if m:
|
|
return m.group(1)
|
|
# Fallback: count depth
|
|
depth = 0
|
|
current = container
|
|
while current is not None:
|
|
parent = current.getparent()
|
|
if parent is None:
|
|
break
|
|
depth += 1
|
|
current = parent
|
|
return "\t" * (depth + 1)
|
|
|
|
|
|
def insert_before_element(container, new_node, ref_node, child_indent):
|
|
if ref_node is not None:
|
|
idx = list(container).index(ref_node)
|
|
if idx == 0:
|
|
prev_text = container.text or ""
|
|
container.text = prev_text.rstrip("\n\t") + "\n" + child_indent
|
|
container.insert(idx, new_node)
|
|
new_node.tail = "\n" + child_indent
|
|
else:
|
|
prev = container[idx - 1]
|
|
prev.tail = (prev.tail or "").rstrip("\n\t") + "\n" + child_indent
|
|
container.insert(idx, new_node)
|
|
new_node.tail = "\n" + child_indent
|
|
else:
|
|
# Append at end
|
|
children = list(container)
|
|
if children:
|
|
last = children[-1]
|
|
last.tail = (last.tail or "").rstrip("\n\t") + "\n" + child_indent
|
|
container.append(new_node)
|
|
parent_indent = child_indent[:-1] if len(child_indent) > 1 else ""
|
|
new_node.tail = "\n" + parent_indent
|
|
else:
|
|
container.text = "\n" + child_indent
|
|
container.append(new_node)
|
|
parent_indent = child_indent[:-1] if len(child_indent) > 1 else ""
|
|
new_node.tail = "\n" + parent_indent
|
|
|
|
|
|
def clear_container_children(container):
|
|
to_remove = [ch for ch in container if isinstance(ch.tag, str)]
|
|
for el in to_remove:
|
|
remove_node_with_whitespace(el)
|
|
|
|
|
|
def remove_node_with_whitespace(node):
|
|
parent = node.getparent()
|
|
idx = list(parent).index(node)
|
|
# Remove the node and adjust whitespace
|
|
if idx > 0:
|
|
prev = parent[idx - 1]
|
|
# Preserve the tail of the removed node's predecessor
|
|
prev.tail = node.tail
|
|
elif idx == 0:
|
|
parent.text = node.tail
|
|
parent.remove(node)
|
|
|
|
|
|
def find_first_element(container, local_names, ns_uri=None):
|
|
for child in container:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
ln = local_name(child)
|
|
if ln in local_names:
|
|
if not ns_uri or etree.QName(child.tag).namespace == ns_uri:
|
|
return child
|
|
return None
|
|
|
|
|
|
def find_last_element(container, ln_name, ns_uri=None):
|
|
last = None
|
|
for child in container:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
if local_name(child) == ln_name:
|
|
if not ns_uri or etree.QName(child.tag).namespace == ns_uri:
|
|
last = child
|
|
return last
|
|
|
|
|
|
def find_element_by_child_value(container, elem_name, child_name, child_value, ns_uri=None):
|
|
for child in container:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
if local_name(child) != elem_name:
|
|
continue
|
|
if ns_uri and etree.QName(child.tag).namespace != ns_uri:
|
|
continue
|
|
for gc in child:
|
|
if isinstance(gc.tag, str) and local_name(gc) == child_name and (gc.text or "").strip() == child_value:
|
|
return child
|
|
return None
|
|
|
|
|
|
def set_or_create_child_element(parent, ln, ns_uri, value, indent):
|
|
existing = None
|
|
for ch in parent:
|
|
if isinstance(ch.tag, str) and local_name(ch) == ln and etree.QName(ch.tag).namespace == ns_uri:
|
|
existing = ch
|
|
break
|
|
if existing is not None:
|
|
existing.text = value
|
|
else:
|
|
prefix = None
|
|
for p, uri in parent.nsmap.items():
|
|
if uri == ns_uri:
|
|
prefix = p
|
|
break
|
|
qual_name = f"{prefix}:{ln}" if prefix else ln
|
|
frag_xml = f"{indent}<{qual_name}>{esc_xml(value)}</{qual_name}>"
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(parent, node, None, indent)
|
|
|
|
|
|
def set_or_create_child_element_with_attr(parent, ln, ns_uri, value, xsi_type, indent):
|
|
existing = None
|
|
for ch in parent:
|
|
if isinstance(ch.tag, str) and local_name(ch) == ln and etree.QName(ch.tag).namespace == ns_uri:
|
|
existing = ch
|
|
break
|
|
if existing is not None:
|
|
existing.text = value
|
|
if xsi_type:
|
|
existing.set(XSI_TYPE, xsi_type)
|
|
else:
|
|
prefix = None
|
|
for p, uri in parent.nsmap.items():
|
|
if uri == ns_uri:
|
|
prefix = p
|
|
break
|
|
qual_name = f"{prefix}:{ln}" if prefix else ln
|
|
type_attr = f' xsi:type="{xsi_type}"' if xsi_type else ""
|
|
frag_xml = f"{indent}<{qual_name}{type_attr}>{esc_xml(value)}</{qual_name}>"
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(parent, node, None, indent)
|
|
|
|
|
|
def resolve_data_set():
|
|
root_el = xml_doc
|
|
|
|
if data_set_arg:
|
|
for child in root_el:
|
|
if isinstance(child.tag, str) and local_name(child) == "dataSet" and etree.QName(child.tag).namespace == SCH_NS:
|
|
for gc in child:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
|
if gc.text == data_set_arg:
|
|
return child
|
|
print(f"DataSet '{data_set_arg}' not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for child in root_el:
|
|
if isinstance(child.tag, str) and local_name(child) == "dataSet" and etree.QName(child.tag).namespace == SCH_NS:
|
|
return child
|
|
print("No dataSet found in DCS", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def resolve_variant_settings():
|
|
root_el = xml_doc
|
|
sv = None
|
|
|
|
if variant_arg:
|
|
for child in root_el:
|
|
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
|
|
for gc in child:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS:
|
|
if gc.text == variant_arg:
|
|
sv = child
|
|
break
|
|
if sv:
|
|
break
|
|
if sv is None:
|
|
print(f"Variant '{variant_arg}' not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
else:
|
|
for child in root_el:
|
|
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
|
|
sv = child
|
|
break
|
|
if sv is None:
|
|
print("No settingsVariant found in DCS", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for gc in sv:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "settings" and etree.QName(gc.tag).namespace == SET_NS:
|
|
return gc
|
|
|
|
print("No <dcsset:settings> found in variant", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def ensure_settings_child(settings, child_name, after_siblings):
|
|
el = find_first_element(settings, [child_name], SET_NS)
|
|
if el is not None:
|
|
return el
|
|
|
|
indent = get_child_indent(settings)
|
|
frag_xml = f"{indent}<dcsset:{child_name}/>"
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
ref_node = None
|
|
for sib_name in after_siblings:
|
|
sib = find_first_element(settings, [sib_name], SET_NS)
|
|
if sib is not None:
|
|
# Get next element sibling
|
|
found = False
|
|
for ch in settings:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is sib:
|
|
found = True
|
|
break
|
|
|
|
for node in nodes:
|
|
insert_before_element(settings, node, ref_node, indent)
|
|
|
|
return find_first_element(settings, [child_name], SET_NS)
|
|
|
|
|
|
def get_variant_name():
|
|
if variant_arg:
|
|
return variant_arg
|
|
root_el = xml_doc
|
|
for child in root_el:
|
|
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
|
|
for gc in child:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS:
|
|
return gc.text or "(unknown)"
|
|
return "(unknown)"
|
|
|
|
|
|
def get_data_set_name(ds_node):
|
|
for gc in ds_node:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
|
return gc.text or "(unknown)"
|
|
return "(unknown)"
|
|
|
|
|
|
def get_container_child_indent(container):
|
|
has_elements = any(isinstance(ch.tag, str) for ch in container)
|
|
if has_elements:
|
|
return get_child_indent(container)
|
|
else:
|
|
parent_indent = get_child_indent(container.getparent())
|
|
return parent_indent + "\t"
|
|
|
|
|
|
# ── 6. Load XML ─────────────────────────────────────────────
|
|
|
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
tree = etree.parse(resolved_path, xml_parser)
|
|
xml_doc = tree.getroot()
|
|
|
|
# ── 7. Batch value splitting ────────────────────────────────
|
|
|
|
if operation in ("set-query", "set-structure", "modify-structure", "add-dataSet"):
|
|
values = [value_arg]
|
|
elif operation == "patch-query":
|
|
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
|
|
elif operation == "add-drilldown":
|
|
if ";;" in value_arg:
|
|
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
|
|
else:
|
|
values = [v.strip() for v in value_arg.split(",") if v.strip()]
|
|
else:
|
|
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
|
|
|
|
# ── 8. Main logic ───────────────────────────────────────────
|
|
|
|
if operation == "add-field":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
|
|
for val in values:
|
|
parsed = parse_field_shorthand(val)
|
|
child_indent = get_child_indent(ds_node)
|
|
|
|
existing = find_element_by_child_value(ds_node, "field", "dataPath", parsed["dataPath"], SCH_NS)
|
|
if existing is not None:
|
|
print(f'[WARN] Field "{parsed["dataPath"]}" already exists in dataset "{ds_name}" -- skipped')
|
|
continue
|
|
|
|
frag_xml = build_field_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
ref_node = find_first_element(ds_node, ["dataSource"], SCH_NS)
|
|
for node in nodes:
|
|
insert_before_element(ds_node, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] Field "{parsed["dataPath"]}" added to dataset "{ds_name}"')
|
|
|
|
if not no_selection:
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
selection = ensure_settings_child(settings, "selection", [])
|
|
existing_sel = find_element_by_child_value(selection, "item", "field", parsed["dataPath"], SET_NS)
|
|
if existing_sel is not None:
|
|
print(f'[INFO] Field "{parsed["dataPath"]}" already in selection -- skipped')
|
|
else:
|
|
sel_indent = get_container_child_indent(selection)
|
|
sel_xml = build_selection_item_fragment(parsed["dataPath"], sel_indent)
|
|
sel_nodes = import_fragment(xml_doc, sel_xml)
|
|
for node in sel_nodes:
|
|
insert_before_element(selection, node, None, sel_indent)
|
|
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
|
|
|
|
elif operation == "add-total":
|
|
for val in values:
|
|
parsed = parse_total_shorthand(val)
|
|
child_indent = get_child_indent(xml_doc)
|
|
|
|
existing = find_element_by_child_value(xml_doc, "totalField", "dataPath", parsed["dataPath"], SCH_NS)
|
|
if existing is not None:
|
|
print(f'[WARN] TotalField "{parsed["dataPath"]}" already exists -- skipped')
|
|
continue
|
|
|
|
frag_xml = build_total_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
last_total = find_last_element(xml_doc, "totalField", SCH_NS)
|
|
if last_total is not None:
|
|
# Insert after last totalField - find next element
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_total:
|
|
found = True
|
|
else:
|
|
ref_node = find_first_element(xml_doc, ["parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
|
|
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] TotalField "{parsed["dataPath"]}" = {parsed["expression"]} added')
|
|
|
|
elif operation == "add-calculated-field":
|
|
for val in values:
|
|
parsed = parse_calc_shorthand(val)
|
|
child_indent = get_child_indent(xml_doc)
|
|
|
|
existing = find_element_by_child_value(xml_doc, "calculatedField", "dataPath", parsed["dataPath"], SCH_NS)
|
|
if existing is not None:
|
|
print(f'[WARN] CalculatedField "{parsed["dataPath"]}" already exists -- skipped')
|
|
continue
|
|
|
|
frag_xml = build_calc_field_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
last_calc = find_last_element(xml_doc, "calculatedField", SCH_NS)
|
|
if last_calc is not None:
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_calc:
|
|
found = True
|
|
else:
|
|
ref_node = find_first_element(xml_doc, ["totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
|
|
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] CalculatedField "{parsed["dataPath"]}" = {parsed["expression"]} added')
|
|
|
|
if not no_selection:
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
selection = ensure_settings_child(settings, "selection", [])
|
|
existing_sel = find_element_by_child_value(selection, "item", "field", parsed["dataPath"], SET_NS)
|
|
if existing_sel is not None:
|
|
print(f'[INFO] Field "{parsed["dataPath"]}" already in selection -- skipped')
|
|
else:
|
|
sel_indent = get_container_child_indent(selection)
|
|
sel_xml = build_selection_item_fragment(parsed["dataPath"], sel_indent)
|
|
sel_nodes = import_fragment(xml_doc, sel_xml)
|
|
for node in sel_nodes:
|
|
insert_before_element(selection, node, None, sel_indent)
|
|
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
|
|
|
|
elif operation == "add-parameter":
|
|
for val in values:
|
|
parsed = parse_param_shorthand(val)
|
|
child_indent = get_child_indent(xml_doc)
|
|
|
|
existing = find_element_by_child_value(xml_doc, "parameter", "name", parsed["name"], SCH_NS)
|
|
if existing is not None:
|
|
print(f'[WARN] Parameter "{parsed["name"]}" already exists -- skipped')
|
|
continue
|
|
|
|
fragments = build_param_fragment(parsed, child_indent)
|
|
|
|
last_param = find_last_element(xml_doc, "parameter", SCH_NS)
|
|
if last_param is not None:
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_param:
|
|
found = True
|
|
else:
|
|
ref_node = find_first_element(xml_doc, ["template", "groupTemplate", "settingsVariant"], SCH_NS)
|
|
|
|
for frag_xml in fragments:
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] Parameter "{parsed["name"]}" added')
|
|
if parsed.get("autoDates"):
|
|
print('[OK] Auto-parameters "\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430", "\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f" added')
|
|
|
|
elif operation == "modify-parameter":
|
|
for val in values:
|
|
# Extract optional [Title] first (mirrors parse_field_shorthand)
|
|
title_val = None
|
|
m_title = re.search(r'\[([^\]]*)\]', val)
|
|
if m_title:
|
|
title_val = m_title.group(1).strip()
|
|
val = re.sub(r'\s*\[[^\]]*\]\s*', ' ', val).strip()
|
|
|
|
parts = val.split(None, 1)
|
|
param_name = parts[0].strip()
|
|
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
|
|
flag_hidden = False
|
|
flag_always = False
|
|
if re.search(r'@hidden\b', rest):
|
|
flag_hidden = True
|
|
rest = re.sub(r'\s*@hidden\b', '', rest).strip()
|
|
if re.search(r'@always\b', rest):
|
|
flag_always = True
|
|
rest = re.sub(r'\s*@always\b', '', rest).strip()
|
|
|
|
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
|
|
if param_el is None:
|
|
print(f'[WARN] Parameter "{param_name}" not found -- skipped')
|
|
continue
|
|
|
|
child_indent = get_child_indent(param_el)
|
|
|
|
# Set/replace title (must come right after <name>, before <valueType>)
|
|
if title_val is not None:
|
|
existing_title = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "title"), None)
|
|
if existing_title is not None:
|
|
remove_node_with_whitespace(existing_title)
|
|
# Insert before the first child after <name>
|
|
title_ref = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) != "name"), None)
|
|
title_frag = build_mltext_xml("title", title_val, child_indent)
|
|
for node in import_fragment(xml_doc, title_frag):
|
|
insert_before_element(param_el, node, title_ref, child_indent)
|
|
print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
|
|
|
|
# Separate availableValue=... from simple kv pairs
|
|
simple_rest = rest
|
|
av_part = None
|
|
av_idx = rest.find("availableValue=")
|
|
if av_idx >= 0:
|
|
simple_rest = rest[:av_idx].strip()
|
|
av_part = rest[av_idx:]
|
|
|
|
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
|
|
if simple_rest:
|
|
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
|
|
key, value = m.group(1), m.group(2)
|
|
existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
|
|
if key == "value":
|
|
# Rebuild <value> with correct xsi:type from <valueType>
|
|
declared_type = ""
|
|
vt_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
if vt_el is not None:
|
|
for tnode in vt_el:
|
|
if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
|
|
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
|
|
break
|
|
value_lines = build_param_value_xml(declared_type, value, child_indent)
|
|
frag_xml = "\r\n".join(value_lines)
|
|
was_existing = existing is not None
|
|
if existing is not None:
|
|
# Find next-element sibling as ref before removing
|
|
idx = list(param_el).index(existing)
|
|
ref_node = param_el[idx + 1] if idx + 1 < len(param_el) else None
|
|
remove_node_with_whitespace(existing)
|
|
else:
|
|
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("useRestriction", "availableValue", "denyIncompleteValues", "use")), None)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(param_el, node, ref_node, child_indent)
|
|
verb = "updated" if was_existing else "added"
|
|
print(f'[OK] Parameter "{param_name}": value {verb} to {value}')
|
|
elif existing is not None:
|
|
existing.text = value
|
|
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
|
|
else:
|
|
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
|
|
ref_node = None
|
|
if key == "denyIncompleteValues":
|
|
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use"), None)
|
|
frag_xml = f"{child_indent}<{key}>{esc_xml(value)}</{key}>"
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(param_el, node, ref_node, child_indent)
|
|
print(f'[OK] Parameter "{param_name}": {key}={value} added')
|
|
|
|
# Process availableValue
|
|
if av_part:
|
|
av_rest = av_part[len("availableValue="):].strip()
|
|
av_items = parse_available_value_list(av_rest)
|
|
|
|
# Prefer declared <valueType> of the parameter; fall back to value pattern
|
|
declared_type = ""
|
|
vt_el = None
|
|
for ch in param_el:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS:
|
|
vt_el = ch
|
|
break
|
|
if vt_el is not None:
|
|
for tnode in vt_el:
|
|
if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
|
|
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
|
|
break
|
|
|
|
# Remove all existing <availableValue>
|
|
to_remove = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "availableValue" and etree.QName(ch.tag).namespace == SCH_NS]
|
|
for el in to_remove:
|
|
remove_node_with_whitespace(el)
|
|
|
|
# Insert each new <availableValue> before (denyIncompleteValues, use)
|
|
ref_node = None
|
|
for child in param_el:
|
|
if isinstance(child.tag, str) and local_name(child) in ("denyIncompleteValues", "use"):
|
|
ref_node = child
|
|
break
|
|
for av in av_items:
|
|
av_lines = build_available_value_fragment(av, declared_type, child_indent)
|
|
frag_xml = "\r\n".join(av_lines)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(param_el, node, ref_node, child_indent)
|
|
print(f'[OK] Parameter "{param_name}": availableValue set to {len(av_items)} item(s)')
|
|
|
|
if flag_hidden:
|
|
ur_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "useRestriction" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
if ur_el is not None:
|
|
if (ur_el.text or "").strip() != "true":
|
|
ur_el.text = "true"
|
|
else:
|
|
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("expression", "availableAsField", "availableValue", "denyIncompleteValues", "use")), None)
|
|
for node in import_fragment(xml_doc, f"{child_indent}<useRestriction>true</useRestriction>"):
|
|
insert_before_element(param_el, node, ref_node, child_indent)
|
|
|
|
af_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "availableAsField" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
if af_el is not None:
|
|
if (af_el.text or "").strip() != "false":
|
|
af_el.text = "false"
|
|
else:
|
|
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("availableValue", "denyIncompleteValues", "use")), None)
|
|
for node in import_fragment(xml_doc, f"{child_indent}<availableAsField>false</availableAsField>"):
|
|
insert_before_element(param_el, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] Parameter "{param_name}": @hidden applied')
|
|
|
|
if flag_always:
|
|
use_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
if use_el is not None:
|
|
if (use_el.text or "").strip() != "Always":
|
|
use_el.text = "Always"
|
|
else:
|
|
for node in import_fragment(xml_doc, f"{child_indent}<use>Always</use>"):
|
|
insert_before_element(param_el, node, None, child_indent)
|
|
print(f'[OK] Parameter "{param_name}": @always applied')
|
|
|
|
elif operation == "rename-parameter":
|
|
root = xml_doc
|
|
for val in values:
|
|
m_rn = re.match(r'^\s*(.+?)\s*=>\s*(.+?)\s*$', val)
|
|
if not m_rn:
|
|
print(f'[WARN] rename-parameter expects "OldName => NewName", got: {val}')
|
|
continue
|
|
old_name = m_rn.group(1).strip()
|
|
new_name = m_rn.group(2).strip()
|
|
|
|
if old_name == new_name:
|
|
print('[WARN] rename-parameter: old and new names are equal -- skipped')
|
|
continue
|
|
|
|
# 1. Rename <parameter><name>OldName</name>
|
|
param_el = find_element_by_child_value(root, "parameter", "name", old_name, SCH_NS)
|
|
if param_el is None:
|
|
print(f'[WARN] Parameter "{old_name}" not found -- skipped')
|
|
continue
|
|
for ch in param_el:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "name" and etree.QName(ch.tag).namespace == SCH_NS:
|
|
ch.text = new_name
|
|
break
|
|
|
|
# 2. Update <expression> in other <parameter> elements.
|
|
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
|
|
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
|
|
esc_old = re.escape(old_name)
|
|
expr_regex = re.compile(rf'&{esc_old}(?=[^\w\u0400-\u04FF]|$)')
|
|
expr_updated = 0
|
|
for ch in root:
|
|
if not (isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS):
|
|
continue
|
|
for gc in ch:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "expression" and etree.QName(gc.tag).namespace == SCH_NS:
|
|
old_expr = gc.text or ""
|
|
new_expr = expr_regex.sub(f'&{new_name}', old_expr)
|
|
if new_expr != old_expr:
|
|
gc.text = new_expr
|
|
expr_updated += 1
|
|
|
|
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
|
|
dp_updated = 0
|
|
for variant_node in root:
|
|
if not (isinstance(variant_node.tag, str) and local_name(variant_node) == "settingsVariant" and etree.QName(variant_node.tag).namespace == SCH_NS):
|
|
continue
|
|
settings_node = find_first_element(variant_node, ["settings"], SET_NS)
|
|
if settings_node is None:
|
|
continue
|
|
dp_el = find_first_element(settings_node, ["dataParameters"], SET_NS)
|
|
if dp_el is None:
|
|
continue
|
|
for item in dp_el:
|
|
if not (isinstance(item.tag, str) and local_name(item) == "item"):
|
|
continue
|
|
for gc in item:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "parameter" and etree.QName(gc.tag).namespace == COR_NS:
|
|
if (gc.text or "").strip() == old_name:
|
|
gc.text = new_name
|
|
dp_updated += 1
|
|
|
|
print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
|
|
|
|
elif operation == "reorder-parameters":
|
|
root = xml_doc
|
|
for val in values:
|
|
order = [s.strip() for s in val.split(",") if s.strip()]
|
|
if not order:
|
|
print('[WARN] reorder-parameters: empty list -- skipped')
|
|
continue
|
|
|
|
all_params = []
|
|
for ch in root:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS:
|
|
all_params.append(ch)
|
|
if not all_params:
|
|
print('[WARN] reorder-parameters: no parameters in schema')
|
|
continue
|
|
|
|
child_indent = get_child_indent(root)
|
|
|
|
by_name = {}
|
|
for pe in all_params:
|
|
for gc in pe:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
|
by_name[(gc.text or "").strip()] = pe
|
|
break
|
|
|
|
new_order = []
|
|
used = set()
|
|
for name in order:
|
|
if name in by_name:
|
|
new_order.append(by_name[name])
|
|
used.add(name)
|
|
else:
|
|
print(f'[WARN] reorder-parameters: parameter "{name}" not found -- skipped')
|
|
|
|
for pe in all_params:
|
|
pe_name = None
|
|
for gc in pe:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
|
pe_name = (gc.text or "").strip()
|
|
break
|
|
if pe_name and pe_name not in used:
|
|
new_order.append(pe)
|
|
|
|
# Anchor: element right after the last parameter in original order
|
|
last_param = all_params[-1]
|
|
anchor = last_param.getnext()
|
|
|
|
# Remove all parameters with surrounding whitespace
|
|
for pe in all_params:
|
|
remove_node_with_whitespace(pe)
|
|
|
|
# Re-insert in new order before anchor
|
|
for pe in new_order:
|
|
insert_before_element(root, pe, anchor, child_indent)
|
|
|
|
print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
|
|
|
|
elif operation == "add-filter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_filter_shorthand(val)
|
|
filter_el = ensure_settings_child(settings, "filter", ["selection"])
|
|
filter_indent = get_container_child_indent(filter_el)
|
|
frag_xml = build_filter_item_fragment(parsed, filter_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(filter_el, node, None, filter_indent)
|
|
print(f'[OK] Filter "{parsed["field"]} {parsed["op"]}" added to variant "{var_name}"')
|
|
|
|
elif operation == "add-dataParameter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_data_param_shorthand(val)
|
|
dp_el = ensure_settings_child(settings, "dataParameters", ["outputParameters", "conditionalAppearance", "order", "filter", "selection"])
|
|
dp_indent = get_container_child_indent(dp_el)
|
|
frag_xml = build_data_param_fragment(parsed, dp_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(dp_el, node, None, dp_indent)
|
|
print(f'[OK] DataParameter "{parsed["parameter"]}" added to variant "{var_name}"')
|
|
|
|
elif operation == "add-order":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_order_shorthand(val)
|
|
order_el = ensure_settings_child(settings, "order", ["filter", "selection"])
|
|
order_indent = get_container_child_indent(order_el)
|
|
|
|
if parsed["field"] == "Auto":
|
|
is_dup = False
|
|
for ch in order_el:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "item":
|
|
type_attr = ch.get(XSI_TYPE, "")
|
|
if "OrderItemAuto" in type_attr:
|
|
is_dup = True
|
|
break
|
|
if is_dup:
|
|
print(f'[WARN] OrderItemAuto already exists in variant "{var_name}" -- skipped')
|
|
continue
|
|
else:
|
|
existing_ord = find_element_by_child_value(order_el, "item", "field", parsed["field"], SET_NS)
|
|
if existing_ord is not None:
|
|
print(f'[WARN] Order "{parsed["field"]}" already exists in variant "{var_name}" -- skipped')
|
|
continue
|
|
|
|
frag_xml = build_order_item_fragment(parsed, order_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(order_el, node, None, order_indent)
|
|
|
|
desc = "Auto" if parsed["field"] == "Auto" else f"{parsed['field']} {parsed['direction']}"
|
|
print(f'[OK] Order "{desc}" added to variant "{var_name}"')
|
|
|
|
elif operation == "add-selection":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
field_name = val.strip()
|
|
group_name = None
|
|
|
|
# Extract @group=Name
|
|
gm = re.search(r'\s*@group=(\S+)', field_name)
|
|
if gm:
|
|
group_name = gm.group(1)
|
|
field_name = re.sub(r'\s*@group=\S+', '', field_name).strip()
|
|
|
|
if group_name:
|
|
# Find named StructureItemGroup
|
|
target_el = None
|
|
for item in settings.iter(f"{{{SET_NS}}}item"):
|
|
xsi_type = item.get(f"{{{XSI_NS}}}type", "")
|
|
if "StructureItemGroup" in xsi_type:
|
|
name_el = item.find(f"{{{SET_NS}}}name")
|
|
if name_el is not None and name_el.text == group_name:
|
|
target_el = item
|
|
break
|
|
if target_el is None:
|
|
print(f'[WARN] StructureItemGroup "{group_name}" not found -- adding to variant level')
|
|
target_el = settings
|
|
else:
|
|
target_el = settings
|
|
|
|
selection = ensure_settings_child(target_el, "selection", [])
|
|
|
|
# Dedup: skip if SelectedItemAuto already exists
|
|
if field_name == "Auto":
|
|
is_dup = False
|
|
for ch in selection:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "item":
|
|
type_attr = ch.get(XSI_TYPE, "")
|
|
if "SelectedItemAuto" in type_attr:
|
|
is_dup = True
|
|
break
|
|
if is_dup:
|
|
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
|
|
print(f'[WARN] SelectedItemAuto already exists in {target} -- skipped')
|
|
continue
|
|
|
|
sel_indent = get_container_child_indent(selection)
|
|
sel_xml = build_selection_item_fragment(field_name, sel_indent)
|
|
sel_nodes = import_fragment(xml_doc, sel_xml)
|
|
for node in sel_nodes:
|
|
insert_before_element(selection, node, None, sel_indent)
|
|
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
|
|
print(f'[OK] Selection "{field_name}" added to {target}')
|
|
|
|
elif operation == "set-query":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
query_el = find_first_element(ds_node, ["query"], SCH_NS)
|
|
if query_el is None:
|
|
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
|
|
sys.exit(1)
|
|
query_el.text = resolve_query_value(value_arg, query_base_dir)
|
|
print(f'[OK] Query replaced in dataset "{ds_name}"')
|
|
|
|
elif operation == "patch-query":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
query_el = find_first_element(ds_node, ["query"], SCH_NS)
|
|
if query_el is None:
|
|
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
|
|
sys.exit(1)
|
|
for val in values:
|
|
once = False
|
|
if re.search(r'@once\b', val):
|
|
once = True
|
|
val = re.sub(r'\s*@once\b', '', val).strip()
|
|
|
|
sep_idx = val.find(" => ")
|
|
if sep_idx < 0:
|
|
print("patch-query value must contain ' => ' separator: old => new", file=sys.stderr)
|
|
sys.exit(1)
|
|
old_str = val[:sep_idx]
|
|
new_str = val[sep_idx + 4:]
|
|
query_text = query_el.text or ""
|
|
|
|
count = query_text.count(old_str)
|
|
if count == 0:
|
|
print(f"Substring not found in query of dataset '{ds_name}': {old_str}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if once and count != 1:
|
|
print(f"@once: expected 1 occurrence of '{old_str}' in dataset '{ds_name}', found {count}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
query_el.text = query_text.replace(old_str, new_str)
|
|
suffix = " (1 occurrence)" if once else f" ({count} occurrence(s))"
|
|
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'{suffix}')
|
|
|
|
elif operation == "set-outputParameter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_output_param_shorthand(val)
|
|
output_el = ensure_settings_child(settings, "outputParameters", ["conditionalAppearance", "order", "filter", "selection"])
|
|
output_indent = get_container_child_indent(output_el)
|
|
|
|
existing_param = find_element_by_child_value(output_el, "item", "parameter", parsed["key"], COR_NS)
|
|
if existing_param is not None:
|
|
remove_node_with_whitespace(existing_param)
|
|
print(f'[OK] Replaced outputParameter "{parsed["key"]}" in variant "{var_name}"')
|
|
else:
|
|
print(f'[OK] OutputParameter "{parsed["key"]}" added to variant "{var_name}"')
|
|
|
|
frag_xml = build_output_param_fragment(parsed, output_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(output_el, node, None, output_indent)
|
|
|
|
elif operation == "set-structure":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
|
|
to_remove = [ch for ch in settings if isinstance(ch.tag, str) and local_name(ch) == "item" and etree.QName(ch.tag).namespace == SET_NS]
|
|
for el in to_remove:
|
|
remove_node_with_whitespace(el)
|
|
|
|
struct_items = parse_structure_shorthand(value_arg)
|
|
settings_indent = get_child_indent(settings)
|
|
|
|
ref_node = find_first_element(settings, ["outputParameters", "dataParameters", "conditionalAppearance", "order", "filter", "selection", "item"], SET_NS)
|
|
|
|
for struct_item in struct_items:
|
|
frag_xml = build_structure_item_fragment(struct_item, settings_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(settings, node, ref_node, settings_indent)
|
|
|
|
print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
|
|
|
|
elif operation == "modify-structure":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
|
|
struct_items = parse_structure_shorthand(value_arg)
|
|
|
|
# Flatten parsed tree into (name, groupBy) targets
|
|
targets = []
|
|
stack = list(struct_items)
|
|
while stack:
|
|
it = stack.pop()
|
|
if it.get("name"):
|
|
targets.append({"name": it["name"], "groupBy": it.get("groupBy", [])})
|
|
for ch in it.get("children", []) or []:
|
|
stack.append(ch)
|
|
|
|
if not targets:
|
|
print(f"modify-structure requires @name= for at least one group: {value_arg}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
ns = {"dcsset": SET_NS, "xsi": XSI_NS}
|
|
|
|
for t in targets:
|
|
xpath = f".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='{t['name']}']"
|
|
group_el = settings.find(xpath, ns)
|
|
if group_el is None:
|
|
print(f'[WARN] Group with @name="{t["name"]}" not found — skipped')
|
|
continue
|
|
|
|
gi_el = None
|
|
for ch in group_el:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
|
|
gi_el = ch
|
|
break
|
|
|
|
group_indent = get_child_indent(group_el)
|
|
if gi_el is None:
|
|
# Insert <groupItems> after <name>, before first non-name sibling
|
|
ref_after_name = None
|
|
saw_name = False
|
|
for ch in group_el:
|
|
if not isinstance(ch.tag, str):
|
|
continue
|
|
if local_name(ch) == "name" and etree.QName(ch.tag).namespace == SET_NS:
|
|
saw_name = True
|
|
elif saw_name:
|
|
ref_after_name = ch
|
|
break
|
|
gi_frag = f"{group_indent}<dcsset:groupItems></dcsset:groupItems>"
|
|
for node in import_fragment(xml_doc, gi_frag):
|
|
insert_before_element(group_el, node, ref_after_name, group_indent)
|
|
for ch in group_el:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
|
|
gi_el = ch
|
|
break
|
|
|
|
to_remove = [ch for ch in gi_el if isinstance(ch.tag, str)]
|
|
for el in to_remove:
|
|
remove_node_with_whitespace(el)
|
|
|
|
item_indent = group_indent + "\t"
|
|
|
|
for field in t["groupBy"]:
|
|
lines = [
|
|
f'{item_indent}<dcsset:item xsi:type="dcsset:GroupItemField">',
|
|
f'{item_indent}\t<dcsset:field>{esc_xml(field)}</dcsset:field>',
|
|
f'{item_indent}\t<dcsset:groupType>Items</dcsset:groupType>',
|
|
f'{item_indent}\t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>',
|
|
f'{item_indent}\t<dcsset:periodAdditionBegin xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionBegin>',
|
|
f'{item_indent}\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>',
|
|
f'{item_indent}</dcsset:item>',
|
|
]
|
|
frag_xml = "\r\n".join(lines)
|
|
for node in import_fragment(xml_doc, frag_xml):
|
|
insert_before_element(gi_el, node, None, item_indent)
|
|
|
|
desc = "details" if not t["groupBy"] else ", ".join(t["groupBy"])
|
|
print(f'[OK] Group "{t["name"]}" groupItems updated: {desc}')
|
|
|
|
elif operation == "add-dataSetLink":
|
|
for val in values:
|
|
parsed = parse_data_set_link_shorthand(val)
|
|
child_indent = get_child_indent(xml_doc)
|
|
|
|
frag_xml = build_data_set_link_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
last_link = find_last_element(xml_doc, "dataSetLink", SCH_NS)
|
|
if last_link is not None:
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_link:
|
|
found = True
|
|
else:
|
|
ref_node = find_first_element(xml_doc, ["calculatedField", "totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
|
|
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
desc = f"{parsed['source']} > {parsed['dest']} on {parsed['sourceExpr']} = {parsed['destExpr']}"
|
|
if parsed.get("parameter"):
|
|
desc += f" [param {parsed['parameter']}]"
|
|
print(f'[OK] DataSetLink "{desc}" added')
|
|
|
|
elif operation == "add-dataSet":
|
|
child_indent = get_child_indent(xml_doc)
|
|
parsed = parse_data_set_shorthand(value_arg)
|
|
parsed["query"] = resolve_query_value(parsed["query"], query_base_dir)
|
|
|
|
if not parsed["name"]:
|
|
count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS)
|
|
parsed["name"] = f"\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445{count + 1}"
|
|
|
|
existing = find_element_by_child_value(xml_doc, "dataSet", "name", parsed["name"], SCH_NS)
|
|
if existing is not None:
|
|
print(f'[WARN] DataSet "{parsed["name"]}" already exists -- skipped')
|
|
else:
|
|
ds_source_el = find_first_element(xml_doc, ["dataSource"], SCH_NS)
|
|
ds_source_name = "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0414\u0430\u043d\u043d\u044b\u04451"
|
|
if ds_source_el is not None:
|
|
name_el = find_first_element(ds_source_el, ["name"], SCH_NS)
|
|
if name_el is not None:
|
|
ds_source_name = (name_el.text or "").strip()
|
|
parsed["dataSource"] = ds_source_name
|
|
|
|
frag_xml = build_data_set_query_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
last_ds = find_last_element(xml_doc, "dataSet", SCH_NS)
|
|
if last_ds is not None:
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_ds:
|
|
found = True
|
|
else:
|
|
ref_node = find_first_element(xml_doc, ["dataSetLink", "calculatedField", "totalField", "parameter", "template", "groupTemplate", "settingsVariant"], SCH_NS)
|
|
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] DataSet "{parsed["name"]}" added (dataSource={ds_source_name})')
|
|
|
|
elif operation == "add-variant":
|
|
child_indent = get_child_indent(xml_doc)
|
|
for val in values:
|
|
parsed = parse_variant_shorthand(val)
|
|
|
|
is_dup = False
|
|
for ch in xml_doc:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "settingsVariant" and etree.QName(ch.tag).namespace == SCH_NS:
|
|
for gc in ch:
|
|
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SET_NS and gc.text == parsed["name"]:
|
|
is_dup = True
|
|
break
|
|
if is_dup:
|
|
break
|
|
if is_dup:
|
|
print(f'[WARN] Variant "{parsed["name"]}" already exists -- skipped')
|
|
continue
|
|
|
|
frag_xml = build_variant_fragment(parsed, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
last_sv = find_last_element(xml_doc, "settingsVariant", SCH_NS)
|
|
if last_sv is not None:
|
|
ref_node = None
|
|
found = False
|
|
for ch in xml_doc:
|
|
if found and isinstance(ch.tag, str):
|
|
ref_node = ch
|
|
break
|
|
if ch is last_sv:
|
|
found = True
|
|
else:
|
|
ref_node = None
|
|
|
|
for node in nodes:
|
|
insert_before_element(xml_doc, node, ref_node, child_indent)
|
|
|
|
print(f'[OK] Variant "{parsed["name"]}" ["{parsed["presentation"]}"] added')
|
|
|
|
elif operation == "add-conditionalAppearance":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_conditional_appearance_shorthand(val)
|
|
ca_el = ensure_settings_child(settings, "conditionalAppearance", ["outputParameters", "order", "filter", "selection"])
|
|
ca_indent = get_container_child_indent(ca_el)
|
|
frag_xml = build_conditional_appearance_item_fragment(parsed, ca_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
for node in nodes:
|
|
insert_before_element(ca_el, node, None, ca_indent)
|
|
|
|
desc = f"{parsed['param']} = {parsed['value']}"
|
|
if parsed.get("filter"):
|
|
flt = parsed["filter"]
|
|
if isinstance(flt, list):
|
|
desc += f" when OrGroup({len(flt)} conditions)"
|
|
else:
|
|
desc += f" when {flt['field']} {flt['op']}"
|
|
if parsed.get("fields"):
|
|
desc += f" for {', '.join(parsed['fields'])}"
|
|
print(f'[OK] ConditionalAppearance "{desc}" added to variant "{var_name}"')
|
|
|
|
elif operation == "clear-selection":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
selection = find_first_element(settings, ["selection"], SET_NS)
|
|
if selection is not None:
|
|
clear_container_children(selection)
|
|
print(f'[OK] Selection cleared in variant "{var_name}"')
|
|
else:
|
|
print(f'[INFO] No selection section in variant "{var_name}"')
|
|
|
|
elif operation == "clear-order":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
order_el = find_first_element(settings, ["order"], SET_NS)
|
|
if order_el is not None:
|
|
clear_container_children(order_el)
|
|
print(f'[OK] Order cleared in variant "{var_name}"')
|
|
else:
|
|
print(f'[INFO] No order section in variant "{var_name}"')
|
|
|
|
elif operation == "clear-filter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
filter_el = find_first_element(settings, ["filter"], SET_NS)
|
|
if filter_el is not None:
|
|
clear_container_children(filter_el)
|
|
print(f'[OK] Filter cleared in variant "{var_name}"')
|
|
else:
|
|
print(f'[INFO] No filter section in variant "{var_name}"')
|
|
|
|
elif operation == "clear-conditionalAppearance":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
ca_el = find_first_element(settings, ["conditionalAppearance"], SET_NS)
|
|
if ca_el is not None:
|
|
clear_container_children(ca_el)
|
|
print(f'[OK] ConditionalAppearance cleared in variant "{var_name}"')
|
|
else:
|
|
print(f'[INFO] No conditionalAppearance section in variant "{var_name}"')
|
|
|
|
elif operation == "modify-filter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_filter_shorthand(val)
|
|
filter_el = find_first_element(settings, ["filter"], SET_NS)
|
|
if filter_el is None:
|
|
print(f'[WARN] No filter section in variant "{var_name}"')
|
|
continue
|
|
|
|
filter_item = find_element_by_child_value(filter_el, "item", "left", parsed["field"], SET_NS)
|
|
if filter_item is None:
|
|
print(f'[WARN] Filter for "{parsed["field"]}" not found in variant "{var_name}"')
|
|
continue
|
|
|
|
item_indent = get_child_indent(filter_item)
|
|
set_or_create_child_element(filter_item, "comparisonType", SET_NS, parsed["op"], item_indent)
|
|
|
|
if parsed.get("value") is not None:
|
|
vt = parsed.get("valueType", "xs:string")
|
|
set_or_create_child_element_with_attr(filter_item, "right", SET_NS, str(parsed["value"]), vt, item_indent)
|
|
|
|
# Update use (only when explicitly set via @off / @on)
|
|
if parsed.get("use") is False:
|
|
set_or_create_child_element(filter_item, "use", SET_NS, "false", item_indent)
|
|
elif parsed.get("use") is True:
|
|
# @on: remove existing use=false if any
|
|
for ch in filter_item:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SET_NS:
|
|
if (ch.text or "").strip() == "false":
|
|
remove_node_with_whitespace(ch)
|
|
break
|
|
|
|
if parsed.get("viewMode"):
|
|
set_or_create_child_element(filter_item, "viewMode", SET_NS, parsed["viewMode"], item_indent)
|
|
|
|
if parsed.get("userSettingID"):
|
|
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
|
|
set_or_create_child_element(filter_item, "userSettingID", SET_NS, uid, item_indent)
|
|
|
|
print(f'[OK] Filter "{parsed["field"]}" modified in variant "{var_name}"')
|
|
|
|
elif operation == "modify-dataParameter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
parsed = parse_data_param_shorthand(val)
|
|
dp_el = find_first_element(settings, ["dataParameters"], SET_NS)
|
|
if dp_el is None:
|
|
print(f'[WARN] No dataParameters section in variant "{var_name}"')
|
|
continue
|
|
|
|
dp_item = find_element_by_child_value(dp_el, "item", "parameter", parsed["parameter"], COR_NS)
|
|
if dp_item is None:
|
|
print(f'[WARN] DataParameter "{parsed["parameter"]}" not found in variant "{var_name}"')
|
|
continue
|
|
|
|
item_indent = get_child_indent(dp_item)
|
|
|
|
if parsed.get("value") is not None:
|
|
existing_val = None
|
|
for ch in dp_item:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == COR_NS:
|
|
existing_val = ch
|
|
break
|
|
if existing_val is not None:
|
|
remove_node_with_whitespace(existing_val)
|
|
|
|
val_lines = []
|
|
pv = parsed["value"]
|
|
if isinstance(pv, dict) and pv.get("variant"):
|
|
val_lines.append(f'{item_indent}<dcscor:value xsi:type="v8:StandardPeriod">')
|
|
val_lines.append(f'{item_indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(pv["variant"])}</v8:variant>')
|
|
val_lines.append(f"{item_indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
|
val_lines.append(f"{item_indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
|
val_lines.append(f"{item_indent}</dcscor:value>")
|
|
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(pv)):
|
|
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(pv))}</dcscor:value>')
|
|
elif str(pv) in ("true", "false"):
|
|
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:boolean">{esc_xml(str(pv))}</dcscor:value>')
|
|
else:
|
|
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:string">{esc_xml(str(pv))}</dcscor:value>')
|
|
|
|
val_xml = "\r\n".join(val_lines)
|
|
val_nodes = import_fragment(xml_doc, val_xml)
|
|
for node in val_nodes:
|
|
insert_before_element(dp_item, node, None, item_indent)
|
|
|
|
# Update use (only when explicitly set via @off / @on)
|
|
if parsed.get("use") is False:
|
|
set_or_create_child_element(dp_item, "use", COR_NS, "false", item_indent)
|
|
elif parsed.get("use") is True:
|
|
# @on: remove existing use=false if any
|
|
for ch in dp_item:
|
|
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == COR_NS:
|
|
if (ch.text or "").strip() == "false":
|
|
remove_node_with_whitespace(ch)
|
|
break
|
|
|
|
if parsed.get("viewMode"):
|
|
set_or_create_child_element(dp_item, "viewMode", SET_NS, parsed["viewMode"], item_indent)
|
|
|
|
if parsed.get("userSettingID"):
|
|
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
|
|
set_or_create_child_element(dp_item, "userSettingID", SET_NS, uid, item_indent)
|
|
|
|
print(f'[OK] DataParameter "{parsed["parameter"]}" modified in variant "{var_name}"')
|
|
|
|
elif operation == "modify-field":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
for val in values:
|
|
parsed = parse_field_shorthand(val)
|
|
field_name = parsed["dataPath"]
|
|
|
|
field_el = find_element_by_child_value(ds_node, "field", "dataPath", field_name, SCH_NS)
|
|
if field_el is None:
|
|
print(f'[WARN] Field "{field_name}" not found in dataset "{ds_name}"')
|
|
continue
|
|
|
|
existing = read_field_properties(field_el)
|
|
|
|
merged = {
|
|
"dataPath": existing["dataPath"],
|
|
"field": existing["field"],
|
|
"title": parsed["title"] if parsed.get("title") else existing["title"],
|
|
"type": parsed["type"] if parsed.get("type") else existing["type"],
|
|
"roles": parsed["roles"] if parsed.get("roles") else existing["roles"],
|
|
"restrict": parsed["restrict"] if parsed.get("restrict") else existing["restrict"],
|
|
}
|
|
|
|
# Find next element sibling for position
|
|
next_sib = None
|
|
found = False
|
|
for ch in ds_node:
|
|
if found and isinstance(ch.tag, str):
|
|
next_sib = ch
|
|
break
|
|
if ch is field_el:
|
|
found = True
|
|
|
|
child_indent = get_child_indent(ds_node)
|
|
remove_node_with_whitespace(field_el)
|
|
|
|
frag_xml = build_field_fragment(merged, child_indent)
|
|
nodes = import_fragment(xml_doc, frag_xml)
|
|
|
|
for node in nodes:
|
|
insert_before_element(ds_node, node, next_sib, child_indent)
|
|
|
|
print(f'[OK] Field "{field_name}" modified in dataset "{ds_name}"')
|
|
|
|
elif operation == "set-field-role":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
|
|
for val in values:
|
|
s = val.strip()
|
|
|
|
flags = []
|
|
for m in re.finditer(r'@(\w+)', s):
|
|
flags.append(m.group(1))
|
|
s = re.sub(r'\s*@\w+', '', s).strip()
|
|
|
|
kv = []
|
|
for m in re.finditer(r'(\w+)=(\S+)', s):
|
|
kv.append((m.group(1), m.group(2)))
|
|
s = re.sub(r'\s*\w+=\S+', '', s).strip()
|
|
|
|
data_path = s
|
|
if not data_path:
|
|
print(f'[WARN] set-field-role: empty dataPath in "{val}"')
|
|
continue
|
|
|
|
field_el = find_element_by_child_value(ds_node, "field", "dataPath", data_path, SCH_NS)
|
|
if field_el is None:
|
|
print(f'[WARN] Field "{data_path}" not found in dataset "{ds_name}"')
|
|
continue
|
|
|
|
field_indent = get_child_indent(field_el)
|
|
|
|
# Remove existing <role>
|
|
old_role = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) == "role" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
if old_role is not None:
|
|
remove_node_with_whitespace(old_role)
|
|
|
|
# Empty spec — remove only
|
|
if not flags and not kv:
|
|
print(f'[OK] Field "{data_path}" role cleared')
|
|
continue
|
|
|
|
# Build new <role>
|
|
lines = [f"{field_indent}<role>"]
|
|
for flag in flags:
|
|
if flag == "period":
|
|
lines.append(f"{field_indent}\t<dcscom:periodNumber>1</dcscom:periodNumber>")
|
|
lines.append(f"{field_indent}\t<dcscom:periodType>Main</dcscom:periodType>")
|
|
else:
|
|
lines.append(f"{field_indent}\t<dcscom:{flag}>true</dcscom:{flag}>")
|
|
for k, v in kv:
|
|
lines.append(f"{field_indent}\t<dcscom:{k}>{esc_xml(v)}</dcscom:{k}>")
|
|
lines.append(f"{field_indent}</role>")
|
|
frag_xml = "\r\n".join(lines)
|
|
|
|
ref_node = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) in ("valueType", "inputParameters") and etree.QName(ch.tag).namespace == SCH_NS), None)
|
|
for node in import_fragment(xml_doc, frag_xml):
|
|
insert_before_element(field_el, node, ref_node, field_indent)
|
|
|
|
parts = []
|
|
if flags:
|
|
parts.append(" ".join(f"@{f}" for f in flags))
|
|
if kv:
|
|
parts.append(" ".join(f"{k}={v}" for k, v in kv))
|
|
print(f'[OK] Field "{data_path}" role set: {" ".join(parts)}')
|
|
|
|
elif operation == "remove-field":
|
|
ds_node = resolve_data_set()
|
|
ds_name = get_data_set_name(ds_node)
|
|
for val in values:
|
|
field_name = val.strip()
|
|
field_el = find_element_by_child_value(ds_node, "field", "dataPath", field_name, SCH_NS)
|
|
if field_el is None:
|
|
print(f'[WARN] Field "{field_name}" not found in dataset "{ds_name}"')
|
|
continue
|
|
remove_node_with_whitespace(field_el)
|
|
print(f'[OK] Field "{field_name}" removed from dataset "{ds_name}"')
|
|
|
|
try:
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
selection = find_first_element(settings, ["selection"], SET_NS)
|
|
if selection is not None:
|
|
sel_item = find_element_by_child_value(selection, "item", "field", field_name, SET_NS)
|
|
if sel_item is not None:
|
|
remove_node_with_whitespace(sel_item)
|
|
print(f'[OK] Field "{field_name}" removed from selection of variant "{var_name}"')
|
|
except SystemExit:
|
|
pass
|
|
|
|
elif operation == "remove-total":
|
|
for val in values:
|
|
data_path = val.strip()
|
|
total_el = find_element_by_child_value(xml_doc, "totalField", "dataPath", data_path, SCH_NS)
|
|
if total_el is None:
|
|
print(f'[WARN] TotalField "{data_path}" not found')
|
|
continue
|
|
remove_node_with_whitespace(total_el)
|
|
print(f'[OK] TotalField "{data_path}" removed')
|
|
|
|
elif operation == "remove-calculated-field":
|
|
for val in values:
|
|
data_path = val.strip()
|
|
calc_el = find_element_by_child_value(xml_doc, "calculatedField", "dataPath", data_path, SCH_NS)
|
|
if calc_el is None:
|
|
print(f'[WARN] CalculatedField "{data_path}" not found')
|
|
continue
|
|
remove_node_with_whitespace(calc_el)
|
|
print(f'[OK] CalculatedField "{data_path}" removed')
|
|
|
|
try:
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
selection = find_first_element(settings, ["selection"], SET_NS)
|
|
if selection is not None:
|
|
sel_item = find_element_by_child_value(selection, "item", "field", data_path, SET_NS)
|
|
if sel_item is not None:
|
|
remove_node_with_whitespace(sel_item)
|
|
print(f'[OK] Field "{data_path}" removed from selection of variant "{var_name}"')
|
|
except SystemExit:
|
|
pass
|
|
|
|
elif operation == "remove-parameter":
|
|
for val in values:
|
|
param_name = val.strip()
|
|
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
|
|
if param_el is None:
|
|
print(f'[WARN] Parameter "{param_name}" not found')
|
|
continue
|
|
remove_node_with_whitespace(param_el)
|
|
print(f'[OK] Parameter "{param_name}" removed')
|
|
|
|
elif operation == "remove-filter":
|
|
settings = resolve_variant_settings()
|
|
var_name = get_variant_name()
|
|
for val in values:
|
|
field_name = val.strip()
|
|
filter_el = find_first_element(settings, ["filter"], SET_NS)
|
|
if filter_el is None:
|
|
print(f'[WARN] No filter section in variant "{var_name}"')
|
|
continue
|
|
filter_item = find_element_by_child_value(filter_el, "item", "left", field_name, SET_NS)
|
|
if filter_item is None:
|
|
print(f'[WARN] Filter for "{field_name}" not found in variant "{var_name}"')
|
|
continue
|
|
remove_node_with_whitespace(filter_item)
|
|
print(f'[OK] Filter for "{field_name}" removed from variant "{var_name}"')
|
|
|
|
elif operation == "add-drilldown":
|
|
# String-based manipulation — templates use dcsat namespace with inline xmlns
|
|
with open(resolved_path, "r", encoding="utf-8-sig") as f:
|
|
raw_text = f.read()
|
|
nl = "\r\n"
|
|
dcsat_ns_decl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"'
|
|
|
|
# Find all outer <template> blocks by nesting-aware scan
|
|
name_regex = re.compile(r'<template>\s*<name>([^<]+)</name>')
|
|
tpl_starts = [(m.start(), m.group(1)) for m in name_regex.finditer(raw_text)]
|
|
|
|
# For each start, find closing </template> at nesting depth 0
|
|
tpl_blocks = []
|
|
for ts_pos, ts_name in tpl_starts:
|
|
depth = 1
|
|
scan_pos = ts_pos + 10 # skip past opening <template>
|
|
while depth > 0 and scan_pos < len(raw_text):
|
|
next_open = raw_text.find("<template", scan_pos)
|
|
next_close = raw_text.find("</template>", scan_pos)
|
|
if next_close < 0:
|
|
break
|
|
if next_open >= 0 and next_open < next_close:
|
|
depth += 1
|
|
scan_pos = next_open + 10
|
|
else:
|
|
depth -= 1
|
|
if depth == 0:
|
|
end_pos = next_close + len("</template>")
|
|
tpl_blocks.append((ts_name, ts_pos, raw_text[ts_pos:end_pos]))
|
|
scan_pos = next_close + 11
|
|
|
|
if not tpl_blocks:
|
|
print("[WARN] No named templates found in schema")
|
|
|
|
# Collect all insertions as (position, text) — apply in reverse order
|
|
insertions = []
|
|
|
|
expr_regex = re.compile(
|
|
r'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*'
|
|
r'<dcsat:name>([^<]+)</dcsat:name>\s*'
|
|
r'<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>'
|
|
)
|
|
|
|
for tpl_name, tpl_start, tpl_text in tpl_blocks:
|
|
|
|
# Build map: expression → paramName from ExpressionAreaTemplateParameter
|
|
expr_map = {}
|
|
for em in expr_regex.finditer(tpl_text):
|
|
p_name = em.group(1)
|
|
p_expr = em.group(2)
|
|
expr_map[p_expr] = p_name
|
|
|
|
for resource in values:
|
|
drill_name = f"Расшифровка_{resource}"
|
|
|
|
# Idempotency: check if already exists
|
|
if drill_name in tpl_text:
|
|
print(f"[INFO] {drill_name} already exists in {tpl_name} — skipped")
|
|
continue
|
|
|
|
# Find ExpressionAreaTemplateParameter by expression
|
|
param_name = expr_map.get(resource)
|
|
if param_name is None:
|
|
print(f'[WARN] Expression "{resource}" not found in template {tpl_name} — skipped')
|
|
continue
|
|
|
|
cell_count = 0
|
|
|
|
# Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template
|
|
last_param_end_tag = "</parameter>"
|
|
last_param_pos = tpl_text.rfind(last_param_end_tag)
|
|
if last_param_pos >= 0:
|
|
insert_pos = tpl_start + last_param_pos + len(last_param_end_tag)
|
|
# Detect indent from context
|
|
prev_nl = tpl_text.rfind("\n", 0, last_param_pos)
|
|
indent = "\t\t"
|
|
if prev_nl >= 0:
|
|
line_start = prev_nl + 1
|
|
indent_match = re.match(r'^(\s*)', tpl_text[line_start:])
|
|
if indent_match:
|
|
indent = indent_match.group(1)
|
|
details_xml = (
|
|
f'{nl}{indent}<parameter {dcsat_ns_decl} xsi:type="dcsat:DetailsAreaTemplateParameter">'
|
|
f'{nl}{indent}\t<dcsat:name>{drill_name}</dcsat:name>'
|
|
f'{nl}{indent}\t<dcsat:fieldExpression>'
|
|
f'{nl}{indent}\t\t<dcsat:field>ИмяРесурса</dcsat:field>'
|
|
f'{nl}{indent}\t\t<dcsat:expression>"{resource}"</dcsat:expression>'
|
|
f'{nl}{indent}\t</dcsat:fieldExpression>'
|
|
f'{nl}{indent}\t<dcsat:mainAction>DrillDown</dcsat:mainAction>'
|
|
f'{nl}{indent}</parameter>'
|
|
)
|
|
insertions.append((insert_pos, details_xml))
|
|
|
|
# Step 2: Insert appearance binding in cells referencing this parameter
|
|
cell_tag = f'<dcsat:value xsi:type="dcscor:Parameter">{param_name}</dcsat:value>'
|
|
search_start = 0
|
|
while True:
|
|
cell_idx = tpl_text.find(cell_tag, search_start)
|
|
if cell_idx < 0:
|
|
break
|
|
cell_end = tpl_text.find("</dcsat:tableCell>", cell_idx)
|
|
if cell_end < 0:
|
|
break
|
|
app_end = tpl_text.rfind("</dcsat:appearance>", cell_idx, cell_end)
|
|
if app_end < cell_idx:
|
|
search_start = cell_end + 1
|
|
continue
|
|
|
|
# Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance>
|
|
app_prev_nl = tpl_text.rfind("\n", 0, app_end)
|
|
app_indent = "\t\t\t\t\t\t"
|
|
if app_prev_nl >= 0:
|
|
app_line_start = app_prev_nl + 1
|
|
app_indent_match = re.match(r'^(\s*)', tpl_text[app_line_start:])
|
|
if app_indent_match:
|
|
app_indent = app_indent_match.group(1)
|
|
item_indent = app_indent + "\t"
|
|
appearance_xml = (
|
|
f'{item_indent}<dcscor:item>{nl}'
|
|
f'{item_indent}\t<dcscor:parameter>Расшифровка</dcscor:parameter>{nl}'
|
|
f'{item_indent}\t<dcscor:value xsi:type="dcscor:Parameter">{drill_name}</dcscor:value>{nl}'
|
|
f'{item_indent}</dcscor:item>{nl}'
|
|
)
|
|
# Insert after \n (before indent of closing tag), not before the tag itself
|
|
insert_at = (tpl_start + app_prev_nl + 1) if app_prev_nl >= 0 else (tpl_start + app_end)
|
|
insertions.append((insert_at, appearance_xml))
|
|
cell_count += 1
|
|
search_start = cell_end + 1
|
|
|
|
print(f"[OK] {drill_name} \u2192 {tpl_name} (param + {cell_count} cell(s))")
|
|
|
|
# Apply insertions in reverse order to preserve offsets.
|
|
# For same position: reverse insertion order so first resource ends up first in file.
|
|
insertions = [(pos, text, seq) for seq, (pos, text) in enumerate(insertions)]
|
|
insertions.sort(key=lambda x: (x[0], x[2]), reverse=True)
|
|
for pos, text, _seq in insertions:
|
|
raw_text = raw_text[:pos] + text + raw_text[pos:]
|
|
|
|
# Write directly — skip lxml save
|
|
with open(resolved_path, "wb") as f:
|
|
f.write(b'\xef\xbb\xbf')
|
|
f.write(raw_text.encode("utf-8"))
|
|
print(f"[OK] Saved {resolved_path}")
|
|
sys.exit(0)
|
|
|
|
# ── 9. Save ─────────────────────────────────────────────────
|
|
|
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
if not xml_bytes.endswith(b"\n"):
|
|
xml_bytes += b"\n"
|
|
with open(resolved_path, "wb") as f:
|
|
f.write(b'\xef\xbb\xbf')
|
|
f.write(xml_bytes)
|
|
|
|
print(f"[OK] Saved {resolved_path}")
|