Merge pull request 'Сводная ОСВ: несопоставленные — только без ИНН; ИНН сопоставленного ЮЛ в строке (#74)' (#75) from feat/74-osv-unmapped-no-inn into main
Reviewed-on: #75
This commit is contained in:
commit
29f24b318d
@ -736,7 +736,10 @@ Q2–Q4 не подходят (R-14a). Привязываются только `
|
||||
(`gaps[]` по паре, строк нет). Q1-эквивалентность ОСВ — см. «Привязка документов».
|
||||
- Контрагент строки: хранимый `osv_entry.counterparty_id` либо резолв на лету (ИНН → точное имя → алиас → нормализованное имя; алиас приоритетнее ЮЛ-тёзок, OQ-128; в БД не пишется,
|
||||
`match_stored = false`); `match_kind` — `INN`/`NAME`/`ALIAS` (шаг резолва), `MANUAL`, `PROPAGATED`.
|
||||
Тип контрагента: `GSK` (член периметра), `GK` (та же ГК), иначе `EXTERNAL`.
|
||||
Тип контрагента: `GSK` (член периметра), `GK` (та же ГК), иначе `EXTERNAL`. Имя и ИНН сопоставленного ЮЛ справочника —
|
||||
`counterparty_entity_name` / `counterparty_entity_inn` (`counterparty_name` / `counterparty_inn` — как в файле).
|
||||
- **Несопоставленная строка** (`unmapped=true`, `unmapped_total`, «Осталось N») — нет ЮЛ после резолва **и** нет ИНН из
|
||||
файла; строка с ИНН, которого нет в справочнике, сопоставления не требует (решение владельца 08.10.2026, отмена П-1, #74).
|
||||
- Фильтры: `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped`, `match_kind` (повторяемый), `q`
|
||||
(имя — подстрока без регистра, ИНН — префикс). Порядок: имя ЮЛ, счёт (натуральный), номер строки.
|
||||
- `warnings` строк — диагностика (расхождение `counterparty_id` между кварталами), не контракт.
|
||||
|
||||
@ -121,6 +121,7 @@ class ConsolidatedRow:
|
||||
counterparty_inn: str | None
|
||||
counterparty_id: uuid.UUID | None
|
||||
counterparty_entity_name: str | None
|
||||
counterparty_entity_inn: str | None # ИНН сопоставленного ЮЛ справочника (не из файла)
|
||||
counterparty_type: CounterpartyType
|
||||
match_kind: CounterpartyMatchKind | None
|
||||
match_stored: bool # `False` — найдено дорезолвом на чтении (или не найдено), в `osv_entry` не записано
|
||||
@ -161,7 +162,7 @@ class Resolver(Protocol):
|
||||
@dataclass(frozen=True)
|
||||
class OsvFilter:
|
||||
"""Фильтры свода (`GET …/osv`, экспорт): пустые поля не фильтруют; `q` — подстрока имени без регистра или префикс
|
||||
ИНН контрагента; `unmapped` — `counterparty_id` пуст после дорезолва (`False` — только сопоставленные)."""
|
||||
ИНН контрагента; `unmapped` — строка требует сопоставления (`needs_mapping`; `False` — все остальные)."""
|
||||
|
||||
legal_entity_id: uuid.UUID | None = None
|
||||
account_root: str | None = None
|
||||
@ -348,6 +349,7 @@ def _compose( # noqa: PLR0913 - сборка строки свода из ча
|
||||
counterparty_inn=representative.counterparty_inn,
|
||||
counterparty_id=counterparty_id,
|
||||
counterparty_entity_name=counterparty.name if counterparty is not None else None,
|
||||
counterparty_entity_inn=counterparty.inn if counterparty is not None else None,
|
||||
counterparty_type=classify(
|
||||
counterparty_id, members=context.members, entities=context.entities, group_id=context.group_id
|
||||
),
|
||||
@ -469,6 +471,12 @@ def build( # noqa: PLR0913 - сигнатура зафиксирована пл
|
||||
# --------------------------------------------------------------------------- фильтры и сводка
|
||||
|
||||
|
||||
def needs_mapping(row: ConsolidatedRow) -> bool:
|
||||
"""Строка требует ручного сопоставления: нет ЮЛ после дорезолва и нет ИНН контрагента в файле. Строка с ИНН,
|
||||
которого нет в справочнике, определена ИНН и сопоставления не требует (решение владельца 08.10.2026, отмена П-1)."""
|
||||
return row.counterparty_id is None and not normalize_inn(row.counterparty_inn or "")
|
||||
|
||||
|
||||
def _matches_query(row: ConsolidatedRow, q: str | None) -> bool:
|
||||
text = (q or "").strip()
|
||||
if not text:
|
||||
@ -485,7 +493,7 @@ def _matches(row: ConsolidatedRow, f: OsvFilter) -> bool: # noqa: PLR0911 - п
|
||||
return False
|
||||
if f.counterparty_type is not None and row.counterparty_type != f.counterparty_type:
|
||||
return False
|
||||
if f.unmapped is not None and (row.counterparty_id is None) != f.unmapped:
|
||||
if f.unmapped is not None and needs_mapping(row) != f.unmapped:
|
||||
return False
|
||||
if f.match_kinds and row.match_kind not in f.match_kinds:
|
||||
return False
|
||||
@ -498,8 +506,8 @@ def apply_filter(rows: Iterable[ConsolidatedRow], f: OsvFilter) -> list[Consolid
|
||||
|
||||
|
||||
def summary(c: Consolidated) -> tuple[int, int, int]:
|
||||
"""Сводка свода: `rows_total`, `unmapped_total` (`counterparty_id` пуст после дорезолва), `auto_matched_total`
|
||||
"""Сводка свода: `rows_total`, `unmapped_total` (`needs_mapping`: нет ЮЛ и нет ИНН из файла), `auto_matched_total`
|
||||
(`match_kind` ∈ {`NAME`, `ALIAS`} независимо от `match_stored`)."""
|
||||
unmapped = sum(1 for row in c.rows if row.counterparty_id is None)
|
||||
unmapped = sum(1 for row in c.rows if needs_mapping(row))
|
||||
auto_matched = sum(1 for row in c.rows if row.match_kind in AUTO_MATCH_KINDS)
|
||||
return len(c.rows), unmapped, auto_matched
|
||||
|
||||
@ -203,6 +203,7 @@ class ConsolidatedRowOut(BaseModel):
|
||||
counterparty_inn: str | None
|
||||
counterparty_id: uuid.UUID | None
|
||||
counterparty_entity_name: str | None
|
||||
counterparty_entity_inn: str | None # ИНН сопоставленного ЮЛ справочника; `counterparty_inn` — ИНН из файла
|
||||
counterparty_type: CounterpartyType
|
||||
match_kind: CounterpartyMatchKind | None
|
||||
match_stored: bool # `False` — сопоставление найдено дорезолвом на чтении и в `osv_entry` не записано
|
||||
|
||||
@ -196,6 +196,7 @@ async def test_page_rows_order_total_and_money_strings(auth_client: AuthClientFa
|
||||
assert row["legal_entity_id"] == str(world.a.id) and row["legal_entity_inn"] == world.a.inn
|
||||
assert row["account_root"] == "60" and row["counterparty_inn"] == world.b.inn
|
||||
assert (row["counterparty_id"], row["counterparty_entity_name"]) == (str(world.b.id), "ООО Б")
|
||||
assert row["counterparty_entity_inn"] == world.b.inn
|
||||
assert (row["counterparty_type"], row["match_kind"], row["match_stored"]) == ("GSK", "INN", True)
|
||||
cumulative = _row(body, "ООО А", "62", "ООО Б")
|
||||
assert cumulative["is_cumulative"] is True and cumulative["closing_credit"] == "42.50"
|
||||
@ -216,6 +217,7 @@ async def test_counterparty_types_and_read_time_resolution(
|
||||
unknown = _row(body, "ООО А", "60", "Неизвестный контрагент")
|
||||
assert (unknown["counterparty_id"], unknown["counterparty_type"], unknown["match_kind"]) == (None, "EXTERNAL", None)
|
||||
assert unknown["match_stored"] is False and unknown["counterparty_entity_name"] is None
|
||||
assert unknown["counterparty_entity_inn"] is None
|
||||
by_name = _row(body, "ООО А", "60", "ООО Внешний")
|
||||
assert (by_name["counterparty_id"], by_name["match_kind"], by_name["match_stored"]) == (
|
||||
str(world.external.id), "NAME", False) # fmt: skip
|
||||
@ -224,6 +226,8 @@ async def test_counterparty_types_and_read_time_resolution(
|
||||
assert (by_alias["counterparty_id"], by_alias["match_kind"], by_alias["match_stored"]) == (
|
||||
str(world.aliased.id), "ALIAS", False) # fmt: skip
|
||||
assert by_alias["counterparty_entity_name"] == "Фирма Эф"
|
||||
# ИНН в файле нет — ИНН сопоставленного ЮЛ справочника отдаётся отдельным полем (arm-core #74)
|
||||
assert (by_alias["counterparty_inn"], by_alias["counterparty_entity_inn"]) == (None, world.aliased.inn)
|
||||
assert _row(body, "ООО Б", "60", "ООО А")["counterparty_type"] == "GSK"
|
||||
|
||||
|
||||
@ -278,7 +282,7 @@ async def test_summary_counts_and_page_total_agree(auth_client: AuthClientFactor
|
||||
unmapped = await _page(client, world, "unmapped=true")
|
||||
assert unmapped["total"] == summary["unmapped_total"]
|
||||
assert len(unmapped["items"]) == UNMAPPED_TOTAL
|
||||
assert all(row["counterparty_id"] is None for row in unmapped["items"])
|
||||
assert all(row["counterparty_id"] is None and row["counterparty_inn"] is None for row in unmapped["items"])
|
||||
mapped = await _page(client, world, "unmapped=false")
|
||||
assert mapped["total"] == ROWS_TOTAL - UNMAPPED_TOTAL
|
||||
|
||||
|
||||
@ -387,9 +387,10 @@ async def test_mapping_from_slice_plan_propagates_and_creates_alias(
|
||||
alias = (await seeded.execute(select(LegalEntityAlias))).scalar_one()
|
||||
assert alias.created_by == (await _user(seeded, KA_RF)).id and alias.created_at is not None
|
||||
after_summary = await _summary(client, world)
|
||||
# строка 76 с чужим ИНН: хранимого сопоставления нет (проверено выше), но ИНН нет в справочнике и не блокирует
|
||||
# поиск по имени, а алиас приоритетнее ЮЛ-тёзки (OQ-128) — на чтении она резолвится по алиасу
|
||||
assert after_summary["unmapped_total"] == before_summary["unmapped_total"] - (1 + body["propagated"]) - 1
|
||||
# строка 76 с чужим ИНН в «Осталось N» не входит и до сопоставления: ИНН из файла есть (отмена П-1, arm-core #74);
|
||||
# на чтении она резолвится по алиасу (ИНН нет в справочнике и не блокирует поиск по имени, алиас приоритетнее
|
||||
# ЮЛ-тёзки, OQ-128)
|
||||
assert after_summary["unmapped_total"] == before_summary["unmapped_total"] - (1 + body["propagated"])
|
||||
assert after_summary["unmapped_total"] == 0
|
||||
page = (await client.get(f"/consolidations/versions/{world.version.id}/osv?limit=100")).json()
|
||||
alien = next(r for r in page["items"] if r["account_code"] == "76" and r["counterparty_name"] == NAME)
|
||||
|
||||
@ -48,9 +48,9 @@ SCHASTL_FILE_NAME = "ПЕКАРНЯ СЧАСТЛИВОЧНАЯ" # в файла
|
||||
RB_NAME_IN_SCHASTL = "Русский бисквит"
|
||||
TIME_BUDGET_SECONDS = 60.0
|
||||
Q1, Q2, Q3 = "Q1", "Q2", "Q3"
|
||||
# свод РБ 2024 Q3; 1379 = 1381 − 2: две строки находят контрагента по ИНН, извлечённому парсером из названия
|
||||
# (tabular-parser #15, golden fixtures #8; issue #33)
|
||||
ROWS_TOTAL, UNMAPPED_TOTAL = 1385, 1379
|
||||
# свод РБ 2024 Q3; несопоставленные — строки без ЮЛ и без ИНН из файла (отмена П-1, #74): из 1379 строк без ЮЛ
|
||||
# (1381 − 2 по ИНН, извлечённому парсером из названия: tabular-parser #15, golden fixtures #8, issue #33) у 119 ИНН есть
|
||||
ROWS_TOTAL, UNMAPPED_TOTAL = 1385, 1260
|
||||
CHANGED_ROWS, PROPAGATED = 4, 17 # строк свода, получивших контрагента; записей osv_entry, кроме выбранной (18 всего)
|
||||
SUMS = ("opening_debit", "opening_credit", "turnover_debit", "turnover_credit", "closing_debit", "closing_credit")
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ from src.domain.consolidated_osv import (
|
||||
apply_filter,
|
||||
build,
|
||||
classify,
|
||||
needs_mapping,
|
||||
summary,
|
||||
)
|
||||
from src.domain.osv_mapping import Resolution
|
||||
@ -331,6 +332,7 @@ def test_counterparty_id_from_last_quarter_with_warning_on_divergence() -> None:
|
||||
row = _build([q1, q2, q3], [early, unmapped, late], resolver=resolver).rows[0]
|
||||
assert (row.counterparty_id, row.match_kind, row.match_stored) == (SCH.id, MANUAL, True)
|
||||
assert (row.counterparty_type, row.counterparty_entity_name) == (GSK, SCH.name)
|
||||
assert row.counterparty_entity_inn == SCH.inn
|
||||
assert len(row.warnings) == 1
|
||||
assert str(early.id) in row.warnings[0]
|
||||
assert str(CHKF.id) in row.warnings[0]
|
||||
@ -366,9 +368,10 @@ def test_counterparty_types_on_rb_fixture_shape() -> None:
|
||||
assert by_entry[to_sch.id].counterparty_type == EXTERNAL
|
||||
assert (by_entry[to_sch.id].counterparty_id, by_entry[to_sch.id].match_kind) == (None, None)
|
||||
assert by_entry[to_sch.id].match_stored is False
|
||||
assert by_entry[to_sch.id].counterparty_entity_name is None
|
||||
assert (by_entry[to_sch.id].counterparty_entity_name, by_entry[to_sch.id].counterparty_entity_inn) == (None, None)
|
||||
assert by_entry[to_chkf.id].counterparty_type == GK
|
||||
assert by_entry[to_chkf.id].counterparty_entity_name == CHKF.name
|
||||
assert by_entry[to_chkf.id].counterparty_entity_inn == CHKF.inn # ИНН ЮЛ справочника, в файле ИНН нет
|
||||
|
||||
|
||||
def test_classify_counterparty_type() -> None:
|
||||
@ -388,6 +391,7 @@ def test_resolve_on_read_is_not_stored() -> None:
|
||||
row = _build([cumulative], [source], resolver=resolver).rows[0]
|
||||
assert (row.counterparty_id, row.match_kind, row.match_stored) == (SCH.id, ALIAS, False)
|
||||
assert (row.counterparty_type, row.counterparty_entity_name) == (GSK, SCH.name)
|
||||
assert row.counterparty_entity_inn == SCH.inn
|
||||
assert resolver.calls == [(" ", "Пекарня Счастливочная ООО")]
|
||||
assert source.counterparty_id is None
|
||||
assert source.match_kind is None
|
||||
@ -462,11 +466,12 @@ def test_gaps_are_ordered_by_entity_name_and_account() -> None:
|
||||
def _filter_fixture() -> tuple[ConsolidatedRow, ...]:
|
||||
rb, sch = _doc(RB, "60", cumulative=True), _doc(SCH, "62", cumulative=True)
|
||||
rows = [
|
||||
_row(rb, "ООО Ромашка", inn="7701000001", row_number=1), # EXTERNAL, не сопоставлена
|
||||
_row(rb, "ООО Ромашка", inn="7701000001", row_number=1), # EXTERNAL, не сопоставлена, но ИНН из файла есть
|
||||
_row(rb, "Пекарня Счастливочная ООО", row_number=2), # ALIAS дорезолвом
|
||||
_row(rb, "ЧКФ", row_number=3, account_code="60.01", counterparty_id=CHKF.id, match_kind=NAME), # GK, NAME
|
||||
_row(sch, "Русский бисквит", inn="3528052718", counterparty_id=RB.id, match_kind=INN), # GSK, INN
|
||||
_row(sch, "Внешний", row_number=2, counterparty_id=OUTSIDER.id, match_kind=MANUAL), # EXTERNAL
|
||||
_row(sch, "Ведомость", inn=" ", row_number=3), # EXTERNAL, не сопоставлена и без ИНН — требует сопоставления
|
||||
]
|
||||
resolver = _Resolver({(None, "Пекарня Счастливочная ООО"): Resolution(SCH.id, ALIAS)})
|
||||
return _build([rb, sch], rows, resolver=resolver).rows
|
||||
@ -484,21 +489,28 @@ def test_empty_filter_returns_all_rows() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("osv_filter", "expected"),
|
||||
[
|
||||
(OsvFilter(legal_entity_id=SCH.id), ["Русский бисквит", "Внешний"]),
|
||||
(OsvFilter(account_root="62"), ["Русский бисквит", "Внешний"]),
|
||||
(OsvFilter(legal_entity_id=SCH.id), ["Русский бисквит", "Внешний", "Ведомость"]),
|
||||
(OsvFilter(account_root="62"), ["Русский бисквит", "Внешний", "Ведомость"]),
|
||||
(OsvFilter(account_root="6"), []),
|
||||
(OsvFilter(counterparty_type=GSK), ["Пекарня Счастливочная ООО", "Русский бисквит"]),
|
||||
(OsvFilter(counterparty_type=GK), ["ЧКФ"]),
|
||||
(OsvFilter(counterparty_type=EXTERNAL), ["ООО Ромашка", "Внешний"]),
|
||||
(OsvFilter(unmapped=True), ["ООО Ромашка"]),
|
||||
(OsvFilter(unmapped=False), ["Пекарня Счастливочная ООО", "ЧКФ", "Русский бисквит", "Внешний"]),
|
||||
(OsvFilter(counterparty_type=EXTERNAL), ["ООО Ромашка", "Внешний", "Ведомость"]),
|
||||
# отмена П-1 (08.10.2026): строка с ИНН из файла не требует сопоставления, даже если ИНН нет в справочнике
|
||||
(OsvFilter(unmapped=True), ["Ведомость"]),
|
||||
(
|
||||
OsvFilter(unmapped=False),
|
||||
["ООО Ромашка", "Пекарня Счастливочная ООО", "ЧКФ", "Русский бисквит", "Внешний"],
|
||||
),
|
||||
(OsvFilter(match_kinds=frozenset({NAME, ALIAS})), ["Пекарня Счастливочная ООО", "ЧКФ"]),
|
||||
(OsvFilter(match_kinds=frozenset({MANUAL})), ["Внешний"]),
|
||||
(OsvFilter(q="рОмАш"), ["ООО Ромашка"]),
|
||||
(OsvFilter(q=" бисквит "), ["Русский бисквит"]),
|
||||
(OsvFilter(q="7701"), ["ООО Ромашка"]),
|
||||
(OsvFilter(q="1000001"), []), # ИНН — только префикс
|
||||
(OsvFilter(q=" "), ["ООО Ромашка", "Пекарня Счастливочная ООО", "ЧКФ", "Русский бисквит", "Внешний"]),
|
||||
(
|
||||
OsvFilter(q=" "),
|
||||
["ООО Ромашка", "Пекарня Счастливочная ООО", "ЧКФ", "Русский бисквит", "Внешний", "Ведомость"],
|
||||
),
|
||||
(OsvFilter(legal_entity_id=RB.id, counterparty_type=EXTERNAL), ["ООО Ромашка"]),
|
||||
(OsvFilter(legal_entity_id=RB.id, match_kinds=frozenset({ALIAS}), q="пекарня"), ["Пекарня Счастливочная ООО"]),
|
||||
(OsvFilter(account_root="60", unmapped=True, q="ЧКФ"), []),
|
||||
@ -513,10 +525,29 @@ def test_summary_counts_and_matches_filters() -> None:
|
||||
rows = _filter_fixture()
|
||||
result = Consolidated(rows=rows, gaps=(), documents_total=2)
|
||||
rows_total, unmapped_total, auto_matched_total = summary(result)
|
||||
assert (rows_total, unmapped_total, auto_matched_total) == (5, 1, 2)
|
||||
assert (rows_total, unmapped_total, auto_matched_total) == (6, 1, 2)
|
||||
assert len(apply_filter(rows, OsvFilter(unmapped=True))) == unmapped_total
|
||||
assert len(apply_filter(rows, OsvFilter(match_kinds=frozenset({NAME, ALIAS})))) == auto_matched_total
|
||||
assert all(r.counterparty_id is None for r in apply_filter(rows, OsvFilter(unmapped=True)))
|
||||
assert all(needs_mapping(r) for r in apply_filter(rows, OsvFilter(unmapped=True)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("inn", "counterparty_id", "expected"),
|
||||
[
|
||||
(None, None, True),
|
||||
("", None, True),
|
||||
(" ", None, True),
|
||||
("7701000001", None, False), # ИНН из файла, в справочнике нет — отмена П-1
|
||||
(None, uuid.uuid4(), False),
|
||||
("7701000001", uuid.uuid4(), False),
|
||||
],
|
||||
)
|
||||
def test_needs_mapping_is_no_counterparty_and_no_inn(
|
||||
inn: str | None, counterparty_id: uuid.UUID | None, *, expected: bool
|
||||
) -> None:
|
||||
"""Строка требует сопоставления («Указать», `unmapped`), только если нет ни ЮЛ, ни ИНН из файла."""
|
||||
row = dataclasses.replace(_filter_fixture()[0], counterparty_inn=inn, counterparty_id=counterparty_id)
|
||||
assert needs_mapping(row) is expected
|
||||
|
||||
|
||||
def test_counterparty_type_is_str_enum() -> None:
|
||||
|
||||
@ -35,6 +35,7 @@ def _row(name: str = "ООО Б", **overrides: object) -> ConsolidatedRow:
|
||||
"counterparty_inn": "0987654321",
|
||||
"counterparty_id": None,
|
||||
"counterparty_entity_name": None,
|
||||
"counterparty_entity_inn": None,
|
||||
"counterparty_type": CounterpartyType.EXTERNAL,
|
||||
"match_kind": None,
|
||||
"match_stored": False,
|
||||
|
||||
@ -87,6 +87,7 @@ def _row(**amounts: Decimal | None) -> ConsolidatedRowOut:
|
||||
"counterparty_inn": None,
|
||||
"counterparty_id": None,
|
||||
"counterparty_entity_name": None,
|
||||
"counterparty_entity_inn": None,
|
||||
"counterparty_type": "EXTERNAL",
|
||||
"match_kind": None,
|
||||
"match_stored": False,
|
||||
|
||||
@ -43,7 +43,8 @@ SHALL склеиваться по ключу (`account_code` строки, `coun
|
||||
### Requirement: Строки свода, признак и дорезолв
|
||||
Строка свода SHALL содержать: `entry_ids`, ЮЛ (`id`, `name`, `inn`), `account_code`, `account_root`, `period`
|
||||
(`"<год> Q<n>"`), `is_cumulative`, `row_number`, `counterparty_name` и `counterparty_inn` как в файле,
|
||||
`counterparty_id`, `counterparty_entity_name`, `counterparty_type` ∈ {`GSK`, `GK`, `EXTERNAL`}, `match_kind` ∈
|
||||
`counterparty_id`, `counterparty_entity_name` и `counterparty_entity_inn` (имя и ИНН сопоставленного ЮЛ справочника;
|
||||
`null` без сопоставления), `counterparty_type` ∈ {`GSK`, `GK`, `EXTERNAL`}, `match_kind` ∈
|
||||
{`INN`, `NAME`, `ALIAS`, `MANUAL`, `PROPAGATED`, null}, `match_stored`, шесть сумм (`Decimal`, в JSON — строки),
|
||||
`warnings[]`. В свод SHALL входить только строки `group_type = COUNTERPARTY` с `data_type IN (BU, SUM)`; строки
|
||||
MUST NOT агрегироваться между контрагентами. `counterparty_id` SHALL браться из исходных строк (непустое значение
|
||||
@ -73,15 +74,25 @@ MUST NOT агрегироваться между контрагентами. `co
|
||||
### Requirement: Страница свода и сводка
|
||||
`GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`,
|
||||
`row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`,
|
||||
`counterparty_type`, `unmapped=true` (`counterparty_id` пуст после дорезолва), `match_kind` (повторяемый), `q`
|
||||
`counterparty_type`, `unmapped=true` (строка требует сопоставления: `counterparty_id` пуст после дорезолва **и**
|
||||
`counterparty_inn` пуст или из пробелов; строка с ИНН из файла, которого нет в справочнике, сопоставления не требует —
|
||||
решение владельца 08.10.2026, отмена П-1; `unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q`
|
||||
(подстрока без регистра в `counterparty_name` или префикс `counterparty_inn`). `GET …/osv/summary` SHALL отдавать
|
||||
`rows_total`, `unmapped_total`, `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} независимо от
|
||||
`rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} независимо от
|
||||
`match_stored`), `documents_total` (привязанные ОСВ-документы) и `gaps[]` (`legal_entity_id`, `legal_entity_name`,
|
||||
`account_code`, `missing_quarters`). Версия в любом статусе, включая `CANCELLED`, SHALL читаться.
|
||||
|
||||
#### Scenario: Только несопоставленные
|
||||
- **WHEN** запрошено `GET …/osv?unmapped=true`
|
||||
- **THEN** все строки ответа с `counterparty_id = null`, `total` = `summary.unmapped_total`
|
||||
- **THEN** все строки ответа с `counterparty_id = null` и без `counterparty_inn`, `total` = `summary.unmapped_total`
|
||||
|
||||
#### Scenario: Строка с ИНН вне справочника не требует сопоставления
|
||||
- **WHEN** у строки `counterparty_inn = "7701000001"`, такого ИНН и имени в справочнике нет (`counterparty_id = null`)
|
||||
- **THEN** строки нет в `GET …/osv?unmapped=true`, она не входит в `summary.unmapped_total`
|
||||
|
||||
#### Scenario: ИНН сопоставленного ЮЛ
|
||||
- **WHEN** строка без ИНН в файле сопоставлена с ЮЛ по алиасу (на лету или хранимо)
|
||||
- **THEN** `counterparty_inn = null`, `counterparty_entity_inn` — ИНН этого ЮЛ из справочника
|
||||
|
||||
#### Scenario: Фильтр по способу
|
||||
- **WHEN** запрошено `GET …/osv?match_kind=NAME&match_kind=ALIAS`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user