arm-web/src/widgets/DocumentReview/osvRows.test.ts
Andrey Epifancev c3fcac2efd feat(osv): «Просмотр ОСВ» — плоская таблица вместо дерева (#12)
- Строка = аналитика нижнего уровня (контрагент, объект ОС); строки счетов и
  субсчетов не показываются, договоры (CONTRACT) — в строке контрагента, как в
  сводной ОСВ (§3); субсчёт/подразделение без нижних строк с суммами — строкой с
  пустой аналитикой.
- Колонки: Субсчёт · Подразделение (если уровень есть в файле) · Контрагент /
  аналитика · ИНН · 6 сумм; закреплённая строка «Итого по документу» = сумма
  корневых строк счёта (= «Итого» файла 1С).
- Поиск по контрагенту/ИНН, фильтр по субсчёту, «Строк: N» — на клиенте.
- flattenEntries (osvRows.ts) и точное sumAmounts (копейками, BigInt) с тестами;
  EntryOut.group_type — как в схеме arm-core; entryTree удалён.
- Пустой документ: «В документе нет строк» вместо «No data».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:22:16 +04:00

119 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest';
import type { EntryOut, OsvGroupType } from '../../types/api';
import { flattenEntries } from './osvRows';
let row = 0;
const e = (
id: string,
parent: string | null,
groupType: OsvGroupType,
value: string,
amounts: Partial<Pick<EntryOut, 'opening_debit' | 'turnover_debit' | 'closing_credit'>> = {},
extra: Partial<EntryOut> = {},
): EntryOut => ({
id,
row_number: ++row,
level: null,
parent_entry_id: parent,
row_value: value,
group_type: groupType,
group_name: null,
account_code: null,
counterparty_name: null,
counterparty_inn: null,
opening_debit: null,
opening_credit: null,
turnover_debit: null,
turnover_credit: null,
closing_debit: null,
closing_credit: null,
...amounts,
...extra,
});
describe('flattenEntries', () => {
it('строки — только аналитика нижнего уровня с субсчётом', () => {
const { rows } = flattenEntries([
e('62', null, 'ACCOUNT', '62', { opening_debit: '10' }, { account_code: '62' }),
e('62.01', '62', 'ACCOUNT', '62.01', {}, { account_code: '62.01' }),
e('a', '62.01', 'COUNTERPARTY', 'ООО А', { opening_debit: '7' }, { account_code: '62.01', counterparty_inn: '123' }),
e('b', '62.01', 'COUNTERPARTY', 'ООО Б', { opening_debit: '3' }, { account_code: '62.01' }),
]);
expect(rows.map((r) => [r.account, r.analytics, r.inn, r.opening_debit])).toEqual([
['62.01', 'ООО А', '123', '7'],
['62.01', 'ООО Б', null, '3'],
]);
});
it('подразделение — из родительской строки DEPARTMENT', () => {
const { rows, hasDepartments } = flattenEntries([
e('62', null, 'ACCOUNT', '62', {}, { account_code: '62' }),
e('62.01', '62', 'ACCOUNT', '62.01', {}, { account_code: '62.01' }),
e('d', '62.01', 'DEPARTMENT', 'БП Аренда', {}, { account_code: '62.01' }),
e('a', 'd', 'COUNTERPARTY', 'РБ', {}, { account_code: '62.01' }),
]);
expect(hasDepartments).toBe(true);
expect(rows.map((r) => [r.department, r.analytics])).toEqual([['БП Аренда', 'РБ']]);
});
it('договоры не отдельными строками: остаётся строка контрагента', () => {
const { rows } = flattenEntries([
e('58', null, 'ACCOUNT', '58.03', {}, { account_code: '58.03' }),
e('a', '58', 'COUNTERPARTY', 'РБ ТК', { opening_debit: '100' }, { account_code: '58.03' }),
e('c1', 'a', 'CONTRACT', 'Договор 1', { opening_debit: '60' }, { account_code: '58.03' }),
e('c2', 'a', 'CONTRACT', 'Договор 2', { opening_debit: '40' }, { account_code: '58.03' }),
]);
expect(rows.map((r) => [r.analytics, r.opening_debit])).toEqual([['РБ ТК', '100']]);
});
it('объекты ОС (OTHER) — строки аналитики', () => {
const { rows } = flattenEntries([
e('01', null, 'ACCOUNT', '01', {}, { account_code: '01' }),
e('o', '01', 'OTHER', 'Автопогрузчик', { opening_debit: '5' }, { account_code: '01.01' }),
]);
expect(rows.map((r) => [r.account, r.analytics])).toEqual([['01.01', 'Автопогрузчик']]);
});
it('субсчёт и подразделение без нижних строк — строка с пустой аналитикой; пустые без сумм — нет', () => {
const { rows } = flattenEntries([
e('62', null, 'ACCOUNT', '62', {}, { account_code: '62' }),
e('62.21', '62', 'ACCOUNT', '62.21', { closing_credit: '128041.39' }, { account_code: '62.21' }),
e('62.22', '62', 'ACCOUNT', '62.22', {}, { account_code: '62.22' }),
e('d', '62', 'DEPARTMENT', 'Опт', { turnover_debit: '1' }, { account_code: '62' }),
]);
expect(rows.map((r) => [r.account, r.department, r.analytics])).toEqual([
['62.21', null, null],
['62', 'Опт', null],
]);
});
it('итог — сумма корневых строк, а не листьев (развёрнутое сальдо 1С)', () => {
const { total } = flattenEntries([
e('60', null, 'ACCOUNT', '60', { closing_credit: '100.10' }, { account_code: '60' }),
e('a', '60', 'COUNTERPARTY', 'А', { closing_credit: '150.10' }, { account_code: '60' }),
e('76', null, 'ACCOUNT', '76', { closing_credit: '0.20' }, { account_code: '76' }),
]);
expect(total.closing_credit).toBe('100.30');
expect(total.opening_debit).toBeNull();
});
it('субсчета для фильтра — уникальные, в натуральном порядке', () => {
const { accounts } = flattenEntries([
e('62', null, 'ACCOUNT', '62', {}, { account_code: '62' }),
e('b', '62', 'COUNTERPARTY', 'Б', {}, { account_code: '62.10' }),
e('a', '62', 'COUNTERPARTY', 'А', {}, { account_code: '62.02' }),
e('c', '62', 'COUNTERPARTY', 'В', {}, { account_code: '62.02' }),
]);
expect(accounts).toEqual(['62.02', '62.10']);
});
it('пустой документ', () => {
expect(flattenEntries([])).toEqual({
rows: [],
total: expect.objectContaining({ opening_debit: null }),
accounts: [],
hasDepartments: false,
});
});
});