Заголовок колонки считается по видам строк-листьев (osvRows.analyticsHeading): «Контрагент», название вида из файла или «Аналитика» с подсказкой. Колонка ИНН и подсказка поиска — только при контрагентах; labelSpan и ширина scroll.x зависят от числа колонок. Требование — в specs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
7.8 KiB
TypeScript
168 lines
7.8 KiB
TypeScript
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,
|
||
analyticsTitle: 'Аналитика',
|
||
analyticsHint: null,
|
||
hasCounterparties: false,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('flattenEntries: вид аналитики', () => {
|
||
const acc = e('60', null, 'ACCOUNT', '60', {}, { account_code: '60' });
|
||
const cp = (id: string) => e(id, '60', 'COUNTERPARTY', `ООО ${id}`, { opening_debit: '1' }, { group_name: 'Контрагенты' });
|
||
const os = (id: string, name: string | null = 'Основные средства') =>
|
||
e(id, '60', 'OTHER', `Объект ${id}`, { opening_debit: '1' }, { group_name: name });
|
||
|
||
it('только контрагенты — «Контрагент», ИНН и поиск по ИНН нужны', () => {
|
||
const t = flattenEntries([acc, cp('a'), cp('b')]);
|
||
expect([t.analyticsTitle, t.analyticsHint, t.hasCounterparties]).toEqual(['Контрагент', null, true]);
|
||
});
|
||
|
||
it('договор под контрагентом вид не меняет', () => {
|
||
const t = flattenEntries([acc, cp('a'), e('d', 'a', 'CONTRACT', 'Договор 1', { opening_debit: '1' }, { group_name: 'Договоры' })]);
|
||
expect([t.analyticsTitle, t.hasCounterparties]).toEqual(['Контрагент', true]);
|
||
});
|
||
|
||
it('только основные средства — название вида из файла, без ИНН', () => {
|
||
const t = flattenEntries([acc, os('a'), os('b')]);
|
||
expect([t.analyticsTitle, t.analyticsHint, t.hasCounterparties]).toEqual(['Основные средства', null, false]);
|
||
});
|
||
|
||
it('смесь видов — «Аналитика» и перечень видов в подсказке', () => {
|
||
const t = flattenEntries([acc, cp('a'), os('b')]);
|
||
expect([t.analyticsTitle, t.analyticsHint, t.hasCounterparties]).toEqual([
|
||
'Аналитика',
|
||
'Виды аналитики: Контрагенты, Основные средства',
|
||
true,
|
||
]);
|
||
});
|
||
|
||
it('два разных вида не-контрагентов — тоже «Аналитика»', () => {
|
||
const t = flattenEntries([acc, os('a'), os('b', 'Номенклатура')]);
|
||
expect([t.analyticsTitle, t.analyticsHint]).toEqual(['Аналитика', 'Виды аналитики: Основные средства, Номенклатура']);
|
||
});
|
||
|
||
it('вид без названия — «Аналитика»', () => {
|
||
const t = flattenEntries([acc, os('a', null)]);
|
||
expect([t.analyticsTitle, t.hasCounterparties]).toEqual(['Аналитика', false]);
|
||
});
|
||
|
||
it('структурные строки вид не определяют; без листьев аналитики — «Аналитика» без ИНН', () => {
|
||
const t = flattenEntries([e('60', null, 'ACCOUNT', '60', { opening_debit: '5' }, { account_code: '60' })]);
|
||
expect([t.analyticsTitle, t.analyticsHint, t.hasCounterparties]).toEqual(['Аналитика', null, false]);
|
||
});
|
||
});
|