feat(types): API формы v5.19 — статусы строк, needs_review, issues, предупреждения файла (#43)
- types/api.ts: LineStatus, Severity, IssueKind, PeriodKind, FileWarningOut, IssueOut, EditionLineOut, IssueCountsOut, CellTarget, LinePatch, ColumnPatch; новые поля StatementHeaderOut, ColumnOut, ValueOut, LineOut, FailedCheckOut, StatementOut (form_variant, issues, edition_lines), DocumentBrief (issue_counts), DocumentOut (issues), JobBrief.warnings — FileWarningOut[]. - statementGrid.ts: GridRow получает статус строки, номер строки, код как распознан, флаги needs_review / is_confirmed / codeModified; cellFlags: внимание только по needs_review, новый флаг confirmed. - labels.ts: lineStatusLabel, severityLabel. - фикстура __fixtures__/statement-v2.json (БАЛАНС 2025 Q3, NEEDS_CHECK): 16 строк × 2 колонки, у каждой пары ValueOut. - тесты: переносы полей v2 и покрытие пар строка × колонка, cellFlags по needs_review, подписи; хелперы row() / v() / gridRow / doc() дополнены значениями по умолчанию. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
0f2bbc80c9
commit
f83b03543e
@ -28,6 +28,25 @@ export interface UploadResult {
|
||||
rejected: RejectedOut[];
|
||||
}
|
||||
|
||||
/** Статусы строки формы (v5.19): ok — строка бланка; unreadable — код не распознан; missing — кода нет; detail — расшифровка. */
|
||||
export type LineStatus = 'ok' | 'unreadable' | 'missing' | 'detail';
|
||||
export type Severity = 'error' | 'info';
|
||||
export type IssueKind = 'warning' | 'relation' | 'soft';
|
||||
export type PeriodKind =
|
||||
| 'absolute'
|
||||
| 'relative:current'
|
||||
| 'relative:prev_year_end'
|
||||
| 'relative:prev_prev_year_end'
|
||||
| 'relative:prev_period'
|
||||
| 'none';
|
||||
/** Предупреждение разбора файла (`job.warnings`); `mapping_warnings` остаются строками. */
|
||||
export interface FileWarningOut {
|
||||
code: string;
|
||||
severity: Severity;
|
||||
message: string;
|
||||
locator: string | null;
|
||||
}
|
||||
|
||||
export type RecognitionStatus = string; // pending | processing | done | error | timeout | dlq — только для показа
|
||||
export interface JobBrief {
|
||||
id: string;
|
||||
@ -35,7 +54,7 @@ export interface JobBrief {
|
||||
error_code: string | null;
|
||||
error_message: string | null;
|
||||
mapping_error: string | null;
|
||||
warnings: string[];
|
||||
warnings: FileWarningOut[];
|
||||
mapping_warnings: string[];
|
||||
finished_at: string | null;
|
||||
}
|
||||
@ -52,6 +71,7 @@ export interface DocumentBrief {
|
||||
counterparty_check: CheckResult | null;
|
||||
locator: string | null;
|
||||
comments_count: number;
|
||||
issue_counts: IssueCountsOut;
|
||||
}
|
||||
export type FileType = 'XLS' | 'XLSX' | 'XLSB' | 'PDF' | 'PNG' | 'JPG';
|
||||
export interface FileOut {
|
||||
@ -74,6 +94,36 @@ export interface DocumentOut extends DocumentBrief {
|
||||
confirmed_by: string | null;
|
||||
confirmed_at: string | null;
|
||||
warnings: string[];
|
||||
issues: IssueOut[];
|
||||
}
|
||||
/** Замечание к документу или форме: предупреждение разбора, нарушение соотношения или мягкая проверка. */
|
||||
export interface IssueOut {
|
||||
id: string;
|
||||
kind: IssueKind;
|
||||
code: string;
|
||||
severity: Severity;
|
||||
message: string;
|
||||
line_ids: string[];
|
||||
column_id: string | null;
|
||||
column_index: number | null;
|
||||
field: string | null;
|
||||
source: 'service' | 'core';
|
||||
relation: string | null;
|
||||
codes: string[];
|
||||
expected: string | null;
|
||||
actual: string | null;
|
||||
}
|
||||
export interface HeaderSourceOut {
|
||||
text: string | null;
|
||||
confidence: string | null;
|
||||
}
|
||||
export interface EditionLineOut {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
export interface IssueCountsOut {
|
||||
error: number;
|
||||
info: number;
|
||||
}
|
||||
/** Уровень строки ОСВ (DBML `osv_group_type`). */
|
||||
export type OsvGroupType = 'ACCOUNT' | 'DEPARTMENT' | 'COUNTERPARTY' | 'CONTRACT' | 'OTHER';
|
||||
@ -109,17 +159,25 @@ export interface StatementHeaderOut {
|
||||
report_date: HeaderFieldOut;
|
||||
units: HeaderFieldOut;
|
||||
organization_name: string | null;
|
||||
organization_name_modified: boolean;
|
||||
okud: string | null;
|
||||
okpo: string | null;
|
||||
okopf: string | null;
|
||||
okfs: string | null;
|
||||
okei: string | null;
|
||||
address: string | null;
|
||||
/** поля шапки, которые требуют проверки аналитиком */
|
||||
needs_review_fields: string[];
|
||||
/** что распознано до нормализации, по имени поля */
|
||||
source: Partial<Record<string, HeaderSourceOut>>;
|
||||
}
|
||||
export interface ColumnOut {
|
||||
id: string;
|
||||
column_index: number;
|
||||
label: string | null;
|
||||
label_confidence: string | null;
|
||||
period_date: string | null;
|
||||
period_kind: PeriodKind;
|
||||
is_current: boolean | null;
|
||||
}
|
||||
export interface ValueOut {
|
||||
@ -128,18 +186,32 @@ export interface ValueOut {
|
||||
column_index: number;
|
||||
amount: string | null;
|
||||
string_value: string | null;
|
||||
text: string | null;
|
||||
confidence: string | null;
|
||||
is_recognized: boolean | null;
|
||||
is_modified: boolean;
|
||||
original_amount: string | null;
|
||||
needs_review: boolean;
|
||||
is_confirmed: boolean;
|
||||
}
|
||||
export interface LineOut {
|
||||
id: string | null; // null — строка достроена по справочнику редакции
|
||||
origin: 'source' | 'template';
|
||||
row_index: number | null;
|
||||
source_ref: string | null;
|
||||
line_status: LineStatus;
|
||||
line_code: string | null;
|
||||
line_code_text: string | null;
|
||||
line_code_confidence: string | null;
|
||||
line_code_modified: boolean;
|
||||
line_name: string | null;
|
||||
/** справочное имя строки по коду редакции */
|
||||
reference_name: string | null;
|
||||
// код четырёхзначной строки-родителя у пятизначной подстроки (считает ядро, AURORA-1527); иначе null
|
||||
parent_code: string | null;
|
||||
parent_row_index: number | null;
|
||||
needs_review: boolean;
|
||||
is_confirmed: boolean;
|
||||
values: ValueOut[];
|
||||
}
|
||||
export interface FailedCheckOut {
|
||||
@ -147,6 +219,8 @@ export interface FailedCheckOut {
|
||||
relation: string;
|
||||
expected: string;
|
||||
actual: string;
|
||||
codes: string[];
|
||||
line_ids: string[];
|
||||
}
|
||||
export interface ChecksOut {
|
||||
failed: FailedCheckOut[];
|
||||
@ -165,17 +239,34 @@ export interface StatementOut {
|
||||
counterparty_check: CheckResult | null;
|
||||
locator: string | null;
|
||||
file_id: string;
|
||||
form_variant: 'full' | 'simplified' | null;
|
||||
};
|
||||
edition: string | null;
|
||||
header: StatementHeaderOut;
|
||||
columns: ColumnOut[];
|
||||
lines: LineOut[];
|
||||
checks: ChecksOut;
|
||||
issues: IssueOut[];
|
||||
edition_lines: EditionLineOut[];
|
||||
}
|
||||
/** Адрес строки в PATCH: из БД — `line_id`, достроенная — `line_code`. */
|
||||
export type CellTarget = { line_id: string } | { line_code: string };
|
||||
export type CellPatch =
|
||||
| (CellTarget & { column_id: string; amount: string | null })
|
||||
| { line_id: string; column_id: string; confirmed: boolean };
|
||||
export type LinePatch =
|
||||
| { line_id: string; line_code: string }
|
||||
| { line_id: string; line_status: 'detail' }
|
||||
| { line_id: string; confirmed: boolean };
|
||||
export interface ColumnPatch {
|
||||
column_id: string;
|
||||
period_date: string;
|
||||
}
|
||||
export type CellPatch = ({ line_id: string } | { line_code: string }) & { column_id: string; amount: string | null };
|
||||
export interface StatementPatch {
|
||||
header?: { inn?: string; report_date?: string; units?: string };
|
||||
header?: { inn?: string; report_date?: string; units?: string; organization_name?: string };
|
||||
values?: CellPatch[];
|
||||
columns?: ColumnPatch[];
|
||||
lines?: LinePatch[];
|
||||
}
|
||||
|
||||
export type CounterpartyType = 'GSK' | 'GK' | 'EXTERNAL';
|
||||
|
||||
@ -4,9 +4,11 @@ import {
|
||||
documentStatusLabel,
|
||||
documentTypeLabel,
|
||||
fileStatusLabel,
|
||||
lineStatusLabel,
|
||||
matchKindLabel,
|
||||
periodLabel,
|
||||
quarterLabel,
|
||||
severityLabel,
|
||||
} from './labels';
|
||||
import { osvPeriodLabel, quarterShortLabel } from './labels';
|
||||
|
||||
@ -71,3 +73,16 @@ describe('подписи периода', () => {
|
||||
expect(osvPeriodLabel('странное')).toBe('странное');
|
||||
});
|
||||
});
|
||||
|
||||
describe('подписи формы v5.19', () => {
|
||||
it('статус строки', () => {
|
||||
expect(lineStatusLabel('ok')).toBe('Строка формы');
|
||||
expect(lineStatusLabel('unreadable')).toBe('Код не распознан');
|
||||
expect(lineStatusLabel('missing')).toBe('Кода нет');
|
||||
expect(lineStatusLabel('detail')).toBe('Расшифровка');
|
||||
});
|
||||
it('уровень замечания', () => {
|
||||
expect(severityLabel('error')).toBe('Ошибка');
|
||||
expect(severityLabel('info')).toBe('Информация');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { CounterpartyType, DocumentStatus, DocumentType, FileStatus, MatchKind, Quarter } from '../types/api';
|
||||
import type { CounterpartyType, DocumentStatus, DocumentType, FileStatus, LineStatus, MatchKind, Quarter, Severity } from '../types/api';
|
||||
|
||||
const TYPE_LABELS: Record<DocumentType, string> = {
|
||||
BALANCE: 'Бух. баланс',
|
||||
@ -70,3 +70,14 @@ const MATCH_KIND: Record<MatchKind, string> = {
|
||||
|
||||
export const counterpartyTypeLabel = (t: CounterpartyType): string => COUNTERPARTY_TYPE[t] ?? t;
|
||||
export const matchKindLabel = (k: MatchKind | null): string => (k ? (MATCH_KIND[k] ?? k) : 'не сопоставлен');
|
||||
|
||||
const LINE_STATUS: Record<LineStatus, string> = {
|
||||
ok: 'Строка формы',
|
||||
unreadable: 'Код не распознан',
|
||||
missing: 'Кода нет',
|
||||
detail: 'Расшифровка',
|
||||
};
|
||||
export const lineStatusLabel = (s: LineStatus): string => LINE_STATUS[s] ?? s;
|
||||
|
||||
const SEVERITY: Record<Severity, string> = { error: 'Ошибка', info: 'Информация' };
|
||||
export const severityLabel = (s: Severity): string => SEVERITY[s] ?? s;
|
||||
|
||||
1006
src/widgets/DocumentReview/__fixtures__/statement-v2.json
Normal file
1006
src/widgets/DocumentReview/__fixtures__/statement-v2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,15 @@ const row = (code: string | null, name: string | null = `строка ${code}`):
|
||||
name,
|
||||
parentCode: null,
|
||||
cells: {},
|
||||
referenceName: null,
|
||||
lineStatus: 'ok',
|
||||
rowIndex: null,
|
||||
parentRowIndex: null,
|
||||
codeText: code,
|
||||
codeConfidence: null,
|
||||
needsReview: false,
|
||||
isConfirmed: false,
|
||||
codeModified: false,
|
||||
});
|
||||
const sub = (code: string, parentCode: string | null): GridRow => ({ ...row(code), parentCode });
|
||||
const shape = (rows: ReturnType<typeof formLayout>) =>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { StatementOut, ValueOut } from '../../types/api';
|
||||
import statementV2Json from './__fixtures__/statement-v2.json';
|
||||
import type { GridRow } from './statementGrid';
|
||||
import { applyDraftInput, buildStatementGrid, cellFlags, cellKey, draftToPatch, emptySublineKeys } from './statementGrid';
|
||||
|
||||
@ -9,13 +10,19 @@ const v = (id: string | null, column_id: string, column_index: number, extra: Pa
|
||||
column_index,
|
||||
amount: '1.00',
|
||||
string_value: null,
|
||||
text: null,
|
||||
confidence: '100',
|
||||
is_recognized: true,
|
||||
is_modified: false,
|
||||
original_amount: null,
|
||||
needs_review: false,
|
||||
is_confirmed: false,
|
||||
...extra,
|
||||
});
|
||||
|
||||
// Фикстура v5.19 (тип StatementOut): приведение нужно, т.к. импорт JSON расширяет строковые литералы до string
|
||||
const statementV2 = statementV2Json as StatementOut;
|
||||
|
||||
const statement = {
|
||||
columns: [
|
||||
{ id: 'c2', column_index: 1, label: '31.12.2023', period_date: null, is_current: false },
|
||||
@ -32,6 +39,32 @@ describe('buildStatementGrid', () => {
|
||||
expect(grid.rows[0].cells.c1?.id).toBe('v1');
|
||||
expect(grid.rows[0].cells.c2?.id).toBe('v2');
|
||||
});
|
||||
it('буквально переносит поля v2 из фикстуры', () => {
|
||||
const { rows } = buildStatementGrid(statementV2);
|
||||
const byKey = new Map(rows.map((row) => [row.key, row]));
|
||||
expect(byKey.get('l-u7')).toMatchObject({
|
||||
lineStatus: 'unreadable',
|
||||
rowIndex: 7,
|
||||
codeText: 'ИЧ,250',
|
||||
code: null,
|
||||
key: 'l-u7',
|
||||
});
|
||||
expect(byKey.get('l-d2')?.parentRowIndex).toBe(1);
|
||||
expect(byKey.get('code-1250')).toMatchObject({
|
||||
key: 'code-1250',
|
||||
rowIndex: null,
|
||||
referenceName: 'Денежные средства и денежные эквиваленты',
|
||||
});
|
||||
expect(byKey.get('l-1230')).toMatchObject({ needsReview: true, codeConfidence: '50' });
|
||||
});
|
||||
it('каждая пара «строка × колонка» фикстуры имеет ValueOut', () => {
|
||||
const { columns, rows } = buildStatementGrid(statementV2);
|
||||
for (const row of rows) {
|
||||
for (const column of columns) {
|
||||
expect(row.cells[column.id], `${row.key} × ${column.id}`).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('достроенная строка: ключ по коду, lineId = null', () => {
|
||||
const withTemplate = {
|
||||
...statement,
|
||||
@ -51,22 +84,28 @@ describe('buildStatementGrid', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellFlags', () => {
|
||||
it('внимание: уверенность < 100 или не распознано', () => {
|
||||
expect(cellFlags(v('a', 'c', 0, { confidence: '87.5' })).attention).toBe(true);
|
||||
expect(cellFlags(v('a', 'c', 0, { is_recognized: false })).attention).toBe(true);
|
||||
expect(cellFlags(v('a', 'c', 0)).attention).toBe(false);
|
||||
expect(cellFlags(v('a', 'c', 0, { confidence: null })).attention).toBe(false);
|
||||
describe('cellFlags — внимание только по needs_review', () => {
|
||||
it('низкая уверенность и is_recognized=false без needs_review — не внимание', () => {
|
||||
expect(cellFlags(v('a', 'c', 0, { confidence: '50', is_recognized: false, needs_review: false })).attention).toBe(false);
|
||||
});
|
||||
it('needs_review=true — внимание, даже при уверенности 100', () => {
|
||||
expect(cellFlags(v('a', 'c', 0, { confidence: '100', needs_review: true })).attention).toBe(true);
|
||||
});
|
||||
it('is_confirmed — подтверждена, уверенность не меняется', () => {
|
||||
expect(cellFlags(v('a', 'c', 0, { is_confirmed: true })).confirmed).toBe(true);
|
||||
});
|
||||
it('undefined — все флаги false', () => {
|
||||
expect(cellFlags(undefined)).toEqual({ attention: false, modified: false, confirmed: false });
|
||||
});
|
||||
it('пустая достроенная ячейка не требует внимания', () => {
|
||||
expect(cellFlags(v(null, 'c', 0, { amount: null, confidence: null, is_recognized: null }))).toEqual({
|
||||
attention: false,
|
||||
modified: false,
|
||||
confirmed: false,
|
||||
});
|
||||
});
|
||||
it('исправлено', () => {
|
||||
expect(cellFlags(v('a', 'c', 0, { is_modified: true })).modified).toBe(true);
|
||||
expect(cellFlags(undefined)).toEqual({ attention: false, modified: false });
|
||||
});
|
||||
});
|
||||
|
||||
@ -129,6 +168,15 @@ describe('parent_code и пустые строки детализации', () =
|
||||
origin: 'source',
|
||||
parentCode,
|
||||
cells,
|
||||
referenceName: null,
|
||||
lineStatus: 'ok',
|
||||
rowIndex: null,
|
||||
parentRowIndex: null,
|
||||
codeText: key,
|
||||
codeConfidence: null,
|
||||
needsReview: false,
|
||||
isConfirmed: false,
|
||||
codeModified: false,
|
||||
});
|
||||
it('пустая подстрока: все ячейки без суммы и без строки; родитель и нулевая сумма — не пустые', () => {
|
||||
const rows = [
|
||||
|
||||
@ -1,13 +1,26 @@
|
||||
import type { CellPatch, ColumnOut, StatementOut, ValueOut } from '../../types/api';
|
||||
import type { CellPatch, ColumnOut, LineStatus, StatementOut, ValueOut } from '../../types/api';
|
||||
|
||||
export type GridRow = {
|
||||
key: string;
|
||||
lineId: string | null;
|
||||
code: string | null;
|
||||
/** line_name — наименование из файла */
|
||||
name: string | null;
|
||||
/** справочное имя строки по коду редакции */
|
||||
referenceName: string | null;
|
||||
origin: 'source' | 'template';
|
||||
/** Родитель строки детализации — отдаёт ядро (`parent_code`); своего правила на фронте нет. */
|
||||
parentCode: string | null;
|
||||
lineStatus: LineStatus;
|
||||
/** порядковый номер строки в файле; null у достроенной и созданной правкой */
|
||||
rowIndex: number | null;
|
||||
parentRowIndex: number | null;
|
||||
/** код как распознан в файле (например «ИЧ,250»), даже если `code` не распознан */
|
||||
codeText: string | null;
|
||||
codeConfidence: string | null;
|
||||
needsReview: boolean;
|
||||
isConfirmed: boolean;
|
||||
codeModified: boolean;
|
||||
cells: Record<string, ValueOut | undefined>;
|
||||
};
|
||||
|
||||
@ -26,21 +39,32 @@ export function buildStatementGrid(statement: StatementOut): { columns: ColumnOu
|
||||
lineId: line.id,
|
||||
code: line.line_code,
|
||||
name: line.line_name,
|
||||
referenceName: line.reference_name,
|
||||
origin: line.origin,
|
||||
parentCode: line.parent_code ?? null,
|
||||
lineStatus: line.line_status,
|
||||
rowIndex: line.row_index,
|
||||
parentRowIndex: line.parent_row_index,
|
||||
codeText: line.line_code_text,
|
||||
codeConfidence: line.line_code_confidence,
|
||||
needsReview: line.needs_review,
|
||||
isConfirmed: line.is_confirmed,
|
||||
codeModified: line.line_code_modified,
|
||||
cells,
|
||||
};
|
||||
});
|
||||
return { columns, rows };
|
||||
}
|
||||
|
||||
/** Уверенность — строка Decimal «0..100»; сравнение с 100 — это не сумма, float здесь безопасен. */
|
||||
export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean } {
|
||||
/**
|
||||
* Флаги ячейки: внимание — только по `needs_review` (уверенность решает ядро, сама по себе не подсвечивает);
|
||||
* подтверждённая ячейка не меняет уверенность, это отдельный флаг.
|
||||
*/
|
||||
export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean; confirmed: boolean } {
|
||||
if (!value) {
|
||||
return { attention: false, modified: false };
|
||||
return { attention: false, modified: false, confirmed: false };
|
||||
}
|
||||
const lowConfidence = value.confidence !== null && Number(value.confidence) < 100;
|
||||
return { attention: lowConfidence || value.is_recognized === false, modified: value.is_modified };
|
||||
return { attention: value.needs_review, modified: value.is_modified, confirmed: value.is_confirmed };
|
||||
}
|
||||
|
||||
export const cellKey = (rowKeyValue: string, columnId: string) => `${rowKeyValue}:${columnId}`;
|
||||
|
||||
@ -14,6 +14,7 @@ const doc = (over: Partial<DocumentBrief> = {}): DocumentBrief => ({
|
||||
counterparty_check: null,
|
||||
locator: null,
|
||||
comments_count: 0,
|
||||
issue_counts: { error: 0, info: 0 },
|
||||
...over,
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user