Andrey Epifancev 52b813617a fix(web): синхронизация встроенного фронта с arm-web@test (de4e477) (#146)
Перенесён диапазон arm-web a3c0010..de4e477 (1 коммитов, 4 файлов набора
синхронизации) скриптом docs/scripts/sync-web.sh. После переноса web/ и
arm-web@test идентичны по набору. Коммиты arm-web:
- e202711 fix(DocumentReview): просмотр ОСВ как в 1С — строки счетов и субсчетов, без строк-показателей (#51, AURORA-1702, AURORA-1703)

Co-Authored-By: sync-web.sh <noreply@aepif.ru>
2026-10-21 00:08:27 +04:00

287 lines
14 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 { filterRows, 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,
data_type: 'BU',
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('все строки в порядке файла: счёт и субсчёт — группы, вложенные — с отступом по уровню (AURORA-1702)', () => {
const { rows } = flattenEntries([
e('62', null, 'ACCOUNT', '62', { opening_debit: '10' }, { account_code: '62' }),
e('62.01', '62', 'ACCOUNT', '62.01', { opening_debit: '10' }, { 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, r.depth, r.isGroup])).toEqual([
['62', '62', null, '10', 0, true],
['62.01', '62.01', null, '10', 1, true],
['62.01', 'ООО А', '123', '7', 2, false],
['62.01', 'ООО Б', null, '3', 2, false],
]);
});
it('группа без сумм всё равно выводится — это заголовок вложенных строк', () => {
const { rows } = flattenEntries([
e('62', null, 'ACCOUNT', '62', {}, { account_code: '62' }),
e('a', '62', 'COUNTERPARTY', 'ООО А', { opening_debit: '7' }, { account_code: '62' }),
]);
expect(rows.map((r) => [r.analytics, r.isGroup])).toEqual([
['62', true],
['ООО А', false],
]);
});
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, r.isGroup])).toEqual([
[null, '62', true],
[null, '62.01', true],
['БП Аренда', null, true],
['БП Аренда', 'РБ', false],
]);
});
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, r.isGroup])).toEqual([
['58.03', null, true],
['РБ ТК', '100', false],
]);
});
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.filter((r) => !r.isGroup).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, r.isGroup])).toEqual([
['62', null, '62', true],
['62.21', null, '62.21', false],
['62', 'Опт', null, false],
]);
});
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', '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: строки-показатели «Кол.», «НУ», «Вал.» (AURORA-1703)', () => {
const ind = (id: string, parent: string, label: string, dataType: EntryOut['data_type'], amount: string) =>
e(id, parent, 'OTHER', label, { opening_debit: amount }, { account_code: '41.01', data_type: dataType });
it('строка БУ остаётся строкой с рублями, показатели под ней не выводятся', () => {
const { rows } = flattenEntries([
e('41', null, 'ACCOUNT', '41.01', { opening_debit: '1000.00' }, { account_code: '41.01' }),
e('n', '41', 'OTHER', 'Мука', { opening_debit: '1000.00' }, { account_code: '41.01', group_name: 'Номенклатура' }),
ind('q', 'n', 'Кол.', 'QUANTITY', '25.000'),
ind('t', 'n', 'НУ', 'NU', '990.00'),
ind('c', 'n', 'Вал.', 'CURRENCY', '11.00'),
ind('p', 'n', 'ПР', null, '10.00'),
]);
expect(rows.map((r) => [r.analytics, r.opening_debit, r.isGroup])).toEqual([
['41.01', '1000.00', true],
['Мука', '1000.00', false],
]);
});
it('показатель под строкой счёта не делает счёт группой без вложенных строк', () => {
const { rows } = flattenEntries([
e('10', null, 'ACCOUNT', '10.01', { opening_debit: '5.00' }, { account_code: '10.01' }),
ind('q', '10', 'Кол.', 'QUANTITY', '3.000'),
]);
expect(rows.map((r) => [r.analytics, r.opening_debit, r.isGroup])).toEqual([['10.01', '5.00', false]]);
});
it('показатели не определяют вид аналитики', () => {
const t = flattenEntries([
e('41', null, 'ACCOUNT', '41.01', {}, { account_code: '41.01' }),
e('n', '41', 'OTHER', 'Мука', { opening_debit: '1' }, { group_name: 'Номенклатура' }),
ind('q', 'n', 'Кол.', 'QUANTITY', '1'),
]);
expect([t.analyticsTitle, t.analyticsHint]).toEqual(['Номенклатура', null]);
});
it('документ целиком в НУ (вид из шапки) — строки обычные, не показатели', () => {
const nu = { data_type: 'NU' as const };
const { rows } = flattenEntries([
e('68', null, 'ACCOUNT', '68', { opening_debit: '5' }, { account_code: '68', ...nu }),
e('a', '68', 'COUNTERPARTY', 'ИФНС', { opening_debit: '5' }, { account_code: '68', ...nu }),
]);
expect(rows.map((r) => r.analytics)).toEqual(['68', 'ИФНС']);
});
it('строка «БУ» под счётом с видом из шапки SUM — обычная строка, не показатель', () => {
const { rows } = flattenEntries([
e('41', null, 'ACCOUNT', '41.01', { opening_debit: '5' }, { account_code: '41.01', data_type: 'SUM' }),
e('n', '41', 'OTHER', 'Мука', { opening_debit: '5' }, { account_code: '41.01', data_type: 'BU' }),
]);
expect(rows.map((r) => r.analytics)).toEqual(['41.01', 'Мука']);
});
it('старые документы без вида строк (data_type null) — дерево как раньше', () => {
const none = { data_type: null };
const { rows } = flattenEntries([
e('60', null, 'ACCOUNT', '60', { opening_debit: '5' }, { account_code: '60', ...none }),
e('o', '60', 'OTHER', 'Прочее', { opening_debit: '5' }, { account_code: '60', ...none }),
]);
expect(rows.map((r) => [r.analytics, r.isGroup])).toEqual([
['60', true],
['Прочее', 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]);
});
});
describe('filterRows', () => {
const { rows } = flattenEntries([
e('62', null, 'ACCOUNT', '62', { opening_debit: '10' }, { account_code: '62' }),
e('62.01', '62', 'ACCOUNT', '62.01', { opening_debit: '7' }, { account_code: '62.01' }),
e('a', '62.01', 'COUNTERPARTY', 'ООО Ромашка', { opening_debit: '7' }, { account_code: '62.01', counterparty_inn: '7701' }),
e('62.02', '62', 'ACCOUNT', '62.02', { opening_debit: '3' }, { account_code: '62.02' }),
e('b', '62.02', 'COUNTERPARTY', 'ООО Лютик', { opening_debit: '3' }, { account_code: '62.02' }),
e('620', null, 'ACCOUNT', '620', { opening_debit: '1' }, { account_code: '620' }),
]);
const keys = (list: typeof rows) => list.map((r) => r.key);
it('без фильтров — все строки', () => {
expect(keys(filterRows(rows, '', undefined))).toEqual(['62', '62.01', 'a', '62.02', 'b', '620']);
});
it('субсчёт — его строка и вложенные', () => {
expect(keys(filterRows(rows, '', '62.01'))).toEqual(['62.01', 'a']);
});
it('счёт — он сам и все субсчета, но не счёт с тем же началом кода', () => {
expect(keys(filterRows(rows, '', '62'))).toEqual(['62', '62.01', 'a', '62.02', 'b']);
});
it('поиск скрывает строки-группы и ищет по аналитике без учёта регистра и по ИНН', () => {
expect(keys(filterRows(rows, 'ромаш', undefined))).toEqual(['a']);
expect(keys(filterRows(rows, ' 7701 ', undefined))).toEqual(['a']);
expect(keys(filterRows(rows, '62', undefined))).toEqual(['620']);
});
});