From d76cad121047566bc3b7483d43583fafebbe6141 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 22:15:41 +0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=E2=80=94=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0,=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=20=D1=81=D1=83=D0=BC=D0=BC=D1=8B,=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=20percentText=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I-2: sourceDiff = суммы сторон, отклонение и порог из агрегата; diffOf только для распределения. M-1: целая часть суммы не более 16 цифр («Слишком большая сумма»). M-6, M-7: комментарий, один percentText. Co-Authored-By: Claude Opus 5.5 --- .../CorrectionTable/CorrectionTable.tsx | 8 +++---- .../CorrectionTable/editorModel.test.ts | 24 ++++++++++++++++++- .../widgets/CorrectionTable/editorModel.ts | 19 ++++++++++++--- web/src/widgets/CorrectionTable/editorView.ts | 4 ++-- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/web/src/widgets/CorrectionTable/CorrectionTable.tsx b/web/src/widgets/CorrectionTable/CorrectionTable.tsx index 12f058d..3d92160 100644 --- a/web/src/widgets/CorrectionTable/CorrectionTable.tsx +++ b/web/src/widgets/CorrectionTable/CorrectionTable.tsx @@ -1,4 +1,4 @@ -// Таблица корректировок (только отображение): строки агрегатов, их строки ОСВ при раскрытии, нераспределённые, «Итого». +// Таблица корректировок: строки агрегатов, их строки ОСВ при раскрытии, нераспределённые, «Итого»; действия над строками — через `actions`. // Фильтры живут в родителе (CorrectionContent); здесь только рисуем и сообщаем о действиях через `actions`. import { DownOutlined, EditOutlined, MoreOutlined, RightOutlined, WarningOutlined } from '@ant-design/icons'; import { Button, Checkbox, Dropdown, Table, Tag, Tooltip } from 'antd'; @@ -8,11 +8,12 @@ import { type ReactNode, useMemo } from 'react'; import type { AllocationIn, CorrectionAggregateOut, CorrectionLineOut, CorrectionRuleOut } from '../../types/corrections'; import { HeaderFilter, SelectFilter, TextFilter } from './HeaderFilter'; import { COLUMN_DEFS, type ColKey } from './columns'; +import { percentText } from './editorView'; import { AGGREGATE_FLAG_HINT, allocationMenu, includeBody, menuBody } from './readOnly'; import { type DeviationFilter, type TableFilters, type TableRow, articleCodes, ruleLabel } from './tableModel'; import type { visibleTotals } from './tableModel'; -/** Действия над строками; их подключают задачи окна корректировки и решений по строкам. */ +/** Действия над строками: подключает CorrectionContent. */ export type RowActions = { onCorrect(a: CorrectionAggregateOut): void; onRevoke(a: CorrectionAggregateOut): void; @@ -55,9 +56,6 @@ const BG_CREDIT = '#f6f8fc'; const BG_UNALLOCATED = '#f2f4f7'; const BG_CHANGED = '#fff6e5'; -/** «394.27» → «394,27%»; строка не переводится в число. */ -const percentText = (p: string | null): string => (p === null ? '—' : `${p.replace('.', ',')}%`); - const showLine = (line: string): string => articleCodes(line).join(' / '); const sideSuffix = (row: TableRow) => { diff --git a/web/src/widgets/CorrectionTable/editorModel.test.ts b/web/src/widgets/CorrectionTable/editorModel.test.ts index 442000b..844e616 100644 --- a/web/src/widgets/CorrectionTable/editorModel.test.ts +++ b/web/src/widgets/CorrectionTable/editorModel.test.ts @@ -76,14 +76,36 @@ describe('validateLines', () => { }); }); +describe('предел суммы numeric(18,2)', () => { + const run = (amount: string) => validateLines({ debit: [{ line_code: '1230', amount }], credit: [], comment: '' }); + it('16 цифр до запятой — можно, 17 — «Слишком большая сумма»', () => { + expect(run('9999999999999999.99').ok).toBe(true); + const r = run('10000000000000000'); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.errors['debit.0.amount']).toBe('Слишком большая сумма'); + }); +}); + describe('liveControl', () => { - it('исходные суммы агрегата ЭПТ — ИЛС: процент 394.27, красный', () => { + it('исходный контроль берётся из ответа сервера: суммы сторон, отклонение, порог', () => { const a = findBy(table(), 'ЭПТ'); const c = liveControl(a, initialEditorState(a)); + expect(c.sourceDiff).toEqual({ + left: a.side_1.amount, + right: a.side_2.amount, + difference: a.deviation_amount, + percent: a.deviation_percent, + red: a.exceeds_threshold, + }); expect(c.sourceDiff.percent).toBe('394.27'); expect(c.sourceDiff.red).toBe(true); }); + it('сервер сказал «не превышает» — не красный, что бы ни вышло из сумм', () => { + const a = { ...findBy(table(), 'ЭПТ'), exceeds_threshold: false }; + expect(liveControl(a, initialEditorState(a)).sourceDiff.red).toBe(false); + }); + it('распределение Дт 1000000.00 и Кт 1000000.00 — разница 0.00, не красный', () => { const a = findBy(table(), 'ЭПТ'); const s: EditorState = { diff --git a/web/src/widgets/CorrectionTable/editorModel.ts b/web/src/widgets/CorrectionTable/editorModel.ts index b84e175..e206baa 100644 --- a/web/src/widgets/CorrectionTable/editorModel.ts +++ b/web/src/widgets/CorrectionTable/editorModel.ts @@ -44,11 +44,14 @@ function withCents(value: string): string { return `${int}.${frac.padEnd(2, '0')}`; } +const MAX_INTEGER_DIGITS = 16; + const AMOUNT_ERROR = { empty: 'Введите сумму', spaces: 'Сумма без пробелов', format: 'Сумма: не более двух знаков после запятой', negative: 'Сумма не может быть отрицательной', + tooBig: 'Слишком большая сумма', } as const; type AmountResult = { ok: true; value: string } | { ok: false; error: string }; @@ -61,7 +64,10 @@ function parseLineAmount(raw: string): AmountResult { const parsed = parseAmountInput(text); if (!parsed.ok || parsed.value === null) return { ok: false, error: AMOUNT_ERROR.format }; if (parsed.value.startsWith('-')) return { ok: false, error: AMOUNT_ERROR.negative }; - return { ok: true, value: withCents(parsed.value) }; + const value = withCents(parsed.value); + // numeric(18,2): не более 16 цифр до запятой + if (value.split('.')[0].replace(/^0+(?=\d)/, '').length > MAX_INTEGER_DIGITS) return { ok: false, error: AMOUNT_ERROR.tooBig }; + return { ok: true, value }; } function lineError(line: EditLine, prefix: string, errors: Record): CorrectionLineIn | null { @@ -104,7 +110,7 @@ function sideSum(lines: EditLine[]): string | null { ); } -/** Разница двух сумм и процент к меньшей; красный — процент ≥ 5 или одна сторона 0 при ненулевой другой. */ +/** Разница двух сумм и процент к меньшей (для распределения статей); красный — процент ≥ 5 или одна сторона 0 при ненулевой другой. */ function diffOf(left: string | null, right: string | null): Diff { const l = left ?? '0.00'; const r = right ?? '0.00'; @@ -120,7 +126,14 @@ function diffOf(left: string | null, right: string | null): Diff { /** Живой контроль окна: исходные суммы сторон агрегата и распределение статей из текущего состояния редактора. */ export function liveControl(a: CorrectionAggregateOut, s: EditorState): { sourceDiff: Diff; allocationDiff: Diff } { return { - sourceDiff: diffOf(a.side_1.amount, a.side_2.amount), + // исходный контроль — как посчитал сервер (те же цифры, что в таблице), распределение — живой пересчёт + sourceDiff: { + left: a.side_1.amount, + right: a.side_2.amount, + difference: a.deviation_amount, + percent: a.deviation_percent, + red: a.exceeds_threshold, + }, allocationDiff: diffOf(sideSum(s.debit), sideSum(s.credit)), }; } diff --git a/web/src/widgets/CorrectionTable/editorView.ts b/web/src/widgets/CorrectionTable/editorView.ts index fa46fb9..619b3ed 100644 --- a/web/src/widgets/CorrectionTable/editorView.ts +++ b/web/src/widgets/CorrectionTable/editorView.ts @@ -17,8 +17,8 @@ export const formatRub = (value: string | null): string => formatScaledAmount(va export const pickLabel = (counterparty: string, account: string, amount: string): string => `Выбрать: ${counterparty}, ${account}, ${formatRub(amount)}`; -/** «394.27» → «394,27%»; строка в число не переводится. */ -export const percentText = (p: string): string => `${p.replace('.', ',')}%`; +/** «394.27» → «394,27%»; строка в число не переводится; нет значения — «—». */ +export const percentText = (p: string | null): string => (p === null ? '—' : `${p.replace('.', ',')}%`); export type ControlText = { left: string; middle: string; right: string; red: boolean };