ОСВ: строки ИНН — соседи по суммам и направление листа (#35)
Строки ИНН не в дереве: с суммами — сосед с теми же суммами (совпали оба — по направлению); без сумм — сосед по направлению листа: подсказка шапки, иначе единогласные сведения пар по суммам и однозначных блоков детей. Противоречие или отсутствие сведений — строка без пары. Хвосты ревью Task 2: тест подразделения переписан на итоговый результат (T2.9); в спеке уточнены реквизит «нерез.» (только лист, design #14) и защита «над, в дереве» с подсказкой шапки (design #16) (T2.10); опечатка в докстринге _parent_can_own (T2.11). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8bfaa4ec19
commit
71d56bd5ed
@ -28,6 +28,7 @@ from src.parser.rules import Rules
|
|||||||
__all__ = ["InnGlue", "glue_inn_rows"]
|
__all__ = ["InnGlue", "glue_inn_rows"]
|
||||||
|
|
||||||
Kind = Literal["total", "account", "indicator", "candidate", "name"]
|
Kind = Literal["total", "account", "indicator", "candidate", "name"]
|
||||||
|
Direction = Literal["above", "below"] # направление листа: строка ИНН над наименованием или под ним
|
||||||
_ZERO = Decimal(0)
|
_ZERO = Decimal(0)
|
||||||
_UNKNOWN_DIRECTION = "направление строк ИНН в листе не определено"
|
_UNKNOWN_DIRECTION = "направление строк ИНН в листе не определено"
|
||||||
|
|
||||||
@ -175,7 +176,7 @@ def _owned_by_parent(parent: int, depth: dict[int, int], schema: Sequence[OsvGro
|
|||||||
|
|
||||||
def _parent_can_own(parent: int, depth: dict[int, int], schema: Sequence[OsvGroup], hint: InnHint | None) -> bool:
|
def _parent_can_own(parent: int, depth: dict[int, int], schema: Sequence[OsvGroup], hint: InnHint | None) -> bool:
|
||||||
"""Строка ИНН без детей под наименованием-родителем: родитель — пара, если подсказка «под», или родитель по схеме —
|
"""Строка ИНН без детей под наименованием-родителем: родитель — пара, если подсказка «под», или родитель по схеме —
|
||||||
контрагент, или схема не позволяет сказать (design, design #15). Иначе строка — сосед наименования
|
контрагент, или схема не позволяет сказать (design #15). Иначе строка — сосед наименования
|
||||||
контрагента под подразделением (R-2, design #15)."""
|
контрагента под подразделением (R-2, design #15)."""
|
||||||
if hint is not None and hint.direction == "below":
|
if hint is not None and hint.direction == "below":
|
||||||
return True
|
return True
|
||||||
@ -235,10 +236,92 @@ def _pair_in_tree(tree: _Tree, schema: Sequence[OsvGroup], hint: InnHint | None,
|
|||||||
pairs.flat.append(row)
|
pairs.flat.append(row)
|
||||||
|
|
||||||
|
|
||||||
def _pair_flat(tree: _Tree, hint: InnHint | None, pairs: _Pairs) -> None: # noqa: ARG001 - hint нужен в Task 3
|
def _neighbours(tree: _Tree, row: int) -> tuple[int | None, int | None]:
|
||||||
"""Строки ИНН не в дереве (соседи наименования) — Task 3; пока без пары."""
|
"""Соседние строки-наименования того же родителя: выше и ниже (`None` — соседа нет или он не наименование)."""
|
||||||
|
siblings = tree.children.get(tree.parent[row], [])
|
||||||
|
index = siblings.index(row)
|
||||||
|
before = siblings[index - 1] if index > 0 else None
|
||||||
|
after = siblings[index + 1] if index + 1 < len(siblings) else None
|
||||||
|
return (
|
||||||
|
before if before is not None and tree.kind[before] == "name" else None,
|
||||||
|
after if after is not None and tree.kind[after] == "name" else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _block_direction(tree: _Tree, pending: list[int], pairs: _Pairs) -> set[Direction]:
|
||||||
|
"""Сведения о направлении из блоков: дети одного родителя — несопоставленные строки ИНН и наименования.
|
||||||
|
|
||||||
|
Начинается строкой ИНН — над (иначе первой строке ИНН не досталось бы владельца), кончается — под; начинается и
|
||||||
|
кончается — блок сломан, его строки ИНН без пары; ни то ни другое — сведений нет.
|
||||||
|
"""
|
||||||
|
waiting = set(pending)
|
||||||
|
claimed = set(pairs.partner.values())
|
||||||
|
found: set[Direction] = set()
|
||||||
|
for parent in dict.fromkeys(tree.parent[row] for row in pending):
|
||||||
|
block = [
|
||||||
|
row
|
||||||
|
for row in tree.children.get(parent, [])
|
||||||
|
if row in waiting or (tree.kind[row] == "name" and row not in claimed)
|
||||||
|
]
|
||||||
|
starts, ends = block[0] in waiting, block[-1] in waiting
|
||||||
|
if starts and ends:
|
||||||
|
for row in block:
|
||||||
|
if row in waiting:
|
||||||
|
pairs.reason[row] = "блок строк начинается и заканчивается строкой ИНН — направление не определить"
|
||||||
|
elif starts:
|
||||||
|
found.add("above")
|
||||||
|
elif ends:
|
||||||
|
found.add("below")
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_flat(tree: _Tree, hint: InnHint | None, pairs: _Pairs) -> None:
|
||||||
|
"""Строки ИНН не в дереве: с суммами — сосед с теми же суммами; без сумм (или совпали оба соседа) — сосед по
|
||||||
|
направлению листа: подсказка шапки, иначе единогласные сведения пар по суммам и блоков."""
|
||||||
|
pending: list[int] = []
|
||||||
|
evidence: set[Direction] = set()
|
||||||
for row in pairs.flat:
|
for row in pairs.flat:
|
||||||
|
item = tree.by_row[row]
|
||||||
|
if not _has_sums(item):
|
||||||
|
pending.append(row)
|
||||||
|
continue
|
||||||
|
before, after = _neighbours(tree, row)
|
||||||
|
sides: list[tuple[int | None, Direction]] = [(before, "below"), (after, "above")]
|
||||||
|
matches = [(other, side) for other, side in sides if other is not None and _same_sums(tree.by_row[other], item)]
|
||||||
|
if len(matches) == 1:
|
||||||
|
other, side = matches[0]
|
||||||
|
pairs.partner[row] = other
|
||||||
|
evidence.add(side)
|
||||||
|
elif matches:
|
||||||
|
pending.append(row)
|
||||||
|
else:
|
||||||
|
pairs.reason[row] = "суммы не совпали ни с одним соседним наименованием"
|
||||||
|
if pending:
|
||||||
|
_pair_by_direction(tree, pending, hint, evidence, pairs)
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_by_direction(
|
||||||
|
tree: _Tree, pending: list[int], hint: InnHint | None, evidence: set[Direction], pairs: _Pairs
|
||||||
|
) -> None:
|
||||||
|
"""Строки ИНН без сумм (и с неоднозначными соседями по суммам) — сосед по направлению листа."""
|
||||||
|
if hint is not None:
|
||||||
|
direction: Direction | None = hint.direction
|
||||||
|
else:
|
||||||
|
evidence |= _block_direction(tree, pending, pairs)
|
||||||
|
direction = next(iter(evidence)) if len(evidence) == 1 else None
|
||||||
|
for row in pending:
|
||||||
|
if row in pairs.reason:
|
||||||
|
continue
|
||||||
|
if direction is None:
|
||||||
pairs.reason[row] = _UNKNOWN_DIRECTION
|
pairs.reason[row] = _UNKNOWN_DIRECTION
|
||||||
|
continue
|
||||||
|
before, after = _neighbours(tree, row)
|
||||||
|
other = after if direction == "above" else before
|
||||||
|
if other is None:
|
||||||
|
where = "под" if direction == "above" else "над"
|
||||||
|
pairs.reason[row] = f"{where} строкой ИНН нет наименования"
|
||||||
|
else:
|
||||||
|
pairs.partner[row] = other
|
||||||
|
|
||||||
|
|
||||||
def _check_sums(tree: _Tree, pairs: _Pairs) -> None:
|
def _check_sums(tree: _Tree, pairs: _Pairs) -> None:
|
||||||
|
|||||||
@ -281,7 +281,7 @@ def test_hint_below_real_name_with_own_inn_row_is_not_an_attribute(rules: Rules)
|
|||||||
|
|
||||||
|
|
||||||
def test_childless_inn_next_to_counterparty_under_department_is_not_paired_with_department(rules: Rules) -> None:
|
def test_childless_inn_next_to_counterparty_under_department_is_not_paired_with_department(rules: Rules) -> None:
|
||||||
# R-2 T2.2: родитель-подразделение — не пара; строка ИНН идёт в соседи (направление без подсказки не определено)
|
# R-2 T2.2 / T2.9: родитель-подразделение — не пара; ИНН без детей уходит соседу-контрагенту (блок «над»)
|
||||||
schema = [
|
schema = [
|
||||||
OsvGroup(type="ACCOUNT", name="Счет"),
|
OsvGroup(type="ACCOUNT", name="Счет"),
|
||||||
OsvGroup(type="DEPARTMENT", name="Подразделения"),
|
OsvGroup(type="DEPARTMENT", name="Подразделения"),
|
||||||
@ -289,9 +289,9 @@ def test_childless_inn_next_to_counterparty_under_department_is_not_paired_with_
|
|||||||
]
|
]
|
||||||
glue = run(rules, r(0, "60", A), r(1, "БП Аренда", A), r(2, INN_A), r(2, "ООО Альфа", A), schema=schema)
|
glue = run(rules, r(0, "60", A), r(1, "БП Аренда", A), r(2, INN_A), r(2, "ООО Альфа", A), schema=schema)
|
||||||
|
|
||||||
assert glue.inns == {}
|
assert glue.inns == {4: INN_A}
|
||||||
assert glue.unpaired == frozenset({3})
|
assert glue.unpaired == frozenset()
|
||||||
assert has_warning(glue, "строка 3", "направление")
|
assert texts(glue) == ["60", "БП Аренда", "ООО Альфа"]
|
||||||
|
|
||||||
|
|
||||||
def test_hint_above_disables_no_extra_level_guard(rules: Rules) -> None:
|
def test_hint_above_disables_no_extra_level_guard(rules: Rules) -> None:
|
||||||
@ -315,3 +315,128 @@ def test_same_inn_in_column_and_row_is_fine(rules: Rules) -> None:
|
|||||||
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A, inn=INN_A), r(2, INN_A, A))
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A, inn=INN_A), r(2, INN_A, A))
|
||||||
|
|
||||||
assert (glue.inns, glue.ok) == ({2: INN_A}, True)
|
assert (glue.inns, glue.ok) == ({2: INN_A}, True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Не в дереве: соседи того же родителя ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_inn_above_sibling_without_sums(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, INN_A), r(1, "ООО Альфа", A), r(1, INN_B), r(1, "ООО Бета", B))
|
||||||
|
|
||||||
|
assert texts(glue) == ["60", "ООО Альфа", "ООО Бета"]
|
||||||
|
assert glue.inns == {3: INN_A, 5: INN_B}
|
||||||
|
assert glue.ok
|
||||||
|
|
||||||
|
|
||||||
|
def test_inn_below_sibling_without_sums_goes_to_own_counterparty(rules: Rules) -> None:
|
||||||
|
# Дефект main: ИНН первого контрагента доставался второму
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A), r(1, "ООО Бета", B), r(1, INN_B))
|
||||||
|
|
||||||
|
assert glue.inns == {2: INN_A, 4: INN_B}
|
||||||
|
assert glue.ok
|
||||||
|
|
||||||
|
|
||||||
|
def test_inn_above_sibling_with_sums(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, INN_A, A), r(1, "ООО Альфа", A), r(1, INN_B, B), r(1, "ООО Бета", B))
|
||||||
|
|
||||||
|
assert glue.inns == {3: INN_A, 5: INN_B}
|
||||||
|
|
||||||
|
|
||||||
|
def test_inn_below_sibling_with_sums(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A, A), r(1, "ООО Бета", B), r(1, INN_B, B))
|
||||||
|
|
||||||
|
assert glue.inns == {2: INN_A, 4: INN_B}
|
||||||
|
|
||||||
|
|
||||||
|
def test_sums_match_no_neighbour(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_B, B), r(1, "ООО Гамма", A))
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({3})
|
||||||
|
assert has_warning(glue, "строка 3", "суммы не совпали")
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_neighbours_match_sums_need_direction(rules: Rules) -> None:
|
||||||
|
rows = (r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A, A), r(1, "ООО Бета", A))
|
||||||
|
|
||||||
|
assert run(rules, *rows).unpaired == frozenset({3})
|
||||||
|
assert run(rules, *rows, hint=ABOVE).inns == {4: INN_A}
|
||||||
|
|
||||||
|
|
||||||
|
def test_ambiguous_block_leaves_inn_unpaired(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A), r(1, "ООО Бета", B))
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({3})
|
||||||
|
assert not glue.ok
|
||||||
|
assert has_warning(glue, "строка 3", "направление")
|
||||||
|
|
||||||
|
|
||||||
|
def test_direction_from_other_block(rules: Rules) -> None:
|
||||||
|
glue = run(
|
||||||
|
rules,
|
||||||
|
r(0, "60", A),
|
||||||
|
r(1, "60.01", A),
|
||||||
|
r(2, "ООО Альфа", A),
|
||||||
|
r(2, INN_A),
|
||||||
|
r(2, "Иванов Иван", B), # блок 60.01 «имя, ИНН, имя» направление не даёт
|
||||||
|
r(1, "60.02", B),
|
||||||
|
r(2, "ООО Гамма", B),
|
||||||
|
r(2, INN_B), # блок 60.02 кончается строкой ИНН — ИНН под наименованием
|
||||||
|
)
|
||||||
|
|
||||||
|
assert glue.inns == {3: INN_A, 7: INN_B}
|
||||||
|
|
||||||
|
|
||||||
|
def test_direction_from_sum_pair(rules: Rules) -> None:
|
||||||
|
glue = run(
|
||||||
|
rules,
|
||||||
|
r(0, "60", A),
|
||||||
|
r(1, "ООО Альфа", A),
|
||||||
|
r(1, INN_A, A),
|
||||||
|
r(1, "ООО Бета", B),
|
||||||
|
r(1, INN_B),
|
||||||
|
r(1, "Иванов Иван", B),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert glue.inns == {2: INN_A, 4: INN_B}
|
||||||
|
|
||||||
|
|
||||||
|
def test_contradicting_blocks_leave_inn_unpaired(rules: Rules) -> None:
|
||||||
|
glue = run(
|
||||||
|
rules,
|
||||||
|
r(0, "60", A),
|
||||||
|
r(1, "60.01", A),
|
||||||
|
r(2, INN_A),
|
||||||
|
r(2, "ООО Альфа", A), # начинается строкой ИНН — над
|
||||||
|
r(1, "60.02", B),
|
||||||
|
r(2, "ООО Бета", B),
|
||||||
|
r(2, INN_B), # кончается строкой ИНН — под
|
||||||
|
)
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({3, 7})
|
||||||
|
|
||||||
|
|
||||||
|
def test_broken_block_leaves_inn_unpaired(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, INN_A), r(1, "ООО Альфа", A), r(1, INN_B))
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({2, 4})
|
||||||
|
assert has_warning(glue, "строка 2", "начинается и заканчивается")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hint_direction_wins_over_blocks(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, BAD), r(1, "ООО Бета", B), hint=ABOVE)
|
||||||
|
|
||||||
|
assert glue.inns == {4: BAD}
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_inn_rows_claim_one_name(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, INN_A, A), r(1, "ООО Альфа", A), r(1, INN_B, A))
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({2, 4})
|
||||||
|
assert has_warning(glue, "претендуют")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inn_without_name_neighbour_in_direction(rules: Rules) -> None:
|
||||||
|
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A), r(1, INN_A), hint=ABOVE)
|
||||||
|
|
||||||
|
assert glue.unpaired == frozenset({3})
|
||||||
|
assert has_warning(glue, "строка 3", "нет наименования")
|
||||||
|
|||||||
@ -141,3 +141,4 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
|
|||||||
| 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не `needs_check` |
|
| 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не `needs_check` |
|
||||||
| 15 | 13.10.2026 | R-2 (design #15, T2.2): строка ИНН без детей под наименованием — пара родителю только при подсказке «под», либо если родитель по схеме COUNTERPARTY, либо схема не позволяет сказать (позиция вне схемы); иначе — сосед (`flat`) | Без этого ИНН соседа контрагента под подразделением отдавался подразделению; тот же помощник позиции в схеме, что в №10 | Файл без подсказки с ИНН-ребёнком подразделения и схемой, не совпадающей с реальностью, — строка в соседях, при неопределённом направлении без пары и `needs_check` |
|
| 15 | 13.10.2026 | R-2 (design #15, T2.2): строка ИНН без детей под наименованием — пара родителю только при подсказке «под», либо если родитель по схеме COUNTERPARTY, либо схема не позволяет сказать (позиция вне схемы); иначе — сосед (`flat`) | Без этого ИНН соседа контрагента под подразделением отдавался подразделению; тот же помощник позиции в схеме, что в №10 | Файл без подсказки с ИНН-ребёнком подразделения и схемой, не совпадающей с реальностью, — строка в соседях, при неопределённом направлении без пары и `needs_check` |
|
||||||
| 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
|
| 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
|
||||||
|
| 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` |
|
||||||
|
|||||||
@ -105,12 +105,13 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
|
|||||||
`needs_check`. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН
|
`needs_check`. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН
|
||||||
«ИНН не указан» (склейка без ИНН, без `needs_check`, R-1 #32). **Наименование** — строка не счёт, не «Итого», не Вал./Кол.,
|
«ИНН не указан» (склейка без ИНН, без `needs_check`, R-1 #32). **Наименование** — строка не счёт, не «Итого», не Вал./Кол.,
|
||||||
не строка ИНН. С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью, которая не
|
не строка ИНН. С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью, которая не
|
||||||
ИНН, — строка ИНН с нераспознанным ИНН (без пары, «ИНН не распознан»).
|
ИНН, — строка ИНН с нераспознанным ИНН (без пары, «ИНН не распознан»); таким ребёнком MUST быть только строка без детей
|
||||||
|
(лист), у строки со своими детьми — подлинное наименование (design #14).
|
||||||
|
|
||||||
**Пара строки ИНН** (связь в дереве важнее соседства):
|
**Пара строки ИНН** (связь в дереве важнее соседства):
|
||||||
- у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если в
|
- у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если в
|
||||||
`group_schema` больше одной группировки и ни одна строка листа (кроме Вал./Кол.) не стоит глубже схемы, строка ИНН не
|
`group_schema` больше одной группировки, ни одна строка листа (кроме Вал./Кол.) не стоит глубже схемы и в шапке нет
|
||||||
склеивается и без предупреждения классифицируется по схеме;
|
подсказки `<группировка>.ИНН`, строка ИНН не склеивается и без предупреждения классифицируется по схеме (design #16);
|
||||||
- родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет. Пара —
|
- родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет. Пара —
|
||||||
родитель только при подсказке «под» или если родитель по схеме — COUNTERPARTY (или позиция вне схемы); иначе у строки
|
родитель только при подсказке «под» или если родитель по схеме — COUNTERPARTY (или позиция вне схемы); иначе у строки
|
||||||
ИНН с детьми действует правило «есть дети», а без детей она — сосед (следующий пункт);
|
ИНН с детьми действует правило «есть дети», а без детей она — сосед (следующий пункт);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user