feat: inventory valuation csv and xlsx export (#308)

* feat: inventory valuation csv and xlsx export

* feat(server): inventory valuation sheet exporting

* feat(webapp): inventory valuation sheet dyanmic columns

* feat: inventory valuation dynamic columns

* feat: inventory valuation TS types
This commit is contained in:
Ahmed Bouhuolia
2024-01-18 20:16:29 +02:00
committed by GitHub
parent 8495990ec2
commit 3672abe7a5
15 changed files with 622 additions and 65 deletions

View File

@@ -3,14 +3,15 @@ import { query, ValidationChain } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { InventoryValuationSheetApplication } from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class InventoryValuationReportController extends BaseFinancialReportController {
@Inject()
inventoryValuationService: InventoryValuationService;
private inventoryValuationApp: InventoryValuationSheetApplication;
/**
* Router constructor.
@@ -71,19 +72,45 @@ export default class InventoryValuationReportController extends BaseFinancialRep
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.inventoryValuationService.inventoryValuationSheet(
tenantId,
filter
);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.inventoryValuationApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV == acceptType) {
const buffer = await this.inventoryValuationApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xslx buffer format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.inventoryValuationApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json format.
} else {
const { data, query, meta } = await this.inventoryValuationApp.sheet(
tenantId,
filter
);
return res.status(200).send({ meta, data, query });
}
}
}

View File

@@ -1,4 +1,5 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IInventoryValuationReportQuery {
asDate: Date | string;
@@ -39,9 +40,19 @@ export interface IInventoryValuationTotal {
quantityFormatted: string;
}
export type IInventoryValuationStatement =
| {
items: IInventoryValuationItem[];
total: IInventoryValuationTotal;
}
| {};
export type IInventoryValuationStatement = {
items: IInventoryValuationItem[];
total: IInventoryValuationTotal;
};
export type IInventoryValuationSheetData = IInventoryValuationStatement;
export interface IInventoryValuationSheet {
data: IInventoryValuationStatement;
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}
export interface IInventoryValuationTable extends IFinancialTable {
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}

View File

@@ -11,7 +11,7 @@ import {
} from '@/interfaces';
import { allPassedConditionsPass, transformToMap } from 'utils';
export default class InventoryValuationSheet extends FinancialSheet {
export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
readonly items: IItem[];
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;

View File

@@ -0,0 +1,76 @@
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationTable,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { InventoryValuationSheetExportable } from './InventoryValuationSheetExportable';
@Service()
export class InventoryValuationSheetApplication {
@Inject()
private inventoryValuationSheet: InventoryValuationSheetService;
@Inject()
private inventoryValuationTable: InventoryValuationSheetTableInjectable;
@Inject()
private inventoryValuationExport: InventoryValuationSheetExportable;
/**
* Retrieves the inventory valuation json format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public sheet(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<IInventoryValuationSheet> {
return this.inventoryValuationSheet.inventoryValuationSheet(
tenantId,
query
);
}
/**
* Retrieves the inventory valuation json table format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<IInventoryValuationTable>}
*/
public table(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<IInventoryValuationTable> {
return this.inventoryValuationTable.table(tenantId, query);
}
/**
* Retrieves the inventory valuation xlsx format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public xlsx(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<Buffer> {
return this.inventoryValuationExport.xlsx(tenantId, query);
}
/**
* Retrieves the inventory valuation csv format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public csv(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<string> {
return this.inventoryValuationExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { IInventoryValuationReportQuery } from '@/interfaces';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
@Service()
export class InventoryValuationSheetExportable {
@Inject()
private inventoryValuationTable: InventoryValuationSheetTableInjectable;
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<Buffer> {
const table = await this.inventoryValuationTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the trial balance sheet in CSV format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<string> {
const table = await this.inventoryValuationTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -3,15 +3,16 @@ import moment from 'moment';
import { isEmpty } from 'lodash';
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import InventoryValuationSheet from './InventoryValuationSheet';
import { InventoryValuationSheet } from './InventoryValuationSheet';
import InventoryService from '@/services/Inventory/Inventory';
import { Tenant } from '@/system/models';
@Service()
export default class InventoryValuationSheetService {
export class InventoryValuationSheetService {
@Inject()
tenancy: TenancyService;
@@ -80,7 +81,7 @@ export default class InventoryValuationSheetService {
public async inventoryValuationSheet(
tenantId: number,
query: IInventoryValuationReportQuery
) {
): Promise<IInventoryValuationSheet> {
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()

View File

@@ -0,0 +1,103 @@
import * as R from 'ramda';
import {
IInventoryValuationItem,
IInventoryValuationSheetData,
IInventoryValuationTotal,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import { ROW_TYPE } from './_constants';
export class InventoryValuationSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private readonly data: IInventoryValuationSheetData;
/**
* Constructor method.
* @param {IInventoryValuationSheetData} data
*/
constructor(data: IInventoryValuationSheetData) {
super();
this.data = data;
}
/**
* Retrieves the common columns accessors.
* @returns {ITableColumnAccessor}
*/
private commonColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity', accessor: 'quantityFormatted' },
{ key: 'valuation', accessor: 'valuationFormatted' },
{ key: 'average', accessor: 'averageFormatted' },
];
}
/**
* Maps the given total node to table row.
* @param {IInventoryValuationTotal} total
* @returns {ITableRow}
*/
private totalRowMapper = (total: IInventoryValuationTotal): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, accessors, meta);
};
/**
* Maps the given item node to table row.
* @param {IInventoryValuationItem} item
* @returns {ITableRow}
*/
private itemRowMapper = (item: IInventoryValuationItem): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, accessors, meta);
};
/**
* Maps the given items nodes to table rowes.
* @param {IInventoryValuationItem[]} items
* @returns {ITableRow[]}
*/
private itemsRowsMapper = (items: IInventoryValuationItem[]): ITableRow[] => {
return R.map(this.itemRowMapper)(items);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const itemsRows = this.itemsRowsMapper(this.data.items);
const totalRow = this.totalRowMapper(this.data.total);
return [...itemsRows, totalRow];
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = [
{ key: 'item_name', label: 'Item Name' },
{ key: 'quantity', label: 'Quantity' },
{ key: 'valuation', label: 'Valuation' },
{ key: 'average', label: 'Average' },
];
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,39 @@
import { Inject, Service } from 'typedi';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import {
IInventoryValuationReportQuery,
IInventoryValuationTable,
} from '@/interfaces';
import { InventoryValuationSheetTable } from './InventoryValuationSheetTable';
@Service()
export class InventoryValuationSheetTableInjectable {
@Inject()
private sheet: InventoryValuationSheetService;
/**
* Retrieves the inventory valuation json table format.
* @param {number} tenantId -
* @param {IInventoryValuationReportQuery} filter -
* @returns {Promise<IInventoryValuationTable>}
*/
public async table(
tenantId: number,
filter: IInventoryValuationReportQuery
): Promise<IInventoryValuationTable> {
const { data, query, meta } = await this.sheet.inventoryValuationSheet(
tenantId,
filter
);
const table = new InventoryValuationSheetTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,4 @@
export enum ROW_TYPE {
ITEM = 'ITEM',
TOTAL = 'TOTAL',
}

View File

@@ -19,6 +19,7 @@ import withInventoryValuationActions from './withInventoryValuationActions';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import { compose, saveInvoke } from '@/utils';
import { InventoryValuationExportMenu } from './components';
function InventoryValuationActionsBar({
// #withInventoryValuation
@@ -109,11 +110,18 @@ function InventoryValuationActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Popover
content={<InventoryValuationExportMenu />}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import FinancialReportPage from '../FinancialReportPage';
import { useInventoryValuation } from '@/hooks/query';
import { useInventoryValuationTable } from '@/hooks/query';
import { transformFilterFormToQuery } from '../common';
const InventoryValuationContext = React.createContext();
@@ -21,7 +21,7 @@ function InventoryValuationProvider({ query, ...props }) {
isFetching,
isLoading,
refetch,
} = useInventoryValuation(requestQuery, {
} = useInventoryValuationTable(requestQuery, {
keepPreviousData: true,
});

View File

@@ -8,23 +8,23 @@ import { ReportDataTable, FinancialSheet } from '@/components';
import { tableRowTypesToClassnames } from '@/utils';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import { useInventoryValuationTableColumns } from './components';
import { useInventoryValuationColumns } from './dynamicColumns';
/**
* inventory valuation data table.
* Inventory valuation data table.
*/
export default function InventoryValuationTable({
//#ownProps
// #ownProps
companyName,
}) {
// inventory valuation context.
// Inventory valuation context.
const {
inventoryValuation: { tableRows, query },
inventoryValuation: { table, query },
isLoading,
} = useInventoryValuationContext();
// inventory valuation table columns.
const columns = useInventoryValuationTableColumns();
// Inventory valuation table columns.
const columns = useInventoryValuationColumns();
return (
<InventoryValuationSheet
@@ -35,7 +35,7 @@ export default function InventoryValuationTable({
>
<InventoryValuationDataTable
columns={columns}
data={tableRows}
data={table.rows}
expandable={true}
expandToggleColumn={1}
expandColumnSpace={1}
@@ -62,7 +62,7 @@ const InventoryValuationDataTable = styled(ReportDataTable)`
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
.tr.row_type--total .td {
.tr.row_type--TOTAL .td {
border-top: 1px solid #bbb;
font-weight: 500;
border-bottom: 3px double #000;

View File

@@ -1,13 +1,25 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { useMemo, useRef } from 'react';
import intl from 'react-intl-universal';
import { If } from '@/components';
import classNames from 'classnames';
import { AppToaster, If, Stack } from '@/components';
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import { CellTextSpan } from '@/components/Datatable/Cells';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import FinancialLoadingBar from '../FinancialLoadingBar';
import {
Classes,
Intent,
Menu,
MenuItem,
ProgressBar,
Text,
} from '@blueprintjs/core';
import {
useInventoryValuationCsvExport,
useInventoryValuationXlsxExport,
} from '@/hooks/query';
/**
* Retrieve inventory valuation table columns.
@@ -77,3 +89,87 @@ export function InventoryValuationLoadingBar() {
</If>
);
}
/**
* Retrieves the inventory valuation sheet export menu.
* @returns {JSX.Element}
*/
export const InventoryValuationExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { query } = useInventoryValuationContext();
const openProgressToast = (amount: number) => {
return (
<Stack spacing={8}>
<Text>The report has been exported successfully.</Text>
<ProgressBar
className={classNames('toast-progress', {
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
})}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
</Stack>
);
};
// Export the report to xlsx.
const { mutateAsync: xlsxExport } = useInventoryValuationXlsxExport(query, {
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
});
// Export the report to csv.
const { mutateAsync: csvExport } = useInventoryValuationCsvExport(query, {
onDownloadProgress: (xlsxExportProgress: number) => {
if (!toastKey.current) {
toastKey.current = AppToaster.show({
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
});
} else {
AppToaster.show(
{
message: openProgressToast(xlsxExportProgress),
...commonToastConfig,
},
toastKey.current,
);
}
},
});
// Handle csv export button click.
const handleCsvExportBtnClick = () => {
csvExport();
};
// Handle xlsx export button click.
const handleXlsxExportBtnClick = () => {
xlsxExport();
};
return (
<Menu>
<MenuItem
text={'XLSX (Microsoft Excel)'}
onClick={handleXlsxExportBtnClick}
/>
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
</Menu>
);
};

View File

@@ -0,0 +1,97 @@
// @ts-nocheck
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { useInventoryValuationContext } from './InventoryValuationProvider';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => {
return getColumnWidth(
data,
accessor,
{ magicSpacing: 10, minWidth: 100 },
headerText,
);
};
/**
* Common column mapper.
*/
const commonAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.key,
textOverview: true,
align: Align.Left,
};
});
/**
* Numeric columns accessor.
*/
const numericColumnAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
const width = getReportColWidth(data, accessor, column.label);
return {
...column,
align: Align.Right,
width,
};
});
/**
* Item name column accessor.
*/
const itemNameColumnAccessor = R.curry((data, column) => {
return {
...column,
width: 240,
}
});
/**
* Dynamic column mapper.
* @param {} data -
* @param {} column -
*/
const dynamicColumnMapper = R.curry((data, column) => {
const _commonAccessor = commonAccessor(data);
const _numericColumnAccessor = numericColumnAccessor(data);
const _itemNameColumnAccessor = itemNameColumnAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'item_name'), _itemNameColumnAccessor),
R.when(R.pathEq(['key'], 'quantity'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'valuation'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'average'), _numericColumnAccessor),
_commonAccessor,
)(column);
});
/**
* Composes the fetched dynamic columns from the server to the columns to pass it
* to the table component.
*/
export const dynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};
/**
* Retrieves the table columns of inventory valuation sheet.
*/
export const useInventoryValuationColumns = () => {
const { inventoryValuation } = useInventoryValuationContext();
if (!inventoryValuation) {
throw new Error('The inventory valuation is not loaded');
}
const { table } = inventoryValuation;
return dynamicColumns(table.columns, table.rows);
};

View File

@@ -176,34 +176,33 @@ export function useGeneralLedgerSheet(query, props) {
);
}
export const useGeneralLedgerSheetXlsxExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/xlsx',
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/xlsx',
},
params: query,
},
params: query,
},
filename: 'general_ledger.xlsx',
...args,
});
filename: 'general_ledger.xlsx',
...args,
});
};
export const useGeneralLedgerSheetCsvExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/csv',
return useDownloadFile({
url: '/financial_statements/general_ledger',
config: {
headers: {
accept: 'application/csv',
},
params: query,
},
params: query,
},
filename: 'general_ledger.csv',
...args,
});
filename: 'general_ledger.csv',
...args,
});
};
/**
* Retrieve journal sheet.
*/
@@ -376,6 +375,56 @@ export function useInventoryValuation(query, props) {
},
);
}
/**
* Retrieve inventory valuation.
*/
export function useInventoryValuationTable(query, props) {
return useRequestQuery(
[t.FINANCIAL_REPORT, t.INVENTORY_VALUATION, query],
{
method: 'get',
url: '/financial_statements/inventory-valuation',
params: query,
headers: {
Accept: 'application/json+table',
},
},
{
select: (res) => res.data,
...props,
},
);
}
export const useInventoryValuationXlsxExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/inventory-valuation',
config: {
headers: {
accept: 'application/xlsx',
},
params: query,
},
filename: 'inventory_valuation.xlsx',
...args,
});
};
export const useInventoryValuationCsvExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/inventory-valuation',
config: {
headers: {
accept: 'application/csv',
},
params: query,
},
filename: 'inventory_valuation.csv',
...args,
});
};
/**
* Retrieve purchases by items.
*/