1-74 rule
This commit is contained in:
parent
fe37cf3660
commit
25f96438b1
@ -204,7 +204,7 @@ def _build_structured_group_rules(
|
||||
one_line_names: list[str] = []
|
||||
component_names: list[str] = []
|
||||
|
||||
# Left-side "one line" can be immediately before structured column.
|
||||
# Поле "в одной строке" может располагаться слева от структурного.
|
||||
left = by_index.get(index - 1)
|
||||
if left:
|
||||
left_name = str(left.get("column_name", "")).strip()
|
||||
@ -290,6 +290,7 @@ REPORT_MAPPING_RULES: tuple[MappingRule, ...] = _build_rules(_MAPPING_DATA)
|
||||
STRUCTURED_GROUP_RULES: tuple[StructuredGroupRule, ...] = _build_structured_group_rules(
|
||||
_MAPPING_DATA
|
||||
)
|
||||
_ACCOUNT_PLACEHOLDER = "00000000000000000000"
|
||||
|
||||
|
||||
def build_fixed_row(
|
||||
@ -332,6 +333,7 @@ def build_fixed_row(
|
||||
source_payload, suffix_index, rule.candidate_keys
|
||||
)
|
||||
_apply_structured_group_rules(row)
|
||||
_apply_conditional_rules_1_74(row)
|
||||
for column in FIXED_REPORT_COLUMNS:
|
||||
row.setdefault(column, "")
|
||||
return row
|
||||
@ -414,3 +416,118 @@ def _pick_structured_value(
|
||||
return ""
|
||||
separator = " " if join_with_space else ", "
|
||||
return separator.join(values)
|
||||
|
||||
|
||||
def _column(index: int) -> str:
|
||||
return FIXED_REPORT_COLUMNS[index - 1]
|
||||
|
||||
|
||||
def _row_value(row: dict[str, str], index: int) -> str:
|
||||
return str(row.get(_column(index), "")).strip()
|
||||
|
||||
|
||||
def _set_row_value(row: dict[str, str], index: int, value: str) -> None:
|
||||
row[_column(index)] = value
|
||||
|
||||
|
||||
def _extract_numeric_codes(value: str) -> set[str]:
|
||||
return {match for match in re.findall(r"\d+", value)}
|
||||
|
||||
|
||||
def _operation_and_extra_codes(row: dict[str, str]) -> set[str]:
|
||||
op_code = _row_value(row, 19)
|
||||
extra_codes = _row_value(row, 20)
|
||||
return _extract_numeric_codes(op_code) | _extract_numeric_codes(extra_codes)
|
||||
|
||||
|
||||
def _apply_conditional_rules_1_74(row: dict[str, str]) -> None:
|
||||
codes = _operation_and_extra_codes(row)
|
||||
op_type_code = _row_value(row, 14)
|
||||
transfer_type = _row_value(row, 35)
|
||||
operator_type = _row_value(row, 37)
|
||||
|
||||
# 10. Допустимые значения: 0..5, иначе очищаем.
|
||||
if _row_value(row, 10) not in {"0", "1", "2", "3", "4", "5"}:
|
||||
_set_row_value(row, 10, "")
|
||||
|
||||
# 11. Заполняется только для приостановлений и валидных кодов основания.
|
||||
if _row_value(row, 11) not in {"1", "2", "3"}:
|
||||
_set_row_value(row, 11, "")
|
||||
|
||||
# 15. Заполняется только для допустимых значений признака ЭСП.
|
||||
if _row_value(row, 15) not in {"1", "2", "3", "4"}:
|
||||
_set_row_value(row, 15, "")
|
||||
|
||||
# 21. Коды необычной операции заполняются только при признаке 6001.
|
||||
if "6001" not in codes:
|
||||
_set_row_value(row, 21, "")
|
||||
|
||||
# 22-24. Для операций с цифровыми правами (код 8) не заполняем.
|
||||
if op_type_code == "8":
|
||||
_set_row_value(row, 22, "")
|
||||
_set_row_value(row, 23, "")
|
||||
_set_row_value(row, 24, "")
|
||||
|
||||
# 28. Идентификатор подозрительной деятельности только для 6001.
|
||||
if "6001" not in codes:
|
||||
_set_row_value(row, 28, "")
|
||||
|
||||
# 30. Наименование драгметалла только для кода C99.
|
||||
if _row_value(row, 29).upper() != "C99":
|
||||
_set_row_value(row, 30, "")
|
||||
|
||||
# 31. Предмет операции только для кодов 5020..5023.
|
||||
if not {"5020", "5021", "5022", "5023"} & codes:
|
||||
_set_row_value(row, 31, "")
|
||||
|
||||
# 36. Код территории заполняется только для признака 5016.
|
||||
if "5016" not in codes:
|
||||
_set_row_value(row, 36, "")
|
||||
|
||||
# 38. Счет плательщика обязателен для видов перевода 1/2/3/10 с заглушкой.
|
||||
if transfer_type in {"1", "2", "3", "10"}:
|
||||
if not _row_value(row, 38):
|
||||
_set_row_value(row, 38, _ACCOUNT_PLACEHOLDER)
|
||||
elif transfer_type in {"12", "13", "14"}:
|
||||
pass
|
||||
else:
|
||||
_set_row_value(row, 38, "")
|
||||
|
||||
# 39 / 47. Идентификаторы ЭСП отсутствуют для признака операции 9.
|
||||
if op_type_code == "9":
|
||||
_set_row_value(row, 39, "")
|
||||
_set_row_value(row, 47, "")
|
||||
|
||||
# 40. Банк плательщика заполняется только при типе оператора 2 или 4.
|
||||
if operator_type not in {"2", "4"}:
|
||||
_set_row_value(row, 40, "")
|
||||
|
||||
# 42/43. Реквизиты банка получателя только при типе оператора 1 или 4.
|
||||
if operator_type not in {"1", "4"}:
|
||||
_set_row_value(row, 42, "")
|
||||
_set_row_value(row, 43, "")
|
||||
|
||||
# 44. Корсчет банка плательщика с заглушкой там, где это применимо.
|
||||
if operator_type in {"3", "5"} or transfer_type in {"12", "13"}:
|
||||
_set_row_value(row, 44, "")
|
||||
elif not _row_value(row, 44):
|
||||
_set_row_value(row, 44, _ACCOUNT_PLACEHOLDER)
|
||||
|
||||
# 45. Корсчет банка получателя с заглушкой там, где это применимо.
|
||||
if operator_type in {"3", "5"} or transfer_type in {"10", "11"}:
|
||||
_set_row_value(row, 45, "")
|
||||
elif not _row_value(row, 45):
|
||||
_set_row_value(row, 45, _ACCOUNT_PLACEHOLDER)
|
||||
|
||||
# 46. Счет получателя с заглушкой для обязательных видов перевода.
|
||||
if transfer_type in {"1", "4", "7", "12"}:
|
||||
if not _row_value(row, 46):
|
||||
_set_row_value(row, 46, _ACCOUNT_PLACEHOLDER)
|
||||
elif transfer_type in {"10", "11", "14"}:
|
||||
pass
|
||||
else:
|
||||
_set_row_value(row, 46, "")
|
||||
|
||||
# 62. Статус перевода заполняется только для видов 2/5/8.
|
||||
if transfer_type not in {"2", "5", "8"}:
|
||||
_set_row_value(row, 62, "")
|
||||
|
||||
@ -27,61 +27,63 @@ class GroupHeader:
|
||||
title: str
|
||||
|
||||
|
||||
# Grouped upper header from the template report for readability.
|
||||
# Верхняя группирующая шапка согласно согласованным логическим категориям.
|
||||
TOP_GROUP_HEADERS: tuple[GroupHeader, ...] = (
|
||||
GroupHeader(start_col=1, end_col=1, title="Служебные поля"),
|
||||
GroupHeader(start_col=2, end_col=6, title="Реквизиты сообщения/КО/филиала"),
|
||||
GroupHeader(
|
||||
start_col=1,
|
||||
end_col=4,
|
||||
title="Информация о КО, передающей сведения в уполномоченный орган",
|
||||
),
|
||||
GroupHeader(start_col=5, end_col=8, title="Сведения об уполномоченном лице"),
|
||||
GroupHeader(
|
||||
start_col=11,
|
||||
start_col=7,
|
||||
end_col=13,
|
||||
title="Информация о филиале КО, передающем сведения в уполномоченный орган",
|
||||
title="Идентификация записи и контрольные признаки",
|
||||
),
|
||||
GroupHeader(start_col=14, end_col=36, title="Информация об операции"),
|
||||
GroupHeader(start_col=14, end_col=36, title="Параметры операции"),
|
||||
GroupHeader(
|
||||
start_col=37,
|
||||
end_col=48,
|
||||
title="Сведения о переводах денежных средств, в том числе электронных денежных средств",
|
||||
title="Перевод ДС и реквизиты расчетов",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=49,
|
||||
end_col=53,
|
||||
title="Сведения о месте приема наличных денежных средств",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=55,
|
||||
end_col=59,
|
||||
title="Сведения о месте выдачи наличных денежных средств",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=60,
|
||||
end_col=64,
|
||||
title="Сведения о месте авторизации ЭСП",
|
||||
end_col=66,
|
||||
title="Места приема/выдачи наличных и авторизация ЭСП",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=67,
|
||||
end_col=69,
|
||||
title=(
|
||||
"Сведения о внесении наличных денежных средств на свой банковский счет "
|
||||
"или о получении наличных денежных средств со своего банковского счета "
|
||||
"у одного оператора по переводу денежных средств"
|
||||
),
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=70,
|
||||
end_col=74,
|
||||
title="Сведения о месте приема (выдачи) наличных денежных средств",
|
||||
end_col=75,
|
||||
title="Внесение/выдача у одного оператора (место операции)",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=76,
|
||||
end_col=80,
|
||||
title="Сведения о месте совершения операции с денежными средствами в наличной форме",
|
||||
end_col=86,
|
||||
title="Наличная форма и основание операции",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=87,
|
||||
end_col=121,
|
||||
title="Участник операции (базовые сведения)",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=122,
|
||||
end_col=154,
|
||||
title="Участник операции (идентификация, документы, адреса)",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=155,
|
||||
end_col=193,
|
||||
title="ЕИО/Бенефициар (идентификация, документы, адреса)",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=194,
|
||||
end_col=204,
|
||||
title="Сведения о ценных бумагах (ЦП)",
|
||||
),
|
||||
GroupHeader(
|
||||
start_col=205,
|
||||
end_col=205,
|
||||
title=(
|
||||
"Колонки вне согласованной группировки (если количество полей расширится)"
|
||||
),
|
||||
),
|
||||
GroupHeader(start_col=87, end_col=104, title="Сведения об участниках операции"),
|
||||
GroupHeader(start_col=105, end_col=118, title="Сведения ЕИО"),
|
||||
)
|
||||
|
||||
|
||||
@ -120,7 +122,7 @@ class StreamingReportWriter:
|
||||
self._column_formats = [self._resolve_format(column) for column in self.columns]
|
||||
self._build_group_header_row()
|
||||
self._build_header_row()
|
||||
# Freeze both header rows.
|
||||
# Фиксируем обе строки шапки.
|
||||
self.sheet.freeze_panes(2, 0)
|
||||
self.sheet.autofilter(1, 0, 1, len(self.columns) - 1)
|
||||
self._next_data_row = 2
|
||||
@ -161,6 +163,9 @@ class StreamingReportWriter:
|
||||
continue
|
||||
first_col = header.start_col - 1
|
||||
last_col = min(header.end_col, len(self.columns)) - 1
|
||||
if first_col == last_col:
|
||||
self.sheet.write(0, first_col, header.title, self._group_format)
|
||||
continue
|
||||
self.sheet.merge_range(
|
||||
0,
|
||||
first_col,
|
||||
|
||||
@ -294,3 +294,86 @@ def test_structured_column_uses_non_one_line_candidate_when_mixed_tags() -> None
|
||||
]
|
||||
== "г. Москва, ул. Пушкина, д. 1"
|
||||
)
|
||||
|
||||
|
||||
def test_digital_rights_operation_clears_currency_amount_fields() -> None:
|
||||
row = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
record_id="R1",
|
||||
operation_index=1,
|
||||
operation_fields={
|
||||
"КодПризнОперации": "8",
|
||||
"КодВал": "643",
|
||||
"СумОперации": "100.00",
|
||||
"СумРуб": "100.00",
|
||||
},
|
||||
participant_fields={},
|
||||
)
|
||||
assert row["Валюта"] == ""
|
||||
assert row["Сумма в валюте проведения"] == ""
|
||||
assert row["Сумма в рубл. эквиваленте"] == ""
|
||||
|
||||
|
||||
def test_abnormal_operation_codes_only_for_6001() -> None:
|
||||
row_without_6001 = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
record_id="R1",
|
||||
operation_index=1,
|
||||
operation_fields={
|
||||
"КодОперации": "5007",
|
||||
"ПризнНеобОперации": "1101",
|
||||
},
|
||||
participant_fields={},
|
||||
)
|
||||
assert row_without_6001["Коды призн. необычной операции"] == ""
|
||||
|
||||
row_with_6001 = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
record_id="R1",
|
||||
operation_index=1,
|
||||
operation_fields={
|
||||
"КодОперации": "6001",
|
||||
"ПризнНеобОперации": "1101",
|
||||
},
|
||||
participant_fields={},
|
||||
)
|
||||
assert row_with_6001["Коды призн. необычной операции"] == "1101"
|
||||
|
||||
|
||||
def test_transfer_accounts_get_placeholder_when_required() -> None:
|
||||
row = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
record_id="R1",
|
||||
operation_index=1,
|
||||
operation_fields={
|
||||
"ВидПереводДС": "1",
|
||||
"ТипОператорДС": "4",
|
||||
},
|
||||
participant_fields={},
|
||||
)
|
||||
assert row["Номер счета плательщика"] == "00000000000000000000"
|
||||
assert row["Номер счета банка плательщика"] == "00000000000000000000"
|
||||
assert row["Номер счета банка получателя"] == "00000000000000000000"
|
||||
assert row["Номер счета получателя"] == "00000000000000000000"
|
||||
|
||||
|
||||
def test_operator_type_conditions_clear_irrelevant_bank_fields() -> None:
|
||||
row = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
record_id="R1",
|
||||
operation_index=1,
|
||||
operation_fields={
|
||||
"ТипОператорДС": "3",
|
||||
"НаимКО": "Банк плательщика",
|
||||
"СведБанкПолучатель.НаимКО": "Банк получателя",
|
||||
"СведБанкПолучатель.БИККО": "044525225",
|
||||
"СчетБанкПлательщик": "30101810000000000001",
|
||||
"СчетБанкПолучатель": "30101810000000000002",
|
||||
},
|
||||
participant_fields={},
|
||||
)
|
||||
assert row["Наименование банка плательщика"] == ""
|
||||
assert row["Наименование банка получателя"] == ""
|
||||
assert row["БИК банка получателя"] == ""
|
||||
assert row["Номер счета банка плательщика"] == ""
|
||||
assert row["Номер счета банка получателя"] == ""
|
||||
|
||||
@ -62,15 +62,15 @@ def test_write_report_contains_top_group_header_row(tmp_path: Path) -> None:
|
||||
|
||||
workbook = load_workbook(destination)
|
||||
sheet = workbook["Отчет"]
|
||||
assert sheet.cell(row=1, column=1).value == (
|
||||
"Информация о КО, передающей сведения в уполномоченный орган"
|
||||
)
|
||||
assert sheet.cell(row=1, column=1).value == "Служебные поля"
|
||||
merged_ranges = {str(rng) for rng in sheet.merged_cells.ranges}
|
||||
expected_ranges = set()
|
||||
for header in TOP_GROUP_HEADERS:
|
||||
if header.start_col > len(FIXED_REPORT_COLUMNS):
|
||||
continue
|
||||
end_col = min(header.end_col, len(FIXED_REPORT_COLUMNS))
|
||||
if header.start_col == end_col:
|
||||
continue
|
||||
expected_ranges.add(
|
||||
f"{_column_letter(header.start_col)}1:{_column_letter(end_col)}1"
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user