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> = {}, extra: Partial = {}, ): 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']); }); });