diff --git a/web/src/App.tsx b/web/src/App.tsx
index 3d972ee..871c730 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -17,7 +17,7 @@ import {
Main,
Mono,
} from './layouts/';
-import { ConsolidatioReportPage } from './layouts/ConsolidatioReportPage';
+import { ConsolidationReportPage } from './layouts/ConsolidationReportPage';
import { store } from './store/store';
import { Sidebar } from './widgets/Sidebar/Sidebar';
@@ -37,7 +37,7 @@ function App() {
} />
} />
- } />
+ } />
} />
} />
diff --git a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx
index b33a829..1c313a0 100644
--- a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx
+++ b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx
@@ -80,6 +80,9 @@ export const TreeFilterDropdown: React.FC = ({
return keys;
};
+ const allFilteredKeys = getAllNodeKeys(filteredTreeData);
+ const visibleCheckedKeys = checkedKeys.filter((key) => allFilteredKeys.includes(key));
+
const handleSelectAll = () => {
setCheckedKeys(getAllNodeKeys(treeData));
};
@@ -116,10 +119,28 @@ export const TreeFilterDropdown: React.FC = ({
checkable
selectable={false}
treeData={filteredTreeData}
- checkedKeys={checkedKeys}
- onCheck={(keys) => {
- const checked = Array.isArray(keys) ? keys : keys.checked;
- setCheckedKeys(checked as string[]);
+ checkedKeys={visibleCheckedKeys}
+ onCheck={(keys, e) => {
+ const newChecked = Array.isArray(keys) ? keys : keys.checked;
+ const targetKey = e.node.key as string;
+ const isChecked = newChecked.includes(targetKey);
+
+ if (isChecked) {
+ setCheckedKeys(Array.from(new Set([...checkedKeys, ...newChecked.map(String)])));
+ } else {
+ const keysToRemove = [targetKey];
+ const findChildren = (node: OptionType) => {
+ if (node.children) {
+ node.children.forEach((child) => {
+ keysToRemove.push(child.key);
+ findChildren(child);
+ });
+ }
+ };
+ findChildren(e.node);
+
+ setCheckedKeys(checkedKeys.filter((k) => !keysToRemove.includes(k)));
+ }
}}
expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])}
diff --git a/web/src/layouts/ConsolidatioReportPage.tsx b/web/src/layouts/ConsolidationReportPage.tsx
similarity index 83%
rename from web/src/layouts/ConsolidatioReportPage.tsx
rename to web/src/layouts/ConsolidationReportPage.tsx
index 19693f6..512a3eb 100644
--- a/web/src/layouts/ConsolidatioReportPage.tsx
+++ b/web/src/layouts/ConsolidationReportPage.tsx
@@ -1,6 +1,6 @@
import { Space, message } from 'antd';
import { type ReactNode, useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { ApiError } from '../api/client';
import PageTitle from '../components/PageTitle';
import { TableTag } from '../components/TableTag/TableTag';
@@ -17,11 +17,12 @@ import { OsvContent } from '../widgets/ConsolidatedOsvContent/ConsolidatedOsvCon
function ReportContent() {
const [card, setCard] = useState(null);
const { version_id, tab } = useParams<{ tab?: string; version_id: string }>();
-
+ const navigation = useNavigate();
+ const { pathname } = useLocation();
const TAB_COMPONENTS: Record = {
osv: ,
- chess: ,
+ chess: ,
};
const ActiveTab = TAB_COMPONENTS[tab as string] ?? null;
@@ -46,6 +47,17 @@ function ReportContent() {
};
}
}, [version_id]);
+
+ useEffect(() => {
+ const isValidTab = !tab || tab === 'osv' || tab === 'chess';
+
+ if (!isValidTab) {
+ const pathParts = pathname.split('/');
+ pathParts[pathParts.length - 1] = 'osv';
+ const newPath = pathParts.join('/');
+ navigation(newPath, { replace: true });
+ }
+ }, [tab, navigation, pathname]);
return (
();
// key: при смене версии всё состояние (карточка, фильтры, страница, данные) создаётся заново
return ;
diff --git a/web/src/layouts/index.ts b/web/src/layouts/index.ts
index 90015f7..33cee41 100644
--- a/web/src/layouts/index.ts
+++ b/web/src/layouts/index.ts
@@ -9,4 +9,4 @@ export { GroupsLayout } from './Groups';
export { AuditLogging } from './AuditLogging';
export { Admin } from './Admin';
export { DocumentPage } from './DocumentPage';
-export { ConsolidatioReportPage } from './ConsolidatioReportPage';
+export { ConsolidationReportPage } from './ConsolidationReportPage';
diff --git a/web/src/services/consodilation/chess.ts b/web/src/services/consodilation/intercompanyMatrix.ts
similarity index 85%
rename from web/src/services/consodilation/chess.ts
rename to web/src/services/consodilation/intercompanyMatrix.ts
index 5ecf609..4ee2f1f 100644
--- a/web/src/services/consodilation/chess.ts
+++ b/web/src/services/consodilation/intercompanyMatrix.ts
@@ -18,3 +18,6 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
},
body: JSON.stringify(data),
});
+
+export const fetchChessConsolidationsResult = (id: string) =>
+ api.get(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
diff --git a/web/src/types/api.ts b/web/src/types/api.ts
index c6e0850..cf2c7c9 100644
--- a/web/src/types/api.ts
+++ b/web/src/types/api.ts
@@ -394,3 +394,15 @@ export interface LegalEntityBrief {
name: string;
inn: string;
}
+
+export interface Account {
+ id: string;
+ code: string;
+ parent_id: string | null;
+ name: string;
+ kind: string;
+ row_type: string;
+ is_off_balance: boolean;
+ is_debit: boolean;
+ is_credit: boolean;
+}
diff --git a/web/src/types/chess/chess.ts b/web/src/types/chess/chess.ts
index 67e4d15..7da93a6 100644
--- a/web/src/types/chess/chess.ts
+++ b/web/src/types/chess/chess.ts
@@ -2,6 +2,10 @@ export interface MatrixCompany {
id: string;
name: string;
}
+export interface MatrixAmount {
+ company_id: string;
+ amount: string;
+}
export interface MatrixCell {
company_a_id: string;
@@ -15,10 +19,20 @@ export interface MatrixCell {
skipped_osv_entry_count: number;
}
+export interface MatrixResCell {
+ code: string;
+ name: string;
+ group_total: string;
+ amounts: MatrixAmount[];
+}
export interface IntercompanyMatrixResponse {
companies: MatrixCompany[];
cells: MatrixCell[];
}
+export interface IntercompanyMatrixResultResponse {
+ companies: MatrixCompany[];
+ articles: MatrixResCell[];
+}
export type ChessManualRequest = {
debit_account_ids: string[];
diff --git a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
index fc076a4..f2a60f6 100644
--- a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
+++ b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
@@ -1,5 +1,5 @@
import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
-import { Button } from 'antd';
+import { Button, ConfigProvider } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ApiError } from '../../api/client';
@@ -8,14 +8,16 @@ import {
exportChessTableManual,
fetchChessConsolidations,
fetchChessConsolidationsManual,
-} from '../../services/consodilation/chess';
+} from '../../services/consodilation/intercompanyMatrix';
import type { IntercompanyMatrixResponse } from '../../types/chess/chess';
-import { saveBlob } from '../../utils/download';
+import { exportFilename, saveBlob } from '../../utils/download';
import { useNotificationApi } from '../../utils/useNotification';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
-import { ConsolidationChessTableFilter } from '../ConsolidationChessTable/ConsolidationChessTableFilter';
+import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
+import { VersionCard } from '../../types/consolidations/version_members';
-export const ChessContent = () => {
+
+export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const { version_id } = useParams<{ version_id: string }>();
const notification = useNotificationApi();
@@ -27,7 +29,7 @@ export const ChessContent = () => {
const [units, setUnits] = useState('thousand');
const [debit, setDebit] = useState([]);
const [credit, setCredit] = useState([]);
-
+
const containerRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
@@ -44,7 +46,11 @@ export const ChessContent = () => {
}
};
- const loadData = async (vId: string, dIds: string[], cIds: string[]) => {
+ const loadData = async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => {
+ if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) {
+ notification.info({description:'Для сравнений по Дт и Кт нужно заполнить оба фильтра'})
+ return;
+ }
setLoading(true);
try {
let res: IntercompanyMatrixResponse;
@@ -53,13 +59,17 @@ export const ChessContent = () => {
} else {
res = await fetchChessConsolidations(vId);
}
- setData(res);
+ if (isActual()) {
+ setData(res);
+ }
} catch (e) {
- if (e instanceof ApiError) {
+ if (isActual() && e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
- setLoading(false);
+ if (isActual()) {
+ setLoading(false);
+ }
}
};
@@ -71,7 +81,7 @@ export const ChessContent = () => {
try {
if (debit?.length && credit?.length) {
const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit });
- saveBlob(blob, filename ?? 'Шахматка');
+ saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? ''));
} else {
const { blob, filename } = await exportChessTableAll(version_id);
saveBlob(blob, filename ?? 'Шахматка');
@@ -87,7 +97,15 @@ export const ChessContent = () => {
useEffect(() => {
if (!version_id) return;
- loadData(version_id, debit, credit);
+
+ let isMounted = true;
+ const isActual = () => isMounted;
+
+ loadData(version_id, debit, credit, isActual);
+
+ return () => {
+ isMounted = false;
+ };
}, [version_id, debit, credit]);
useEffect(() => {
@@ -111,56 +129,58 @@ export const ChessContent = () => {
}, [data, companies]);
return (
-
-
-
-
+
containerRef.current || document.body}>
+
+
+
+
+
+
+ : }>
+ {isFullscreen ? 'Свернуть' : 'На весь экран'}
+
-
- : }>
- {isFullscreen ? 'Свернуть' : 'На весь экран'}
-
-
- }
- />
-
+ }
+ />
+
+
);
};
diff --git a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
index d380f64..5a226dd 100644
--- a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
+++ b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
@@ -36,20 +36,53 @@ export const ConsolidationChessTable: React.FC = ({
return result;
}, [data?.cells]);
- const formatValue = (val: string | number) => {
+ const formatValue = (val: string | number | null | undefined) => {
if (val === undefined || val === null || val === '') return '';
- let num = typeof val === 'string' ? Number.parseFloat(val) : val;
- if (Number.isNaN(num)) return String(val);
- if (units === 'thousand') {
- num /= 1000;
- } else if (units === 'million') {
- num /= 1000000;
- } else if (units === 'billion') {
- num /= 1_000_000_000;
+ let strVal = String(val);
+
+ if (strVal.startsWith('(') && strVal.endsWith(')')) {
+ strVal = `-${strVal.slice(1, -1)}`;
}
- return num.toFixed(decimals).replace('.', ',');
+ if (units !== 'rub') {
+ let shift = 0;
+ if (units === 'thousand') shift = 3;
+ else if (units === 'million') shift = 6;
+ else if (units === 'billion') shift = 9;
+
+ if (shift > 0) {
+ const isNegative = strVal.startsWith('-');
+ const clean = isNegative ? strVal.slice(1) : strVal;
+ const [intPart = '0', fracPart = ''] = clean.split('.');
+
+ let newInt = intPart;
+ let newFrac = fracPart;
+
+ if (intPart.length <= shift) {
+ const zeros = '0'.repeat(shift - intPart.length);
+ newInt = '0';
+ newFrac = `${zeros}${intPart}${fracPart}`;
+ } else {
+ newInt = intPart.slice(0, -shift);
+ newFrac = `${intPart.slice(-shift)}${fracPart}`;
+ }
+
+ strVal = `${isNegative ? '-' : ''}${newInt}.${newFrac.slice(0, 4)}`;
+ }
+ }
+
+
+ const num = Number(strVal);
+
+ if (Number.isNaN(num)) {
+ return strVal;
+ }
+
+ return new Intl.NumberFormat('ru-RU', {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ }).format(num);
};
const parseAmount = (val: string | null | undefined): number => {
@@ -58,7 +91,7 @@ export const ConsolidationChessTable: React.FC = ({
const isNegative: boolean = val.startsWith('(') && val.endsWith(')');
const cleanVal = isNegative ? val.slice(1, -1) : val;
- const num = Number.parseFloat(cleanVal);
+ const num = Number(cleanVal);
if (Number.isNaN(num)) {
return 0;
@@ -76,13 +109,7 @@ export const ConsolidationChessTable: React.FC = ({
key: 'name',
fixed: 'left',
width: 200,
- render: (text, row) => {text},
- onCell: (row) => ({
- style: {
- background: row.isTotal ? '#e6f4ea' : '#fff',
- zIndex: 2,
- },
- }),
+
};
const companyColumns = data.companies.map((company) => ({
@@ -92,17 +119,12 @@ export const ConsolidationChessTable: React.FC = ({
width: 150,
align: 'right' as const,
onCell: (row: TableRow) => {
- if (row.isTotal) return { style: { background: '#e6f4ea' } };
const cell = matrixData[row.id]?.[company.id];
if (!cell) return { className: 'empty-cell' };
return {};
},
render: (_: unknown, row: TableRow) => {
- if (row.isTotal) {
- const totalVal = (row as any)[company.id];
- return {formatValue(totalVal)};
- }
const cell = matrixData[row.id]?.[company.id];
if (!cell) return —;
return (
diff --git a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
similarity index 93%
rename from web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx
rename to web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
index 2a0f3af..f60f706 100644
--- a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx
+++ b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
@@ -5,18 +5,9 @@ import { ApiError, api } from '../../api/client';
import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
import type { MatrixCompany } from '../../types/chess/chess';
import { useNotificationApi } from '../../utils/useNotification';
+import { Account } from '../../types/api';
+
-interface Account {
- id: string;
- code: string;
- parent_id: string | null;
- name: string;
- kind: string;
- row_type: string;
- is_off_balance: boolean;
- is_debit: boolean;
- is_credit: boolean;
-}
interface ConsolidationChessTableFilterI {
companies: string[];
@@ -124,6 +115,16 @@ export const ConsolidationChessTableFilter: React.FC {
+ let result: { label: string; value: string }[] = [];
+ nodes.forEach((node) => {
+ result.push({ label: String(node.code), value: String(node.key) });
+ if (node.children) {
+ result = [...result, ...flattenTree(node.children)];
+ }
+ });
+ return result;
+ };
function dropFiltersHandler() {
onCompaniesChange([]);
onDebitChange([]);
@@ -187,7 +188,7 @@ export const ConsolidationChessTableFilter: React.FC ({ label: node.code, value: node.key }))}
+ options={flattenTree(debitTreeData)}
popupRender={() => (
({ label: node.code, value: node.key }))}
+ options={flattenTree(creditTreeData)}
popupRender={() => (