from pathlib import Path
from one_c_normalizer import (
COMMON_BRANCH_CHILDREN,
METADATA_TYPE_BY_BRANCH,
METADATA_TYPE_BY_CODE,
build_normalized_project,
normalize_bsl_source,
normalize_one_c_project,
normalize_source_path,
parse_one_c_xml_file,
)
def test_normalize_bsl_source_removes_bom_and_normalizes_newlines():
assert normalize_bsl_source("\ufeffПроцедура X()\r\n A = 1; \r\n\r\n") == (
"Процедура X()\n A = 1;\n"
)
def test_normalize_source_path_uses_forward_slashes():
assert normalize_source_path("src\\module.bsl") == "src/module.bsl"
def test_parse_one_c_xml_file_extracts_ui_objects(tmp_path: Path):
xml = tmp_path / "form.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
assert [item.object_kind for item in objects] == ["FORM", "COMMAND", "ATTRIBUTE"]
assert objects[0].qualified_name == "Документ.Заказ.ФормаДокумента"
def test_parse_one_c_xml_file_extracts_metadata_objects(tmp_path: Path):
xml = tmp_path / "metadata.xml"
xml.write_text(
"""
ЗаказПокупателя
Документ.ЗаказПокупателя
ОстаткиТоваров
РегистрНакопления.ОстаткиТоваров
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
assert [item.object_kind for item in objects] == [
"CATALOG",
"ATTRIBUTE",
"DOCUMENT",
"TABULAR_SECTION",
"FORM",
"ACCUMULATION_REGISTER",
]
assert objects[2].qualified_name == "Документ.ЗаказПокупателя"
assert objects[5].name == "ОстаткиТоваров"
def test_normalize_one_c_project_preserves_configuration_root_metadata(tmp_path: Path):
xml = tmp_path / "configuration.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="configuration-root")
assert normalized.configuration.name == "УправлениеТорговлей"
assert normalized.configuration.metadata["synonym"] == "Управление торговлей"
assert normalized.configuration.metadata["platformVersion"] == "8.3.24"
assert normalized.configuration.metadata["compatibilityMode"] == "8.3.20"
assert normalized.configuration.metadata["source_path"].endswith("configuration.xml")
assert [group.name for group in normalized.configuration.groups] == ["Справочники"]
def test_parse_edt_mdo_derives_qualified_names(tmp_path: Path):
mdo = tmp_path / "Товары.mdo"
mdo.write_text(
"""
Товары
Артикул
Цены
ВидЦены
ФормаЭлемента
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(mdo)
by_name = {item.name: item for item in objects}
assert by_name["Товары"].qualified_name == "Справочник.Товары"
assert by_name["Артикул"].qualified_name == "Справочник.Товары.Артикул"
assert by_name["Цены"].qualified_name == "Справочник.Товары.Цены"
assert by_name["ВидЦены"].qualified_name == "Справочник.Товары.Цены.ВидЦены"
assert by_name["ФормаЭлемента"].qualified_name == "Справочник.Товары.ФормаЭлемента"
def test_normalize_http_service_keeps_url_templates_and_methods(tmp_path: Path):
mdo = tmp_path / "ПубличныйAPI.mdo"
mdo.write_text(
"""
ПубличныйAPI
api
Orders
/orders/{id}
GET
ПолучитьЗаказ
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="http-service")
http_group = next(group for group in normalized.configuration.groups if group.name == "HTTP-сервисы")
service = http_group.objects[0]
assert service.qualified_name == "HTTPСервис.ПубличныйAPI"
assert service.metadata["rootURL"] == "api"
assert service.url_templates[0].name == "Orders"
assert service.url_templates[0].attributes["template"] == "/orders/{id}"
assert service.url_templates[0].children[0].kind == "METHOD"
assert service.url_templates[0].children[0].name == "ПолучитьЗаказ"
def test_normalize_edt_project_keeps_tabular_section_columns_nested(tmp_path: Path):
mdo = tmp_path / "ЗаказПокупателя.mdo"
mdo.write_text(
"""
ЗаказПокупателя
Товары
Номенклатура
Количество
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-tabular")
documents = next(group for group in normalized.configuration.groups if group.name == "Документы")
document = documents.objects[0]
assert document.attributes == []
assert document.tabular_sections[0].qualified_name == "Документ.ЗаказПокупателя.Товары"
assert [child.name for child in document.tabular_sections[0].children] == ["Номенклатура", "Количество"]
assert [child.qualified_name for child in document.tabular_sections[0].children] == [
"Документ.ЗаказПокупателя.Товары.Номенклатура",
"Документ.ЗаказПокупателя.Товары.Количество",
]
def test_normalize_edt_project_preserves_source_path_and_common_object_descriptions(tmp_path: Path):
common_form = tmp_path / "ФормаПодбора.mdo"
common_form.write_text(
"""
ФормаПодбора
Форма подбора
Используется в подборе товаров
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-common")
groups = {group.name: group for group in normalized.configuration.groups}
common_forms = groups["Общие формы"].objects
assert common_forms[0].qualified_name == "ОбщаяФорма.ФормаПодбора"
assert common_forms[0].source_path.endswith("ФормаПодбора.mdo")
assert common_forms[0].metadata["synonym"] == "Форма подбора"
assert common_forms[0].metadata["comment"] == "Используется в подборе товаров"
def test_normalize_edt_project_knows_full_common_metadata_catalog(tmp_path: Path):
for file_name, class_name, object_name in [
("Продажи.mdo", "Subsystem", "Продажи"),
("Менеджер.mdo", "Role", "Менеджер"),
("ПроведениеДокументов.mdo", "Sequence", "ПроведениеДокументов"),
("ОбщийНумератор.mdo", "DocumentNumerator", "ОбщийНумератор"),
("ДоступностьСкидок.mdo", "FunctionalOption", "ДоступностьСкидок"),
("ФормаПодбора.mdo", "CommonForm", "ФормаПодбора"),
("ПубличныйAPI.mdo", "HTTPService", "ПубличныйAPI"),
]:
(tmp_path / file_name).write_text(
f"""
{object_name}
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-full-common")
objects = {
item.qualified_name
for group in normalized.configuration.groups
for item in group.objects
}
assert {
"Подсистема.Продажи",
"Роль.Менеджер",
"Последовательность.ПроведениеДокументов",
"НумераторДокументов.ОбщийНумератор",
"ФункциональнаяОпция.ДоступностьСкидок",
"ОбщаяФорма.ФормаПодбора",
"HTTPСервис.ПубличныйAPI",
}.issubset(objects)
def test_normalize_project_loads_access_profiles_groups_and_users(tmp_path: Path):
xml = tmp_path / "access.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="access-data")
assert normalized.access.profiles[0].name == "МенеджерПродаж"
assert normalized.access.profiles[0].roles[0].role_qualified_name == "Роль.ЧтениеПродаж"
assert normalized.access.groups[0].profile == "МенеджерПродаж"
assert normalized.access.groups[0].users == ["ivanov"]
assert normalized.access.users[0].full_name == "Иванов Иван"
def test_normalize_edt_project_preserves_localized_descriptions(tmp_path: Path):
catalog = tmp_path / "Контрагенты.mdo"
catalog.write_text(
"""
Контрагенты
ru
Контрагенты
ru
Описание справочника контрагентов
ИНН
ru
ИНН
ru
Идентификационный номер налогоплательщика
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-localized")
catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники")
metadata_object = catalogs.objects[0]
attribute = metadata_object.attributes[0]
assert metadata_object.metadata["synonym"] == "Контрагенты"
assert metadata_object.metadata["synonym_localized"] == {"ru": "Контрагенты"}
assert metadata_object.metadata["comment"] == "Описание справочника контрагентов"
assert metadata_object.metadata["comment_localized"] == {"ru": "Описание справочника контрагентов"}
assert attribute.attributes["synonym"] == "ИНН"
assert attribute.attributes["comment"] == "Идентификационный номер налогоплательщика"
def test_normalize_edt_project_attaches_bsl_modules_to_metadata_objects(tmp_path: Path):
catalog_dir = tmp_path / "Catalogs" / "Контрагенты"
module_dir = catalog_dir / "Ext"
module_dir.mkdir(parents=True)
(catalog_dir / "Контрагенты.mdo").write_text(
"""
Контрагенты
""",
encoding="utf-8",
)
(module_dir / "ObjectModule.bsl").write_text(
"""
Процедура ПроверитьКонтрагента() Экспорт
КонецПроцедуры
""",
encoding="utf-8",
)
(module_dir / "ManagerModule.bsl").write_text(
"""
Процедура Создать() Экспорт
КонецПроцедуры
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-modules")
catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники")
catalog = catalogs.objects[0]
assert [module.module_kind for module in catalog.modules] == ["MANAGER_MODULE", "OBJECT_MODULE"]
assert all(module.source_path.endswith(".bsl") for module in catalog.modules)
assert all(module.attributes["source_hash"] for module in catalog.modules)
def test_normalize_edt_project_attaches_form_modules_to_owner_with_form_name(tmp_path: Path):
catalog_dir = tmp_path / "Catalogs" / "Контрагенты"
form_dir = catalog_dir / "Forms" / "ФормаЭлемента" / "Ext"
form_dir.mkdir(parents=True)
(catalog_dir / "Контрагенты.mdo").write_text(
"""
Контрагенты
ФормаЭлемента
""",
encoding="utf-8",
)
(form_dir / "Module.bsl").write_text(
"""
Процедура ПриОткрытии()
КонецПроцедуры
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-form-module")
catalog = next(group for group in normalized.configuration.groups if group.name == "Справочники").objects[0]
assert catalog.modules[0].module_kind == "FORM_MODULE"
assert catalog.modules[0].attributes["form_name"] == "ФормаЭлемента"
assert catalog.modules[0].qualified_name == "Справочник.Контрагенты.Форма.ФормаЭлемента.Модуль"
def test_normalize_edt_project_attaches_common_form_modules(tmp_path: Path):
common_form_dir = tmp_path / "CommonForms" / "ФормаПодбора"
module_dir = common_form_dir / "Ext"
module_dir.mkdir(parents=True)
(common_form_dir / "ФормаПодбора.mdo").write_text(
"""
ФормаПодбора
""",
encoding="utf-8",
)
(module_dir / "Module.bsl").write_text(
"""
Процедура ПриСозданииНаСервере() Экспорт
КонецПроцедуры
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-common-form-module")
common_forms = next(group for group in normalized.configuration.groups if group.name == "Общие формы")
common_form = common_forms.objects[0]
assert common_form.qualified_name == "ОбщаяФорма.ФормаПодбора"
assert len(common_form.modules) == 1
assert common_form.modules[0].module_kind == "MODULE"
assert common_form.modules[0].source_path.endswith("Module.bsl")
def test_parse_register_dimensions_and_resources_preserves_attribute_roles(tmp_path: Path):
mdo = tmp_path / "ОстаткиТоваров.mdo"
mdo.write_text(
"""
ОстаткиТоваров
Номенклатура
Количество
Комментарий
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(mdo)
by_name = {item.name: item for item in objects}
assert by_name["ОстаткиТоваров"].object_kind == "ACCUMULATION_REGISTER"
assert by_name["ОстаткиТоваров"].qualified_name == "РегистрНакопления.ОстаткиТоваров"
assert by_name["Номенклатура"].attributes["attribute_role"] == "DIMENSION"
assert by_name["Количество"].attributes["attribute_role"] == "RESOURCE"
assert by_name["Комментарий"].attributes["attribute_role"] == "REQUISITE"
def test_normalize_edt_project_preserves_specific_register_kinds(tmp_path: Path):
for file_name, tag, name in (
("Цены.mdo", "InformationRegister", "Цены"),
("ОстаткиТоваров.mdo", "AccumulationRegister", "ОстаткиТоваров"),
("Хозрасчетный.mdo", "AccountingRegister", "Хозрасчетный"),
("Начисления.mdo", "CalculationRegister", "Начисления"),
):
(tmp_path / file_name).write_text(
f"""
{name}
""",
encoding="utf-8",
)
normalized = normalize_one_c_project(tmp_path, project_id="edt-register-kinds")
groups = {group.name: group for group in normalized.configuration.groups}
assert groups["Регистры сведений"].objects[0].object_kind == "INFORMATION_REGISTER"
assert groups["Регистры накопления"].objects[0].qualified_name == "РегистрНакопления.ОстаткиТоваров"
assert groups["Регистры бухгалтерии"].objects[0].object_kind == "ACCOUNTING_REGISTER"
assert groups["Регистры расчета"].objects[0].object_kind == "CALCULATION_REGISTER"
def test_parse_one_c_xml_file_extracts_role_rights(tmp_path: Path):
xml = tmp_path / "roles.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
assert [item.object_kind for item in objects] == ["ROLE", "RIGHT"]
assert objects[1].name == "Документ.ЗаказПокупателя"
assert objects[1].attributes["role"] == "Роль.Менеджер"
assert objects[1].attributes["post"] == "true"
def test_normalized_project_attaches_role_rights(tmp_path: Path):
xml = tmp_path / "roles.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
normalized = build_normalized_project(parse_one_c_xml_file(xml), project_id="roles", source_path=str(tmp_path))
roles_group = next(group for group in normalized.configuration.groups if group.name == "Роли")
role = roles_group.objects[0]
assert role.name == "Менеджер"
assert role.rights[0].target == "Документ.ЗаказПокупателя"
assert role.rights[0].permissions["write"] == "true"
def test_parse_one_c_xml_file_extracts_child_element_command_action_and_role_right(tmp_path: Path):
xml = tmp_path / "metadata.xml"
xml.write_text(
"""
ЗаказПокупателя
Документ.ЗаказПокупателя
Менеджер
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
by_kind = {item.object_kind: item for item in objects}
command = next(item for item in objects if item.object_kind == "COMMAND")
right = next(item for item in objects if item.object_kind == "RIGHT")
assert by_kind["FORM"].qualified_name == "Документ.ЗаказПокупателя.ФормаДокумента"
assert command.attributes["Action"] == "ПровестиКоманда"
assert right.name == "Документ.ЗаказПокупателя"
assert right.attributes["role"] == "Роль.Менеджер"
def test_normalized_project_groups_extended_1c_objects(tmp_path: Path):
xml = tmp_path / "extended.xml"
xml.write_text(
"""
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
normalized = build_normalized_project(objects, project_id="extended", source_path=str(tmp_path))
groups = {group.name: group for group in normalized.configuration.groups}
document = next(item for item in groups["Документы"].objects if item.name == "ЗаказПокупателя")
assert "Подсистемы" in groups
assert "HTTP-сервисы" in groups
assert "XDTO-пакеты" in groups
assert document.layouts[0].name == "ПечатнаяФорма"
assert document.movements[0].name == "Остатки"
assert normalized.configuration.extensions[0].name == "CRM"
def test_normalize_one_c_project_skips_unreadable_or_invalid_metadata(tmp_path: Path):
(tmp_path / "valid.xml").write_text(
"""
""",
encoding="utf-8",
)
(tmp_path / "broken.xml").write_text("", encoding="utf-8")
normalized = normalize_one_c_project(tmp_path, project_id="skip-invalid")
catalogs = next(group for group in normalized.configuration.groups if group.name == "Справочники")
assert catalogs.objects[0].qualified_name == "Справочник.Контрагенты"
def test_configuration_extension_has_own_metadata_tree_structure(tmp_path: Path):
xml = tmp_path / "extension.mdo"
xml.write_text(
"""
CRM
1.0
КонтрагентыCRM
ВнешнийКод
CRMСервер
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
normalized = build_normalized_project(objects, project_id="extension-structure", source_path=str(tmp_path))
assert normalized.configuration.groups == []
extension = normalized.configuration.extensions[0]
assert extension.name == "CRM"
assert extension.version == "1.0"
extension_groups = {group.name: group for group in extension.groups}
assert extension_groups["Справочники"].objects[0].name == "КонтрагентыCRM"
assert extension_groups["Справочники"].objects[0].attributes[0].name == "ВнешнийКод"
assert extension_groups["Общие модули"].objects[0].name == "CRMСервер"
def test_report_metadata_parts_include_dcs_variants_settings_and_tabular_documents(tmp_path: Path):
xml = tmp_path / "report.mdo"
xml.write_text(
"""
АнализПродаж
Период
Показатели
ФормаОтчета
ПечатнаяФорма
ТабличныйДокумент
ОсновнаяСхемаКомпоновкиДанных
Основной
НастройкиПоУмолчанию
""",
encoding="utf-8",
)
objects = parse_one_c_xml_file(xml)
normalized = build_normalized_project(objects, project_id="report-parts", source_path=str(tmp_path))
reports = next(group for group in normalized.configuration.groups if group.name == "Отчеты")
report = reports.objects[0]
assert report.qualified_name == "Отчет.АнализПродаж"
assert report.attributes[0].name == "Период"
assert report.tabular_sections[0].name == "Показатели"
assert report.forms[0].name == "ФормаОтчета"
assert report.layouts[0].name == "ПечатнаяФорма"
assert report.tabular_documents[0].name == "ТабличныйДокумент"
assert report.data_composition_schemas[0].name == "ОсновнаяСхемаКомпоновкиДанных"
assert report.report_variants[0].name == "Основной"
assert report.report_settings[0].name == "НастройкиПоУмолчанию"
def test_metadata_catalog_describes_core_1c_tree_branches():
assert "Общие модули" in COMMON_BRANCH_CHILDREN
assert "HTTP-сервисы" in COMMON_BRANCH_CHILDREN
document = METADATA_TYPE_BY_CODE["DOCUMENT"]
assert document.tree_branch == "Документы"
assert "Реквизиты" in document.child_groups
assert "Табличные части" in document.child_groups
assert "Движения" in document.child_groups
assert document.module_kinds == ("Модуль объекта", "Модуль менеджера")
register = METADATA_TYPE_BY_CODE["ACCUMULATION_REGISTER"]
assert register.tree_branch == "Регистры накопления"
assert "Измерения" in register.child_groups
assert "Ресурсы" in register.child_groups
common_form = METADATA_TYPE_BY_CODE["COMMON_FORM"]
assert common_form.tree_branch == "Общие формы"
assert "Модуль формы" in common_form.child_groups
report = METADATA_TYPE_BY_CODE["REPORT"]
assert "СКД" in report.child_groups
assert "Табличные документы" in report.child_groups
assert "Варианты отчета" in report.child_groups
assert "Настройки" in report.child_groups
assert METADATA_TYPE_BY_BRANCH["XDTO-пакеты"].code == "XDTO_PACKAGE"
assert METADATA_TYPE_BY_BRANCH["Сервисы интеграции"].code == "INTEGRATION_SERVICE"