diff --git a/src/types/api.ts b/src/types/api.ts index 023da04..55026d8 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -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>; } 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'; diff --git a/src/utils/labels.test.ts b/src/utils/labels.test.ts index 13b2ce6..feef141 100644 --- a/src/utils/labels.test.ts +++ b/src/utils/labels.test.ts @@ -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('Информация'); + }); +}); diff --git a/src/utils/labels.ts b/src/utils/labels.ts index a3959d8..b27aab6 100644 --- a/src/utils/labels.ts +++ b/src/utils/labels.ts @@ -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 = { BALANCE: 'Бух. баланс', @@ -70,3 +70,14 @@ const MATCH_KIND: Record = { 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 = { + ok: 'Строка формы', + unreadable: 'Код не распознан', + missing: 'Кода нет', + detail: 'Расшифровка', +}; +export const lineStatusLabel = (s: LineStatus): string => LINE_STATUS[s] ?? s; + +const SEVERITY: Record = { error: 'Ошибка', info: 'Информация' }; +export const severityLabel = (s: Severity): string => SEVERITY[s] ?? s; diff --git a/src/widgets/DocumentReview/__fixtures__/statement-v2.json b/src/widgets/DocumentReview/__fixtures__/statement-v2.json new file mode 100644 index 0000000..8cab5b9 --- /dev/null +++ b/src/widgets/DocumentReview/__fixtures__/statement-v2.json @@ -0,0 +1,1006 @@ +{ + "document": { + "id": "7d0c2a64-2f1e-4b8a-9c1d-5e3f6a7b8c01", + "document_type": "BALANCE", + "status": "NEEDS_CHECK", + "period_year": 2025, + "period_quarter": "Q3", + "reporting_date": "2025-09-30", + "counterparty_check": null, + "locator": null, + "file_id": "3f8e1b2a-6c4d-4e5f-8a9b-0c1d2e3f4a5b", + "form_variant": "full" + }, + "edition": "fsbu4", + "header": { + "inn": { + "value": "7700000000", + "original_value": "7700000000", + "confidence": "100", + "verified": false, + "modified": false + }, + "report_date": { + "value": "2025-09-30", + "original_value": "30 сентября 2025 г.", + "confidence": "67", + "verified": false, + "modified": false + }, + "units": { + "value": "тыс. руб.", + "original_value": "тыс. руб.", + "confidence": "100", + "verified": null, + "modified": false + }, + "organization_name": "ООО Ромашка", + "organization_name_modified": false, + "okud": null, + "okpo": null, + "okopf": null, + "okfs": null, + "okei": "384", + "address": null, + "needs_review_fields": ["report_date"], + "source": { + "organization_name": { + "text": "ООО «Ромашкa»", + "confidence": "67.00" + }, + "report_date": { + "text": "30 сентября 2025 г.", + "confidence": "67.00" + } + } + }, + "columns": [ + { + "id": "c1", + "column_index": 2, + "label": "На 30 сентября 2025 г.", + "label_confidence": null, + "period_date": "2025-09-30", + "period_kind": "absolute", + "is_current": true + }, + { + "id": "c2", + "column_index": 3, + "label": null, + "label_confidence": null, + "period_date": null, + "period_kind": "none", + "is_current": null + } + ], + "lines": [ + { + "id": "l-1110", + "origin": "source", + "row_index": 0, + "source_ref": null, + "line_status": "ok", + "line_code": "1110", + "line_code_text": "1110", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Нематериальные активы", + "reference_name": "Нематериальные активы", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1110-c1", + "column_id": "c1", + "column_index": 2, + "amount": "10.00", + "string_value": null, + "text": "10", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1110-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1150", + "origin": "source", + "row_index": 1, + "source_ref": null, + "line_status": "ok", + "line_code": "1150", + "line_code_text": "1150", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Основные средства", + "reference_name": "Основные средства", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1150-c1", + "column_id": "c1", + "column_index": 2, + "amount": "70045.00", + "string_value": null, + "text": "70045", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1150-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-d2", + "origin": "source", + "row_index": 2, + "source_ref": null, + "line_status": "detail", + "line_code": null, + "line_code_text": null, + "line_code_confidence": null, + "line_code_modified": false, + "line_name": "в том числе: пригодные к использованию", + "reference_name": null, + "parent_code": null, + "parent_row_index": 1, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-d2-c1", + "column_id": "c1", + "column_index": 2, + "amount": "70045.00", + "string_value": null, + "text": "70045", + "confidence": "67", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-d2-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1100", + "origin": "source", + "row_index": 3, + "source_ref": null, + "line_status": "ok", + "line_code": "1100", + "line_code_text": "1100", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Итого по разделу I", + "reference_name": "Итого по разделу I", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1100-c1", + "column_id": "c1", + "column_index": 2, + "amount": "70055.00", + "string_value": null, + "text": "70055", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1100-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1210", + "origin": "source", + "row_index": 4, + "source_ref": null, + "line_status": "ok", + "line_code": "1210", + "line_code_text": "1210", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Запасы", + "reference_name": "Запасы", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1210-c1", + "column_id": "c1", + "column_index": 2, + "amount": "100.00", + "string_value": null, + "text": "100", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1210-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1230", + "origin": "source", + "row_index": 5, + "source_ref": null, + "line_status": "ok", + "line_code": "1230", + "line_code_text": "1230", + "line_code_confidence": "50", + "line_code_modified": false, + "line_name": "Дебиторская задолжен-ность", + "reference_name": "Дебиторская задолженность", + "parent_code": null, + "parent_row_index": null, + "needs_review": true, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1230-c1", + "column_id": "c1", + "column_index": 2, + "amount": "428311.00", + "string_value": null, + "text": "42831 1", + "confidence": "50", + "is_recognized": false, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1230-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1240", + "origin": "source", + "row_index": 6, + "source_ref": null, + "line_status": "ok", + "line_code": "1240", + "line_code_text": "1240", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Финансовые вложения", + "reference_name": "Финансовые вложения", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1240-c1", + "column_id": "c1", + "column_index": 2, + "amount": "5.00", + "string_value": null, + "text": "5", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1240-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-u7", + "origin": "source", + "row_index": 7, + "source_ref": null, + "line_status": "unreadable", + "line_code": null, + "line_code_text": "ИЧ,250", + "line_code_confidence": null, + "line_code_modified": false, + "line_name": "Денежные средства", + "reference_name": null, + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-u7-c1", + "column_id": "c1", + "column_index": 2, + "amount": "50.00", + "string_value": null, + "text": "50", + "confidence": "67", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": true, + "is_confirmed": false + }, + { + "id": "v-l-u7-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-m8", + "origin": "source", + "row_index": 8, + "source_ref": null, + "line_status": "missing", + "line_code": null, + "line_code_text": null, + "line_code_confidence": null, + "line_code_modified": false, + "line_name": "Прочие оборотные активы", + "reference_name": null, + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-m8-c1", + "column_id": "c1", + "column_index": 2, + "amount": "7.00", + "string_value": null, + "text": "7", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-m8-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1200", + "origin": "source", + "row_index": 9, + "source_ref": null, + "line_status": "ok", + "line_code": "1200", + "line_code_text": "1200", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Итого по разделу II", + "reference_name": "Итого по разделу II", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1200-c1", + "column_id": "c1", + "column_index": 2, + "amount": "200.00", + "string_value": null, + "text": "200", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": true, + "is_confirmed": false + }, + { + "id": "v-l-1200-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1600", + "origin": "source", + "row_index": 10, + "source_ref": null, + "line_status": "ok", + "line_code": "1600", + "line_code_text": "1600", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "БАЛАНС", + "reference_name": "БАЛАНС", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1600-c1", + "column_id": "c1", + "column_index": 2, + "amount": "70255.00", + "string_value": null, + "text": "70255", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1600-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1370a", + "origin": "source", + "row_index": 11, + "source_ref": null, + "line_status": "ok", + "line_code": "1370", + "line_code_text": "1370", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "Нераспределенная прибыль", + "reference_name": "Нераспределенная прибыль", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1370a-c1", + "column_id": "c1", + "column_index": 2, + "amount": "500.00", + "string_value": null, + "text": "500", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1370a-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-1370b", + "origin": "source", + "row_index": 12, + "source_ref": null, + "line_status": "ok", + "line_code": "1370", + "line_code_text": "1370", + "line_code_confidence": "100", + "line_code_modified": false, + "line_name": "в т.ч. прошлых лет", + "reference_name": "Нераспределенная прибыль", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-1370b-c1", + "column_id": "c1", + "column_index": 2, + "amount": "300.00", + "string_value": null, + "text": "300", + "confidence": "100", + "is_recognized": true, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-1370b-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-d13", + "origin": "source", + "row_index": 13, + "source_ref": null, + "line_status": "detail", + "line_code": null, + "line_code_text": null, + "line_code_confidence": null, + "line_code_modified": false, + "line_name": "справочно", + "reference_name": null, + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-l-d13-c1", + "column_id": "c1", + "column_index": 2, + "amount": null, + "string_value": "211(1 |", + "text": "211(1 |", + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-l-d13-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": null, + "origin": "template", + "row_index": null, + "source_ref": null, + "line_status": "ok", + "line_code": "1250", + "line_code_text": null, + "line_code_confidence": null, + "line_code_modified": false, + "line_name": null, + "reference_name": "Денежные средства и денежные эквиваленты", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": null, + "column_id": "c1", + "column_index": 2, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": null, + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + }, + { + "id": "l-t1170", + "origin": "template", + "row_index": null, + "source_ref": null, + "line_status": "ok", + "line_code": "1170", + "line_code_text": null, + "line_code_confidence": null, + "line_code_modified": false, + "line_name": null, + "reference_name": "Финансовые вложения", + "parent_code": null, + "parent_row_index": null, + "needs_review": false, + "is_confirmed": false, + "values": [ + { + "id": "v-t1170-c1", + "column_id": "c1", + "column_index": 2, + "amount": "3.00", + "string_value": null, + "text": "3", + "confidence": null, + "is_recognized": true, + "is_modified": true, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + }, + { + "id": "v-t1170-c2", + "column_id": "c2", + "column_index": 3, + "amount": null, + "string_value": null, + "text": null, + "confidence": null, + "is_recognized": null, + "is_modified": false, + "original_amount": null, + "needs_review": false, + "is_confirmed": false + } + ] + } + ], + "checks": { + "failed": [ + { + "column_index": 2, + "relation": "1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260", + "expected": "155", + "actual": "200", + "codes": ["RELATION_1200"], + "line_ids": ["l-1200", "l-1210", "l-1230", "l-1240"] + } + ], + "soft_failed": [], + "cross_form": null, + "accepted": null + }, + "issues": [ + { + "id": "i1", + "kind": "warning", + "code": "ROW_SPLIT", + "severity": "info", + "message": "Строка 1370 разбита на две строки файла", + "line_ids": ["l-1370a", "l-1370b"], + "column_id": null, + "column_index": null, + "field": null, + "source": "service", + "relation": null, + "codes": [], + "expected": null, + "actual": null + }, + { + "id": "i2", + "kind": "warning", + "code": "COLUMN_WITHOUT_DATE", + "severity": "error", + "message": "Колонка без даты периода", + "line_ids": [], + "column_id": "c2", + "column_index": null, + "field": null, + "source": "service", + "relation": null, + "codes": [], + "expected": null, + "actual": null + }, + { + "id": "i3", + "kind": "warning", + "code": "ORGANIZATION_UNCERTAIN", + "severity": "info", + "message": "Наименование организации распознано неуверенно", + "line_ids": [], + "column_id": null, + "column_index": null, + "field": "organization_name", + "source": "service", + "relation": null, + "codes": [], + "expected": null, + "actual": null + }, + { + "id": "i4", + "kind": "warning", + "code": "COLUMN_SKIPPED", + "severity": "info", + "message": "Колонка 5 пропущена", + "line_ids": [], + "column_id": null, + "column_index": 5, + "field": null, + "source": "service", + "relation": null, + "codes": [], + "expected": null, + "actual": null + }, + { + "id": "i5", + "kind": "warning", + "code": "REPORT_DATE_UNCERTAIN", + "severity": "error", + "message": "Дата отчёта распознана неуверенно", + "line_ids": [], + "column_id": null, + "column_index": null, + "field": "report_date", + "source": "service", + "relation": null, + "codes": [], + "expected": null, + "actual": null + }, + { + "id": "i6", + "kind": "relation", + "code": "RELATION_FAILED", + "severity": "error", + "message": "Не сходится соотношение строки 1200", + "line_ids": ["l-1200", "l-1210", "l-1230", "l-1240"], + "column_id": "c1", + "column_index": 2, + "field": null, + "source": "service", + "relation": "1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260", + "codes": ["RELATION_1200"], + "expected": "155", + "actual": "200" + }, + { + "id": "i7", + "kind": "warning", + "code": "NAME_MISMATCH", + "severity": "info", + "message": "Наименование строки 1230 отличается от справочного", + "line_ids": ["l-1230"], + "column_id": null, + "column_index": null, + "field": null, + "source": "core", + "relation": null, + "codes": [], + "expected": null, + "actual": null + } + ], + "edition_lines": [ + { + "code": "1110", + "name": "Нематериальные активы" + }, + { + "code": "1150", + "name": "Основные средства" + }, + { + "code": "1170", + "name": "Финансовые вложения" + }, + { + "code": "1100", + "name": "Итого по разделу I" + }, + { + "code": "1210", + "name": "Запасы" + }, + { + "code": "1230", + "name": "Дебиторская задолженность" + }, + { + "code": "1240", + "name": "Финансовые вложения" + }, + { + "code": "1250", + "name": "Денежные средства и денежные эквиваленты" + }, + { + "code": "1200", + "name": "Итого по разделу II" + }, + { + "code": "1600", + "name": "БАЛАНС" + }, + { + "code": "1370", + "name": "Нераспределенная прибыль (непокрытый убыток)" + } + ] +} diff --git a/src/widgets/DocumentReview/formLayout.test.ts b/src/widgets/DocumentReview/formLayout.test.ts index 8fedfce..a73a408 100644 --- a/src/widgets/DocumentReview/formLayout.test.ts +++ b/src/widgets/DocumentReview/formLayout.test.ts @@ -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) => diff --git a/src/widgets/DocumentReview/statementGrid.test.ts b/src/widgets/DocumentReview/statementGrid.test.ts index b0a2350..943b52e 100644 --- a/src/widgets/DocumentReview/statementGrid.test.ts +++ b/src/widgets/DocumentReview/statementGrid.test.ts @@ -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 = [ diff --git a/src/widgets/DocumentReview/statementGrid.ts b/src/widgets/DocumentReview/statementGrid.ts index 8819844..0638112 100644 --- a/src/widgets/DocumentReview/statementGrid.ts +++ b/src/widgets/DocumentReview/statementGrid.ts @@ -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; }; @@ -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}`; diff --git a/src/widgets/FilesDrawer/documentRows.test.ts b/src/widgets/FilesDrawer/documentRows.test.ts index 9b20410..5983204 100644 --- a/src/widgets/FilesDrawer/documentRows.test.ts +++ b/src/widgets/FilesDrawer/documentRows.test.ts @@ -14,6 +14,7 @@ const doc = (over: Partial = {}): DocumentBrief => ({ counterparty_check: null, locator: null, comments_count: 0, + issue_counts: { error: 0, info: 0 }, ...over, });