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] = []
|
one_line_names: list[str] = []
|
||||||
component_names: list[str] = []
|
component_names: list[str] = []
|
||||||
|
|
||||||
# Left-side "one line" can be immediately before structured column.
|
# Поле "в одной строке" может располагаться слева от структурного.
|
||||||
left = by_index.get(index - 1)
|
left = by_index.get(index - 1)
|
||||||
if left:
|
if left:
|
||||||
left_name = str(left.get("column_name", "")).strip()
|
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(
|
STRUCTURED_GROUP_RULES: tuple[StructuredGroupRule, ...] = _build_structured_group_rules(
|
||||||
_MAPPING_DATA
|
_MAPPING_DATA
|
||||||
)
|
)
|
||||||
|
_ACCOUNT_PLACEHOLDER = "00000000000000000000"
|
||||||
|
|
||||||
|
|
||||||
def build_fixed_row(
|
def build_fixed_row(
|
||||||
@ -332,6 +333,7 @@ def build_fixed_row(
|
|||||||
source_payload, suffix_index, rule.candidate_keys
|
source_payload, suffix_index, rule.candidate_keys
|
||||||
)
|
)
|
||||||
_apply_structured_group_rules(row)
|
_apply_structured_group_rules(row)
|
||||||
|
_apply_conditional_rules_1_74(row)
|
||||||
for column in FIXED_REPORT_COLUMNS:
|
for column in FIXED_REPORT_COLUMNS:
|
||||||
row.setdefault(column, "")
|
row.setdefault(column, "")
|
||||||
return row
|
return row
|
||||||
@ -414,3 +416,118 @@ def _pick_structured_value(
|
|||||||
return ""
|
return ""
|
||||||
separator = " " if join_with_space else ", "
|
separator = " " if join_with_space else ", "
|
||||||
return separator.join(values)
|
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
|
title: str
|
||||||
|
|
||||||
|
|
||||||
# Grouped upper header from the template report for readability.
|
# Верхняя группирующая шапка согласно согласованным логическим категориям.
|
||||||
TOP_GROUP_HEADERS: tuple[GroupHeader, ...] = (
|
TOP_GROUP_HEADERS: tuple[GroupHeader, ...] = (
|
||||||
|
GroupHeader(start_col=1, end_col=1, title="Служебные поля"),
|
||||||
|
GroupHeader(start_col=2, end_col=6, title="Реквизиты сообщения/КО/филиала"),
|
||||||
GroupHeader(
|
GroupHeader(
|
||||||
start_col=1,
|
start_col=7,
|
||||||
end_col=4,
|
|
||||||
title="Информация о КО, передающей сведения в уполномоченный орган",
|
|
||||||
),
|
|
||||||
GroupHeader(start_col=5, end_col=8, title="Сведения об уполномоченном лице"),
|
|
||||||
GroupHeader(
|
|
||||||
start_col=11,
|
|
||||||
end_col=13,
|
end_col=13,
|
||||||
title="Информация о филиале КО, передающем сведения в уполномоченный орган",
|
title="Идентификация записи и контрольные признаки",
|
||||||
),
|
),
|
||||||
GroupHeader(start_col=14, end_col=36, title="Информация об операции"),
|
GroupHeader(start_col=14, end_col=36, title="Параметры операции"),
|
||||||
GroupHeader(
|
GroupHeader(
|
||||||
start_col=37,
|
start_col=37,
|
||||||
end_col=48,
|
end_col=48,
|
||||||
title="Сведения о переводах денежных средств, в том числе электронных денежных средств",
|
title="Перевод ДС и реквизиты расчетов",
|
||||||
),
|
),
|
||||||
GroupHeader(
|
GroupHeader(
|
||||||
start_col=49,
|
start_col=49,
|
||||||
end_col=53,
|
end_col=66,
|
||||||
title="Сведения о месте приема наличных денежных средств",
|
title="Места приема/выдачи наличных и авторизация ЭСП",
|
||||||
),
|
|
||||||
GroupHeader(
|
|
||||||
start_col=55,
|
|
||||||
end_col=59,
|
|
||||||
title="Сведения о месте выдачи наличных денежных средств",
|
|
||||||
),
|
|
||||||
GroupHeader(
|
|
||||||
start_col=60,
|
|
||||||
end_col=64,
|
|
||||||
title="Сведения о месте авторизации ЭСП",
|
|
||||||
),
|
),
|
||||||
GroupHeader(
|
GroupHeader(
|
||||||
start_col=67,
|
start_col=67,
|
||||||
end_col=69,
|
end_col=75,
|
||||||
title=(
|
title="Внесение/выдача у одного оператора (место операции)",
|
||||||
"Сведения о внесении наличных денежных средств на свой банковский счет "
|
|
||||||
"или о получении наличных денежных средств со своего банковского счета "
|
|
||||||
"у одного оператора по переводу денежных средств"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GroupHeader(
|
|
||||||
start_col=70,
|
|
||||||
end_col=74,
|
|
||||||
title="Сведения о месте приема (выдачи) наличных денежных средств",
|
|
||||||
),
|
),
|
||||||
GroupHeader(
|
GroupHeader(
|
||||||
start_col=76,
|
start_col=76,
|
||||||
end_col=80,
|
end_col=86,
|
||||||
title="Сведения о месте совершения операции с денежными средствами в наличной форме",
|
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._column_formats = [self._resolve_format(column) for column in self.columns]
|
||||||
self._build_group_header_row()
|
self._build_group_header_row()
|
||||||
self._build_header_row()
|
self._build_header_row()
|
||||||
# Freeze both header rows.
|
# Фиксируем обе строки шапки.
|
||||||
self.sheet.freeze_panes(2, 0)
|
self.sheet.freeze_panes(2, 0)
|
||||||
self.sheet.autofilter(1, 0, 1, len(self.columns) - 1)
|
self.sheet.autofilter(1, 0, 1, len(self.columns) - 1)
|
||||||
self._next_data_row = 2
|
self._next_data_row = 2
|
||||||
@ -161,6 +163,9 @@ class StreamingReportWriter:
|
|||||||
continue
|
continue
|
||||||
first_col = header.start_col - 1
|
first_col = header.start_col - 1
|
||||||
last_col = min(header.end_col, len(self.columns)) - 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(
|
self.sheet.merge_range(
|
||||||
0,
|
0,
|
||||||
first_col,
|
first_col,
|
||||||
|
|||||||
@ -294,3 +294,86 @@ def test_structured_column_uses_non_one_line_candidate_when_mixed_tags() -> None
|
|||||||
]
|
]
|
||||||
== "г. Москва, ул. Пушкина, д. 1"
|
== "г. Москва, ул. Пушкина, д. 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)
|
workbook = load_workbook(destination)
|
||||||
sheet = workbook["Отчет"]
|
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}
|
merged_ranges = {str(rng) for rng in sheet.merged_cells.ranges}
|
||||||
expected_ranges = set()
|
expected_ranges = set()
|
||||||
for header in TOP_GROUP_HEADERS:
|
for header in TOP_GROUP_HEADERS:
|
||||||
if header.start_col > len(FIXED_REPORT_COLUMNS):
|
if header.start_col > len(FIXED_REPORT_COLUMNS):
|
||||||
continue
|
continue
|
||||||
end_col = min(header.end_col, len(FIXED_REPORT_COLUMNS))
|
end_col = min(header.end_col, len(FIXED_REPORT_COLUMNS))
|
||||||
|
if header.start_col == end_col:
|
||||||
|
continue
|
||||||
expected_ranges.add(
|
expected_ranges.add(
|
||||||
f"{_column_letter(header.start_col)}1:{_column_letter(end_col)}1"
|
f"{_column_letter(header.start_col)}1:{_column_letter(end_col)}1"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user