fix(web): окно корректировки — исходный контроль из ответа сервера, предел суммы, один percentText (#206)
I-2: sourceDiff = суммы сторон, отклонение и порог из агрегата; diffOf только для распределения. M-1: целая часть суммы не более 16 цифр («Слишком большая сумма»). M-6, M-7: комментарий, один percentText. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f4a4f01c28
commit
d76cad1210
@ -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) => {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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<string, string>): 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)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user