diff --git a/web/src/components/Labeled.tsx b/web/src/components/Labeled.tsx
new file mode 100644
index 0000000..b1e198c
--- /dev/null
+++ b/web/src/components/Labeled.tsx
@@ -0,0 +1,10 @@
+import { Space } from 'antd';
+import type { ReactNode } from 'react';
+
+/** Поле с серой подписью сверху — для фильтров и настроек над таблицами (селекты отчётности, чек-листа). */
+export const Labeled = ({ label, children }: { label: string; children: ReactNode }) => (
+
+ {label}
+ {children}
+
+);
diff --git a/web/src/services/api/consolidations.ts b/web/src/services/api/consolidations.ts
index 003e2ba..97a968b 100644
--- a/web/src/services/api/consolidations.ts
+++ b/web/src/services/api/consolidations.ts
@@ -9,7 +9,12 @@ export const getConsolidations = (groupId: string, in_work_only: boolean) =>
api.get(`/company-groups/${groupId}/consolidations`, { in_work_only });
export const getVersionCard = (versionId: string) => api.get(`/consolidations/versions/${versionId}`);
export const createConsolidation = (body: ConsolidationCreate) => api.post('/consolidations', body);
-export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`);
+/** CSV чек-листа; `documents` — отбор колонок (`BALANCE,INCOME,OSV:01`), не задан — вся матрица. */
+export const downloadChecklistCsv = (versionId: string, documents?: string) => {
+ const params = new URLSearchParams({ format: 'csv' });
+ if (documents) params.set('documents', documents);
+ return apiFetchBlob(`/consolidations/versions/${versionId}/checklist?${params}`);
+};
export const getChecklist = (versionId: string) => api.get(`/consolidations/versions/${versionId}/checklist`);
/** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */
export const lockVersion = (versionId: string, reason?: string | null) =>
diff --git a/web/src/widgets/ConsoleLegalEntities/ChecklistDocumentsFilter.tsx b/web/src/widgets/ConsoleLegalEntities/ChecklistDocumentsFilter.tsx
new file mode 100644
index 0000000..1893eec
--- /dev/null
+++ b/web/src/widgets/ConsoleLegalEntities/ChecklistDocumentsFilter.tsx
@@ -0,0 +1,49 @@
+// Фильтр «Документы» окна чек-листа: селект как в отчётности (подпись сверху, underlined), в списке — чекбоксы.
+import { Checkbox, Select } from 'antd';
+import { Labeled } from '../../components/Labeled';
+import { documentFilterLabel, toggleAllDocuments, toggleDocument } from './checklist_utils';
+
+type Props = {
+ options: { key: string; title: string }[];
+ value: string[];
+ onChange: (next: string[]) => void;
+};
+
+const itemStyle: React.CSSProperties = { display: 'flex', padding: '5px 12px', borderRadius: 4, cursor: 'pointer' };
+
+export function ChecklistDocumentsFilter({ options, value, onChange }: Props) {
+ const allKeys = options.map((o) => o.key);
+ const isAll = value.length === allKeys.length;
+
+ return (
+
+
+ );
+}
diff --git a/web/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx b/web/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx
index cf6c7f5..8cbc7e0 100644
--- a/web/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx
+++ b/web/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx
@@ -5,25 +5,40 @@ import { ApiError } from '../../api/client';
import { AppModal } from '../../components/AppModal/AppModal';
import { getChecklist } from '../../services/api/consolidations';
import type { ChecklistBackendShape, IVersionCard } from '../../types/consolidations/version_members';
-import { buildChecklistColumns, buildChecklistRows, getMarksItems } from './checklist_utils';
+import { ChecklistDocumentsFilter } from './ChecklistDocumentsFilter';
+import {
+ buildChecklistColumns,
+ buildChecklistRows,
+ buildDocumentFilterOptions,
+ documentsQueryParam,
+ getMarksItems,
+} from './checklist_utils';
type ChecklistProps = {
open: boolean;
- onDownload: () => void;
+ onDownload: (documents?: string) => void;
consolidationData: IVersionCard;
onClose: () => void;
};
export default function ChecklistModal({ open, onDownload, consolidationData, onClose }: ChecklistProps) {
const [checklistData, setChecklistData] = useState(null);
+ const [selectedDocuments, setSelectedDocuments] = useState([]);
const rows = useMemo(() => (checklistData ? buildChecklistRows(checklistData) : []), [checklistData]);
- const columns = useMemo(() => (checklistData ? buildChecklistColumns(checklistData) : []), [checklistData]);
+ const columns = useMemo(
+ () => (checklistData ? buildChecklistColumns(checklistData, selectedDocuments) : []),
+ [checklistData, selectedDocuments],
+ );
+ const documentOptions = useMemo(() => (checklistData ? buildDocumentFilterOptions(checklistData) : []), [checklistData]);
useEffect(() => {
if (!consolidationData.id || !open) return;
getChecklist(consolidationData.id)
- .then(setChecklistData)
+ .then((data) => {
+ setChecklistData(data);
+ setSelectedDocuments(buildDocumentFilterOptions(data).map((o) => o.key));
+ })
.catch((e: unknown) => message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить чек-лист'));
}, [open, consolidationData.id]);
@@ -33,15 +48,21 @@ export default function ChecklistModal({ open, onDownload, consolidationData, on
open={open}
cancelText='Отмена'
onCancel={onClose}
- title='Чеклист'
+ title='Чек-лист загруженных документов'
destroyOnHidden
onOk={() => {
- onDownload();
+ onDownload(
+ documentsQueryParam(
+ selectedDocuments,
+ documentOptions.map((o) => o.key),
+ ),
+ );
onClose();
}}
- okText='Скачать чек-лист'
- okButtonProps={{ icon: }}>
+ okText='Скачать'
+ okButtonProps={{ icon: , disabled: selectedDocuments.length === 0 }}>
diff --git a/web/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx b/web/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx
index 39ae782..9b7440f 100644
--- a/web/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx
+++ b/web/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx
@@ -39,10 +39,10 @@ export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawe
}, [consolidation_id, reloadKey]);
const rawRows = useMemo(() => normalizeData(consolidationData), [consolidationData]);
- const downloadChecklist = async () => {
+ const downloadChecklist = async (documents?: string) => {
if (!consolidationData) return;
try {
- const { blob, filename } = await downloadChecklistCsv(consolidationData.id);
+ const { blob, filename } = await downloadChecklistCsv(consolidationData.id, documents);
const card = consolidationData;
saveBlob(blob, filename ?? checklistFilename(card.company_group.name, card.year, card.quarter));
} catch (e: unknown) {
diff --git a/web/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts b/web/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts
index fc67fd6..53005d4 100644
--- a/web/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts
+++ b/web/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts
@@ -1,6 +1,14 @@
import { describe, expect, it } from 'vitest';
import type { ChecklistBackendShape } from '../../types/consolidations/version_members';
-import { buildChecklistRows } from './checklist_utils';
+import {
+ buildChecklistColumns,
+ buildChecklistRows,
+ buildDocumentFilterOptions,
+ documentFilterLabel,
+ documentsQueryParam,
+ toggleAllDocuments,
+ toggleDocument,
+} from './checklist_utils';
const checklist: ChecklistBackendShape = {
rules: [],
@@ -37,3 +45,96 @@ describe('buildChecklistRows', () => {
expect(rows[0]).toMatchObject({ balance: 'MISSING', ofr: 'MISSING', osv: {} });
});
});
+
+const twoOsv: ChecklistBackendShape = {
+ rules: [],
+ rows: [
+ {
+ ...checklist.rows[0],
+ cells: [
+ ...checklist.rows[0].cells,
+ { document_type: 'OSV', account: '01', status: 'CONFIRMED', missing_quarters: [], document_ids: ['d4'] },
+ ],
+ },
+ ],
+};
+
+/** Ключи колонок верхнего уровня и подколонок ОСВ. */
+const columnKeys = (selected?: string[]) =>
+ buildChecklistColumns(twoOsv, selected).map((c) => ('children' in c && c.children ? [c.key, c.children.map((ch) => ch.key)] : c.key));
+
+describe('buildDocumentFilterOptions', () => {
+ it('ББ, ОФР и ОСВ по счетам в порядке колонок', () => {
+ expect(buildDocumentFilterOptions(twoOsv)).toEqual([
+ { key: 'balance', title: 'Бух. баланс' },
+ { key: 'ofr', title: 'ОФР' },
+ { key: 'osv_60', title: 'ОСВ 60' },
+ { key: 'osv_01', title: 'ОСВ 01' },
+ ]);
+ });
+});
+
+describe('buildChecklistColumns', () => {
+ it('без выбора — все документы', () => {
+ expect(columnKeys()).toEqual(['name', 'balance', 'ofr', ['osv', ['osv_60', 'osv_01']]]);
+ expect(columnKeys(['balance', 'ofr', 'osv_60', 'osv_01'])).toEqual(columnKeys());
+ });
+
+ it('только выбранные документы; организация — всегда', () => {
+ expect(columnKeys(['ofr', 'osv_01'])).toEqual(['name', 'ofr', ['osv', ['osv_01']]]);
+ });
+
+ it('ни одного счёта ОСВ не выбрано — группы ОСВ нет', () => {
+ expect(columnKeys(['balance'])).toEqual(['name', 'balance']);
+ });
+
+ it('документы не выбраны — только организация', () => {
+ expect(columnKeys([])).toEqual(['name']);
+ });
+});
+
+describe('toggleDocument', () => {
+ const all = ['balance', 'ofr', 'osv_01'];
+
+ it('снятие и отметка пункта; порядок — как у пунктов', () => {
+ expect(toggleDocument(all, 'ofr', all)).toEqual(['balance', 'osv_01']);
+ expect(toggleDocument(['osv_01'], 'balance', all)).toEqual(['balance', 'osv_01']);
+ });
+
+ it('сняли последний пункт — ничего не выбрано', () => {
+ expect(toggleDocument(['ofr'], 'ofr', all)).toEqual([]);
+ });
+});
+
+describe('toggleAllDocuments', () => {
+ const all = ['balance', 'ofr', 'osv_01'];
+
+ it('при всех отмеченных снимает все, иначе отмечает все', () => {
+ expect(toggleAllDocuments(all, all)).toEqual([]);
+ expect(toggleAllDocuments(['ofr'], all)).toEqual(all);
+ expect(toggleAllDocuments([], all)).toEqual(all);
+ });
+});
+
+describe('documentFilterLabel', () => {
+ it('подпись по числу отмеченных', () => {
+ expect(documentFilterLabel(['a', 'b', 'c'], 3)).toBe('Все документы');
+ expect(documentFilterLabel([], 3)).toBe('Документы не выбраны');
+ expect(documentFilterLabel(['a'], 3)).toBe('1 документ');
+ expect(documentFilterLabel(['a', 'b'], 3)).toBe('2 документа');
+ expect(documentFilterLabel(['a', 'b', 'c', 'd', 'e'], 9)).toBe('5 документов');
+ });
+});
+
+describe('documentsQueryParam', () => {
+ const all = ['balance', 'ofr', 'osv_01', 'osv_60'];
+
+ it('отмечены все — без отбора', () => {
+ expect(documentsQueryParam(all, all)).toBeUndefined();
+ });
+
+ it('ключи колонок → документы API через запятую', () => {
+ expect(documentsQueryParam(['balance', 'ofr', 'osv_01'], all)).toBe('BALANCE,INCOME,OSV:01');
+ expect(documentsQueryParam(['osv_60'], all)).toBe('OSV:60');
+ });
+});
diff --git a/web/src/widgets/ConsoleLegalEntities/checklist_utils.tsx b/web/src/widgets/ConsoleLegalEntities/checklist_utils.tsx
index bbc77d3..5297a7c 100644
--- a/web/src/widgets/ConsoleLegalEntities/checklist_utils.tsx
+++ b/web/src/widgets/ConsoleLegalEntities/checklist_utils.tsx
@@ -10,6 +10,7 @@ import { Avatar, type TableColumnsType } from 'antd';
import type { ReactNode } from 'react';
import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets';
import type { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members';
+import { pluralRu } from '../../utils/labels';
import { fileStatusMapping } from './consts';
/** Строка таблицы чек-листа: ЮЛ и статусы ББ, ОФР и ОСВ по счетам. */
@@ -93,7 +94,8 @@ export function buildChecklistRows(checklistData: ChecklistBackendShape): Checkl
});
}
-export function buildChecklistColumns(checklistData: ChecklistBackendShape): TableColumnsType {
+/** Счета ОСВ матрицы — в порядке первого появления в ячейках строк. */
+function collectOsvCodes(checklistData: ChecklistBackendShape): string[] {
const osvCodes: string[] = [];
for (const row of checklistData.rows) {
for (const cell of row.cells) {
@@ -102,7 +104,65 @@ export function buildChecklistColumns(checklistData: ChecklistBackendShape): Tab
}
}
}
+ return osvCodes;
+}
+
+const osvKey = (code: string) => `osv_${code}`;
+
+/** Пункты фильтра «Документы» — колонки матрицы: ББ, ОФР, ОСВ по счетам. Ключ пункта = ключ колонки. */
+export function buildDocumentFilterOptions(checklistData: ChecklistBackendShape): { key: string; title: string }[] {
return [
+ { key: 'balance', title: 'Бух. баланс' },
+ { key: 'ofr', title: 'ОФР' },
+ ...collectOsvCodes(checklistData).map((code) => ({ key: osvKey(code), title: `ОСВ ${code}` })),
+ ];
+}
+
+/** Клик по документу в фильтре «Документы»: отметить или снять. Порядок ключей — как в `allKeys`. */
+export function toggleDocument(selected: string[], key: string, allKeys: string[]): string[] {
+ return allKeys.filter((k) => (k === key ? !selected.includes(k) : selected.includes(k)));
+}
+
+/** Клик по «Все документы»: при всех отмеченных снимает все (чтобы оставить один документ), иначе отмечает все. */
+export function toggleAllDocuments(selected: string[], allKeys: string[]): string[] {
+ return selected.length === allKeys.length ? [] : [...allKeys];
+}
+
+/** Ключ колонки → документ в параметре `documents` выгрузки: `BALANCE`, `INCOME`, `OSV:<счёт>`. */
+function toApiDocument(key: string): string {
+ if (key === 'balance') return 'BALANCE';
+ if (key === 'ofr') return 'INCOME';
+ return `OSV:${key.slice(osvKey('').length)}`;
+}
+
+/**
+ * Параметр `documents` выгрузки CSV по выбору фильтра: отмечены все — `undefined` (вся матрица, без отбора),
+ * иначе документы через запятую в порядке колонок: `BALANCE,INCOME,OSV:01`.
+ */
+export function documentsQueryParam(selected: string[], allKeys: string[]): string | undefined {
+ if (selected.length === allKeys.length) return undefined;
+ return selected.map(toApiDocument).join(',');
+}
+
+/** Подпись фильтра «Документы» в поле: «Все документы», «Документы не выбраны», «1 документ», «2 документа», «5 документов». */
+export function documentFilterLabel(selected: string[], total: number): string {
+ if (selected.length === 0) return 'Документы не выбраны';
+ if (selected.length === total) return 'Все документы';
+ return `${selected.length} ${pluralRu(selected.length, ['документ', 'документа', 'документов'])}`;
+}
+
+/**
+ * Колонки таблицы чек-листа. `selectedDocuments` — ключи колонок, отмеченные в фильтре «Документы»
+ * (не передан — все колонки; пустой список — только «Организация»).
+ */
+export function buildChecklistColumns(
+ checklistData: ChecklistBackendShape,
+ selectedDocuments?: string[],
+): TableColumnsType {
+ const isShown = (key: string) => !selectedDocuments || selectedDocuments.includes(key);
+ const osvCodes = collectOsvCodes(checklistData).filter((code) => isShown(osvKey(code)));
+
+ const columns: TableColumnsType = [
{
key: 'name',
title: 'ОРГАНИЗАЦИЯ',
@@ -112,16 +172,23 @@ export function buildChecklistColumns(checklistData: ChecklistBackendShape): Tab
),
},
- { key: 'balance', dataIndex: 'balance', title: 'БУХ. БАЛАНС', render: (v: FileStatus) => mapStatus(v) },
- { key: 'ofr', dataIndex: 'ofr', title: 'ОФР', render: (v: FileStatus) => mapStatus(v) },
- {
+ ];
+ if (isShown('balance')) {
+ columns.push({ key: 'balance', dataIndex: 'balance', title: 'БУХ. БАЛАНС', render: (v: FileStatus) => mapStatus(v) });
+ }
+ if (isShown('ofr')) {
+ columns.push({ key: 'ofr', dataIndex: 'ofr', title: 'ОФР', render: (v: FileStatus) => mapStatus(v) });
+ }
+ if (osvCodes.length > 0) {
+ columns.push({
key: 'osv',
title: 'ОСВ',
children: osvCodes.map((code) => ({
- key: `osv_${code}`,
+ key: osvKey(code),
title: code,
render: (_: unknown, r: ChecklistTableRow) => mapStatus(r.osv[code] ?? 'MISSING'),
})),
- },
- ];
+ });
+ }
+ return columns;
}
diff --git a/web/src/widgets/ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx b/web/src/widgets/ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx
index 5c2b5d9..61f29b7 100644
--- a/web/src/widgets/ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx
+++ b/web/src/widgets/ConsolidationChess/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx
@@ -1,5 +1,6 @@
import { Select, Space } from 'antd';
import type React from 'react';
+import { Labeled } from '../../../components/Labeled';
interface ConsolidationChessFormatTableFilterI {
decimals: number;
@@ -30,13 +31,11 @@ export const ConsolidationChessFormatTableFilter: React.FC
-
- Единицы
+
-
+
-
- Знаков после запятой
+
-
+
);
diff --git a/web/src/widgets/ConsolidationChess/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx b/web/src/widgets/ConsolidationChess/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
index dc61696..266f5e0 100644
--- a/web/src/widgets/ConsolidationChess/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
+++ b/web/src/widgets/ConsolidationChess/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
@@ -2,6 +2,7 @@ import { Button, Select, Space } from 'antd';
import type React from 'react';
import { useEffect, useState } from 'react';
import { ApiError, api } from '../../../api/client';
+import { Labeled } from '../../../components/Labeled';
import { type OptionType, TreeFilterDropdown } from '../../../components/TreeFilterDropdown/TreeFilterDropdown';
import type { Account, MatrixCompany, Quarter } from '../../../types/api';
import { CHESS_HALF_FILTER_HINT, chessMatrixMode } from '../../../utils/chessMatrixMode';
@@ -115,8 +116,7 @@ export const ConsolidationChessTableFilter: React.FC
-
- Год
+
-
-
- Период
+
+
-
-
- Компании
+
+
)}
/>
-
+
-
- Дт
+
-
+
-
- Кт
+
-
+
{companies?.length || debit?.length || credit?.length ? : null}
);
diff --git a/web/src/widgets/CorrectionTable/CorrectionToolbar.tsx b/web/src/widgets/CorrectionTable/CorrectionToolbar.tsx
index 80d6c72..15829e7 100644
--- a/web/src/widgets/CorrectionTable/CorrectionToolbar.tsx
+++ b/web/src/widgets/CorrectionTable/CorrectionToolbar.tsx
@@ -1,7 +1,8 @@
// Панель над таблицей: «Отображение», «Правило», «Тип записи», единицы и знаки, сброс фильтров, настройка, экспорт, весь экран.
import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
-import { Button, Select, Space } from 'antd';
+import { Button, Select } from 'antd';
import { ColumnSettings } from '../../components/GenericTable/ColumnSettings';
+import { Labeled } from '../../components/Labeled';
import type { CorrectionRuleOut } from '../../types/corrections';
import { PRECISIONS, UNITS, type Unit } from '../ConsolidatedOsv/useNumberDisplay';
import { COLUMN_DEFS, type ColKey, type ColShape } from './columns';
@@ -31,13 +32,6 @@ const RECORD_TYPES: { value: RecordType; label: string }[] = [
{ value: 'unallocated', label: 'Нераспределенные записи' },
];
-const Labeled = ({ label, children }: { label: string; children: React.ReactNode }) => (
-
- {label}
- {children}
-
-);
-
export function CorrectionToolbar(p: Props) {
const { filters, rules } = p;