feat(statements): PATCH создаёт строку и ячейку по line_code (#80)
CellPatch адресует ячейку ровно одним из line_id / line_code. Строка справочника редакции, которой нет в форме, создаётся при первой непустой правке (line_code_original_value = NULL — признак «не из источника»); отсутствующая ячейка существующей строки тоже создаётся. Адресация (колонка, строка, код) проверяется целиком до первой вставки, две ячейки новой строки делят одну строку, повтор line_code после создания даёт 422 UNKNOWN_CELL. Аудит — одно RECOGNIZED_DATA_EDIT на запрос, у созданной ячейки before = null. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a543b5cd1
commit
37e7f40890
@ -22,7 +22,11 @@ from src.models.statements import (
|
||||
|
||||
__all__ = [
|
||||
"BATCH",
|
||||
"add_line",
|
||||
"add_value",
|
||||
"column_ids",
|
||||
"delete_file_statements",
|
||||
"form_lines",
|
||||
"get_meta",
|
||||
"insert_statement",
|
||||
"load_form",
|
||||
@ -189,6 +193,55 @@ async def lock_cells(
|
||||
}
|
||||
|
||||
|
||||
async def form_lines(session: AsyncSession, meta_id: uuid.UUID) -> list[FinancialStatementLine]:
|
||||
"""Строки формы (без значений) — адресация ячеек правки по `line_id` и `line_code`."""
|
||||
stmt = select(FinancialStatementLine).where(FinancialStatementLine.meta_id == meta_id)
|
||||
return list((await session.execute(stmt)).scalars())
|
||||
|
||||
|
||||
async def column_ids(session: AsyncSession, meta_id: uuid.UUID) -> set[uuid.UUID]:
|
||||
"""Id колонок формы."""
|
||||
stmt = select(FinancialStatementColumn.id).where(FinancialStatementColumn.meta_id == meta_id)
|
||||
return set((await session.execute(stmt)).scalars())
|
||||
|
||||
|
||||
async def add_line(session: AsyncSession, meta_id: uuid.UUID, code: str, name: str) -> FinancialStatementLine:
|
||||
"""Строка справочника, созданная правкой аналитика: `*_original_value = NULL` — признак «не из источника»."""
|
||||
line = FinancialStatementLine(
|
||||
id=uuid.uuid4(),
|
||||
meta_id=meta_id,
|
||||
line_code=code,
|
||||
line_code_original_value=None,
|
||||
line_code_confidence=None,
|
||||
line_code_modified=False,
|
||||
line_name=name,
|
||||
line_name_original_value=None,
|
||||
line_name_confidence=None,
|
||||
line_name_modified=False,
|
||||
)
|
||||
session.add(line)
|
||||
await session.flush()
|
||||
return line
|
||||
|
||||
|
||||
async def add_value(session: AsyncSession, line_id: uuid.UUID, column_id: uuid.UUID) -> FinancialStatementValue:
|
||||
"""Пустая ячейка, созданная правкой: не распознана (`is_recognized = NULL`), исходной суммы нет."""
|
||||
value = FinancialStatementValue(
|
||||
id=uuid.uuid4(),
|
||||
line_id=line_id,
|
||||
column_id=column_id,
|
||||
amount=None,
|
||||
string_value=None,
|
||||
confidence=None,
|
||||
is_recognized=None,
|
||||
is_modified=False,
|
||||
original_amount=None,
|
||||
)
|
||||
session.add(value)
|
||||
await session.flush()
|
||||
return value
|
||||
|
||||
|
||||
async def get_meta(
|
||||
session: AsyncSession, document_id: uuid.UUID, *, refresh: bool = False
|
||||
) -> FinancialStatementMeta | None:
|
||||
|
||||
@ -11,7 +11,7 @@ from collections.abc import Sequence
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
||||
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator
|
||||
|
||||
from src.domain.statement_checks import ChecksReport, FailedCheck
|
||||
from src.domain.statement_lines import TemplateLine
|
||||
@ -289,14 +289,24 @@ def statement_out( # noqa: PLR0913, PLR0917 - сборка ответа из ч
|
||||
|
||||
|
||||
class CellPatch(BaseModel):
|
||||
"""Новая сумма ячейки: не более двух знаков и 16 цифр до запятой (`numeric(18,2)`), `null` — очистить."""
|
||||
"""Новая сумма ячейки: не более двух знаков и 16 цифр до запятой (`numeric(18,2)`), `null` — очистить.
|
||||
|
||||
Ячейка адресуется ровно одним из: `line_id` — строка формы (если ячейки в колонке нет, она создаётся); `line_code` —
|
||||
строка справочника редакции, которой в форме нет (создаётся при первой непустой правке; design D-4)."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
line_id: uuid.UUID
|
||||
line_id: uuid.UUID | None = None
|
||||
line_code: str | None = Field(None, max_length=10)
|
||||
column_id: uuid.UUID
|
||||
amount: Decimal | None = Field(max_digits=18, decimal_places=2)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _one_address(self) -> "CellPatch":
|
||||
if (self.line_id is None) == (self.line_code is None):
|
||||
raise ValueError("укажите ровно одно из line_id и line_code")
|
||||
return self
|
||||
|
||||
|
||||
class HeaderPatch(BaseModel):
|
||||
"""Отсутствующее поле не меняется; `report_date: null` — очистить дату (`model_fields_set`); `inn: null` — 422
|
||||
|
||||
@ -16,7 +16,7 @@ from src.common.exceptions import ConflictError, DomainValidationError, NotFound
|
||||
from src.domain.osv_mapping import counterparty_check
|
||||
from src.domain.policy import VisibilityScope
|
||||
from src.domain.statement_checks import ChecksReport, FailedCheck, cross_form_check, load_check_rules, run_checks
|
||||
from src.domain.statement_lines import load_line_catalog
|
||||
from src.domain.statement_lines import TemplateLine, load_line_catalog
|
||||
from src.domain.statement_mapping import edition_for, quarter_of, statement_period, statement_status, values_ok
|
||||
from src.models.directories import LegalEntity
|
||||
from src.models.enums import (
|
||||
@ -29,7 +29,7 @@ from src.models.enums import (
|
||||
RecognitionStatus,
|
||||
)
|
||||
from src.models.files import Document, RecognitionJob
|
||||
from src.models.statements import FinancialStatementMeta
|
||||
from src.models.statements import FinancialStatementLine, FinancialStatementMeta
|
||||
from src.repositories import files as files_repo
|
||||
from src.repositories import statements as repo
|
||||
from src.repositories.counterparties import CounterpartyIndex
|
||||
@ -39,6 +39,7 @@ from src.services.documents import CONFIRMABLE_STATUSES, DOCUMENT_NOT_CONFIRMABL
|
||||
DOCUMENT_NOT_FOUND = "документ не найден" # одинаково для чужого и несуществующего
|
||||
DOCUMENT_NOT_STATEMENT = "DOCUMENT_NOT_STATEMENT"
|
||||
DOCUMENT_READONLY = "DOCUMENT_READONLY"
|
||||
LINE_EXISTS = "строка уже есть в форме — передайте line_id"
|
||||
UNKNOWN_CELL = "UNKNOWN_CELL"
|
||||
DUPLICATE_CELL = "DUPLICATE_CELL"
|
||||
NOTHING_TO_ACCEPT = "NOTHING_TO_ACCEPT"
|
||||
@ -156,18 +157,40 @@ class StatementService:
|
||||
document.updated_at = utc_now()
|
||||
|
||||
async def _apply_cells(
|
||||
self, meta: FinancialStatementMeta, body: schemas.StatementPatch, before: dict[str, Any], after: dict[str, Any]
|
||||
self,
|
||||
document: Document,
|
||||
meta: FinancialStatementMeta,
|
||||
body: schemas.StatementPatch,
|
||||
before: dict[str, Any],
|
||||
after: dict[str, Any],
|
||||
) -> None:
|
||||
"""Значения `body.values` (D-7): `original_amount` при первой правке, `is_modified`, `modified_by/at`."""
|
||||
keys = [(cell.line_id, cell.column_id) for cell in body.values]
|
||||
"""Значения `body.values` (D-7 Среза 3b; design D-4 statement-template-lines): `original_amount` при первой
|
||||
правке, `is_modified`, `modified_by/at`. Ячейка, которой нет в БД, создаётся при первой непустой правке; строка
|
||||
по `line_code` — из справочника редакции документа. Вся адресация проверяется до первой вставки."""
|
||||
if not body.values:
|
||||
return
|
||||
keys = [(cell.line_id or cell.line_code, cell.column_id) for cell in body.values]
|
||||
if len(keys) != len(set(keys)):
|
||||
raise DomainValidationError(DUPLICATE_CELL, "ячейка указана в запросе дважды")
|
||||
cells = await repo.lock_cells(self._session, meta.id, keys)
|
||||
if len(cells) != len(set(keys)):
|
||||
raise DomainValidationError(UNKNOWN_CELL, "ячейка не принадлежит форме")
|
||||
targets = await self._resolve_cells(document, meta, body)
|
||||
existing = await repo.lock_cells(
|
||||
self._session, meta.id, [(line.id, cell.column_id) for cell, line, _ in targets if line is not None]
|
||||
)
|
||||
created: dict[str, FinancialStatementLine] = {}
|
||||
now = utc_now()
|
||||
for cell in body.values:
|
||||
value = cells[(cell.line_id, cell.column_id)]
|
||||
for cell, line, item in targets:
|
||||
value = existing.get((line.id, cell.column_id)) if line is not None else None
|
||||
if value is None:
|
||||
if cell.amount is None:
|
||||
continue # пустая сумма в несуществующую ячейку — не изменение
|
||||
target = line
|
||||
if target is None and item is not None:
|
||||
target = created.get(item.code)
|
||||
if target is None:
|
||||
target = created[item.code] = await repo.add_line(self._session, meta.id, item.code, item.name)
|
||||
if target is None:
|
||||
raise DomainValidationError(UNKNOWN_CELL, "ячейка не принадлежит форме")
|
||||
value = await repo.add_value(self._session, target.id, cell.column_id)
|
||||
if value.amount == cell.amount:
|
||||
continue
|
||||
key = f"value:{value.id}"
|
||||
@ -178,6 +201,37 @@ class StatementService:
|
||||
value.is_modified = value.amount != value.original_amount
|
||||
value.modified_by, value.modified_at = self._audit.user_id, now
|
||||
|
||||
async def _resolve_cells(
|
||||
self, document: Document, meta: FinancialStatementMeta, body: schemas.StatementPatch
|
||||
) -> list[tuple[schemas.CellPatch, FinancialStatementLine | None, TemplateLine | None]]:
|
||||
"""Адресация ячеек правки: строка формы (`line_id`) либо строка справочника редакции (`line_code`). Ничего не
|
||||
вставляет — любая ошибка адресации (422) происходит до первой записи."""
|
||||
columns = await repo.column_ids(self._session, meta.id)
|
||||
lines = await repo.form_lines(self._session, meta.id)
|
||||
by_id = {line.id: line for line in lines}
|
||||
codes = {line.line_code for line in lines}
|
||||
template = {
|
||||
item.code: item
|
||||
for item in load_line_catalog().lines(edition_for(document.reporting_date), document.document_type.value)
|
||||
}
|
||||
targets: list[tuple[schemas.CellPatch, FinancialStatementLine | None, TemplateLine | None]] = []
|
||||
for cell in body.values:
|
||||
if cell.column_id not in columns:
|
||||
raise DomainValidationError(UNKNOWN_CELL, "ячейка не принадлежит форме")
|
||||
if cell.line_id is not None:
|
||||
line = by_id.get(cell.line_id)
|
||||
if line is None:
|
||||
raise DomainValidationError(UNKNOWN_CELL, "ячейка не принадлежит форме")
|
||||
targets.append((cell, line, None))
|
||||
continue
|
||||
if cell.line_code in codes:
|
||||
raise DomainValidationError(UNKNOWN_CELL, LINE_EXISTS)
|
||||
item = template.get(cell.line_code or "")
|
||||
if item is None:
|
||||
raise DomainValidationError(UNKNOWN_CELL, "строки с таким кодом нет в форме этой редакции")
|
||||
targets.append((cell, None, item))
|
||||
return targets
|
||||
|
||||
async def _apply_header(
|
||||
self,
|
||||
document: Document,
|
||||
@ -218,7 +272,7 @@ class StatementService:
|
||||
raise DomainValidationError(INVALID_INN, "ИНН — 10 или 12 цифр")
|
||||
before: dict[str, Any] = {}
|
||||
after: dict[str, Any] = {}
|
||||
await self._apply_cells(meta, body, before, after)
|
||||
await self._apply_cells(document, meta, body, before, after)
|
||||
if header is not None:
|
||||
await self._apply_header(document, meta, header, before, after)
|
||||
if not after:
|
||||
|
||||
@ -1060,3 +1060,229 @@ async def test_status_with_lines_but_no_columns_is_stable_after_edit(
|
||||
response = await client.patch(f"/documents/{document.id}/statement", json={"header": {"units": "руб."}})
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["lines"] and response.json()["document"]["status"] == "RECOGNIZED"
|
||||
|
||||
|
||||
async def _column(client, document_id, index: int) -> str:
|
||||
body = (await client.get(f"/documents/{document_id}/statement")).json()
|
||||
return next(c["id"] for c in body["columns"] if c["column_index"] == index)
|
||||
|
||||
|
||||
async def _db_line(session: AsyncSession, document: Document, code: str) -> FinancialStatementLine | None:
|
||||
meta = await _meta(session, document)
|
||||
return (
|
||||
await session.execute(
|
||||
select(FinancialStatementLine)
|
||||
.where(FinancialStatementLine.meta_id == meta.id, FinancialStatementLine.line_code == code)
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
async def _edit_events(session: AsyncSession, document: Document) -> list[AuditEvent]:
|
||||
return list(
|
||||
(
|
||||
await session.execute(
|
||||
select(AuditEvent).where(
|
||||
AuditEvent.object_id == document.id, AuditEvent.action == AuditAction.RECOGNIZED_DATA_EDIT
|
||||
)
|
||||
)
|
||||
).scalars()
|
||||
)
|
||||
|
||||
|
||||
async def test_patch_creates_template_line(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={"values": [{"line_code": "1700", "column_id": col0, "amount": "2371333"}]},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
line = next(item for item in response.json()["lines"] if item["line_code"] == "1700")
|
||||
assert line["id"] is not None and line["origin"] == "template" and line["line_name"] == "БАЛАНС"
|
||||
cell = next(v for v in line["values"] if v["column_id"] == col0)
|
||||
assert Decimal(cell["amount"]) == Decimal("2371333") and cell["original_amount"] is None
|
||||
assert cell["is_modified"] is True and cell["is_recognized"] is None and cell["modified_by"] is not None
|
||||
other = [v for v in line["values"] if v["column_id"] != col0]
|
||||
assert other and all(v["id"] is None for v in other) # остальные колонки не созданы
|
||||
db_line = await _db_line(seeded, document, "1700")
|
||||
assert db_line is not None and db_line.line_code_original_value is None and db_line.line_name_original_value is None
|
||||
(event,) = await _edit_events(seeded, document)
|
||||
key = f"value:{cell['id']}"
|
||||
assert event.before_value == {key: None} and Decimal(event.after_value[key]) == Decimal("2371333")
|
||||
|
||||
|
||||
async def test_patch_two_cells_of_new_line_create_one_line(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
"""Review Focus 2."""
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
before = await db_line_count(seeded, document)
|
||||
client = await auth_client(KA_RF)
|
||||
col0, col1 = await _column(client, document.id, 0), await _column(client, document.id, 1)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={
|
||||
"values": [
|
||||
{"line_code": "1700", "column_id": col0, "amount": "1"},
|
||||
{"line_code": "1700", "column_id": col1, "amount": "2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert await db_line_count(seeded, document) == before + 1
|
||||
line = next(item for item in response.json()["lines"] if item["line_code"] == "1700")
|
||||
assert sum(1 for v in line["values"] if v["id"] is not None) == 2 # noqa: PLR2004
|
||||
|
||||
|
||||
async def test_patch_repeat_line_code_after_creation_rejected(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
"""Review Focus 3: второй «Сохранить» со старым черновиком по коду — 422, строка не задвоена."""
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
item = {"line_code": "1700", "column_id": col0, "amount": "1"}
|
||||
assert (
|
||||
await client.patch(f"/documents/{document.id}/statement", json={"values": [item]})
|
||||
).status_code == HTTPStatus.OK
|
||||
count = await db_line_count(seeded, document)
|
||||
response = await client.patch(f"/documents/{document.id}/statement", json={"values": [{**item, "amount": "5"}]})
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
assert response.json()["detail"]["code"] == "UNKNOWN_CELL"
|
||||
assert "line_id" in response.json()["detail"]["message"]
|
||||
assert await db_line_count(seeded, document) == count
|
||||
|
||||
|
||||
async def test_patch_line_code_existing_or_unknown_rejected(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
count = await db_line_count(seeded, document)
|
||||
for code in ("1150", "1105"): # 1150 есть в форме; 1105 нет в справочнике fsbu4
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={"values": [{"line_code": code, "column_id": col0, "amount": "1"}]},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, code
|
||||
assert response.json()["detail"]["code"] == "UNKNOWN_CELL"
|
||||
assert await db_line_count(seeded, document) == count
|
||||
assert await _edit_events(seeded, document) == []
|
||||
|
||||
|
||||
async def test_patch_line_code_without_edition_rejected(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
document.reporting_date = None
|
||||
await seeded.flush()
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={"values": [{"line_code": "1700", "column_id": col0, "amount": "1"}]},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
assert response.json()["detail"]["code"] == "UNKNOWN_CELL"
|
||||
|
||||
|
||||
async def test_patch_null_into_missing_cell_is_noop(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
count = await db_line_count(seeded, document)
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={"values": [{"line_code": "1700", "column_id": col0, "amount": None}]},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert await db_line_count(seeded, document) == count
|
||||
assert await _edit_events(seeded, document) == []
|
||||
|
||||
|
||||
async def test_patch_creates_missing_cell_of_existing_line(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_value(seeded, document, "1150", 1)
|
||||
client = await auth_client(KA_RF)
|
||||
body = (await client.get(f"/documents/{document.id}/statement")).json()
|
||||
line = next(item for item in body["lines"] if item["line_code"] == "1150")
|
||||
col1 = next(c["id"] for c in body["columns"] if c["column_index"] == 1)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={"values": [{"line_id": line["id"], "column_id": col1, "amount": "77"}]},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
cell = next(
|
||||
v
|
||||
for v in next(i for i in response.json()["lines"] if i["id"] == line["id"])["values"]
|
||||
if v["column_id"] == col1
|
||||
)
|
||||
assert cell["id"] is not None and Decimal(cell["amount"]) == Decimal("77") and cell["original_amount"] is None
|
||||
assert cell["is_modified"] is True
|
||||
|
||||
|
||||
async def test_patch_bad_column_rolls_back_new_line(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
"""Review Focus 1: одна ячейка корректна (новая строка), другая — чужая колонка: 422, строка не создана."""
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
count = await db_line_count(seeded, document)
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement",
|
||||
json={
|
||||
"values": [
|
||||
{"line_code": "1700", "column_id": col0, "amount": "1"},
|
||||
{"line_code": "1700", "column_id": str(uuid.uuid4()), "amount": "2"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
assert response.json()["detail"]["code"] == "UNKNOWN_CELL"
|
||||
assert await db_line_count(seeded, document) == count
|
||||
|
||||
|
||||
async def test_patch_cell_address_must_be_exactly_one(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
client = await auth_client(KA_RF)
|
||||
line, cell = await _first_cell(client, document.id)
|
||||
for item in (
|
||||
{"line_id": line["id"], "line_code": "1700", "column_id": cell["column_id"], "amount": "1"},
|
||||
{"column_id": cell["column_id"], "amount": "1"},
|
||||
):
|
||||
response = await client.patch(f"/documents/{document.id}/statement", json={"values": [item]})
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
assert isinstance(response.json()["detail"], list) # 422 валидации тела — массив
|
||||
|
||||
|
||||
async def test_patch_duplicate_line_code_rejected(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||
) -> None:
|
||||
document = await form(seeded, fixtures_dir)
|
||||
await drop_line(seeded, document, "1700")
|
||||
client = await auth_client(KA_RF)
|
||||
col0 = await _column(client, document.id, 0)
|
||||
item = {"line_code": "1700", "column_id": col0, "amount": "1"}
|
||||
response = await client.patch(
|
||||
f"/documents/{document.id}/statement", json={"values": [item, {**item, "amount": "2"}]}
|
||||
)
|
||||
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
assert response.json()["detail"]["code"] == "DUPLICATE_CELL"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user