feat: export purchases by items to csv/xlsx (#327)

This commit is contained in:
Ahmed Bouhuolia
2024-01-23 12:33:43 +02:00
committed by GitHub
parent 7eb84474a5
commit 429159acf9
17 changed files with 672 additions and 150 deletions

View File

@@ -1,17 +1,18 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator'; import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService'; import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
@Service() @Service()
export default class PurchasesByItemReportController extends BaseFinancialReportController { export default class PurchasesByItemReportController extends BaseFinancialReportController {
@Inject() @Inject()
purchasesByItemsService: PurchasesByItemsService; private purchasesByItemsApp: PurcahsesByItemsApplication;
/** /**
* Router constructor. * Router constructor.
@@ -63,20 +64,47 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
async purchasesByItems(req: Request, res: Response, next: NextFunction) { public async purchasesByItems(req: Request, res: Response) {
const { tenantId } = req; const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { const accept = this.accepts(req);
const { data, query, meta } =
await this.purchasesByItemsService.purchasesByItems(tenantId, filter); const acceptType = accept.types([
return res.status(200).send({ ACCEPT_TYPE.APPLICATION_JSON,
meta: this.transfromToResponse(meta), ACCEPT_TYPE.APPLICATION_JSON_TABLE,
data: this.transfromToResponse(data), ACCEPT_TYPE.APPLICATION_XLSX,
query: this.transfromToResponse(query), ACCEPT_TYPE.APPLICATION_CSV,
}); ]);
} catch (error) {
next(error); // JSON table response format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.purchasesByItemsApp.table(tenantId, filter);
return res.status(200).send(table);
// CSV response format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.purchasesByItemsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Xlsx response format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.purchasesByItemsApp.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);
// Json response format.
} else {
const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} }
} }

View File

@@ -1,13 +1,10 @@
import { import {
IAgingPeriod, IAgingPeriod,
IAgingPeriodTotal,
IAgingAmount,
IAgingSummaryQuery, IAgingSummaryQuery,
IAgingSummaryTotal, IAgingSummaryTotal,
IAgingSummaryContact, IAgingSummaryContact,
IAgingSummaryData, IAgingSummaryData,
} from './AgingReport'; } from './AgingReport';
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table'; import { IFinancialTable } from './Table';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery { export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {

View File

@@ -0,0 +1,54 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IPurchasesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[];
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
onlyActive: boolean;
}
export interface IPurchasesByItemsSheetMeta {
organizationName: string;
baseCurrency: string;
}
export interface IPurchasesByItemsItem {
id: number;
name: string;
code: string;
quantitySold: number;
soldCost: number;
averageSellPrice: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
averageSellPriceFormatted: string;
currencyCode: string;
}
export interface IPurchasesByItemsTotal {
quantitySold: number;
soldCost: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
currencyCode: string;
}
export type IPurchasesByItemsSheetData = {
items: IPurchasesByItemsItem[];
total: IPurchasesByItemsTotal;
};
export interface IPurchasesByItemsSheet {
data: IPurchasesByItemsSheetData;
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}
export interface IPurchasesByItemsTable extends IFinancialTable {
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}

View File

@@ -3,8 +3,10 @@ import {
IAccount, IAccount,
IAccountCreateDTO, IAccountCreateDTO,
IAccountEditDTO, IAccountEditDTO,
IAccountResponse,
IAccountsFilter, IAccountsFilter,
IAccountsTransactionsFilter, IAccountsTransactionsFilter,
IFilterMeta,
IGetAccountTransactionPOJO, IGetAccountTransactionPOJO,
} from '@/interfaces'; } from '@/interfaces';
import { CreateAccount } from './CreateAccount'; import { CreateAccount } from './CreateAccount';
@@ -14,6 +16,7 @@ import { ActivateAccount } from './ActivateAccount';
import { GetAccounts } from './GetAccounts'; import { GetAccounts } from './GetAccounts';
import { GetAccount } from './GetAccount'; import { GetAccount } from './GetAccount';
import { GetAccountTransactions } from './GetAccountTransactions'; import { GetAccountTransactions } from './GetAccountTransactions';
@Service() @Service()
export class AccountsApplication { export class AccountsApplication {
@Inject() @Inject()
@@ -115,9 +118,12 @@ export class AccountsApplication {
* Retrieves the accounts list. * Retrieves the accounts list.
* @param {number} tenantId * @param {number} tenantId
* @param {IAccountsFilter} filterDTO * @param {IAccountsFilter} filterDTO
* @returns * @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/ */
public getAccounts = (tenantId: number, filterDTO: IAccountsFilter) => { public getAccounts = (
tenantId: number,
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
return this.getAccountsService.getAccountsList(tenantId, filterDTO); return this.getAccountsService.getAccountsList(tenantId, filterDTO);
}; };

View File

@@ -2,36 +2,34 @@ import { get, isEmpty, sumBy } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, transformToMap } from 'utils'; import { allPassedConditionsPass, transformToMap } from 'utils';
import { IAccountTransaction, IItem } from '@/interfaces';
import { import {
IAccountTransaction, IPurchasesByItemsItem,
IInventoryValuationTotal, IPurchasesByItemsReportQuery,
IInventoryValuationItem, IPurchasesByItemsSheetData,
IInventoryValuationReportQuery, IPurchasesByItemsTotal,
IInventoryValuationStatement, } from '@/interfaces/PurchasesByItemsSheet';
IItem,
} from '@/interfaces';
export default class InventoryValuationReport extends FinancialSheet { export class PurchasesByItems extends FinancialSheet {
readonly baseCurrency: string; readonly baseCurrency: string;
readonly items: IItem[]; readonly items: IItem[];
readonly itemsTransactions: Map<number, IAccountTransaction>; readonly itemsTransactions: Map<number, IAccountTransaction>;
readonly query: IInventoryValuationReportQuery; readonly query: IPurchasesByItemsReportQuery;
/** /**
* Constructor method. * Constructor method.
* @param {IInventoryValuationReportQuery} query * @param {IPurchasesByItemsReportQuery} query
* @param {IItem[]} items * @param {IItem[]} items
* @param {IAccountTransaction[]} itemsTransactions * @param {IAccountTransaction[]} itemsTransactions
* @param {string} baseCurrency * @param {string} baseCurrency
*/ */
constructor( constructor(
query: IInventoryValuationReportQuery, query: IPurchasesByItemsReportQuery,
items: IItem[], items: IItem[],
itemsTransactions: IAccountTransaction[], itemsTransactions: IAccountTransaction[],
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.items = items; this.items = items;
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId'); this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
@@ -98,7 +96,7 @@ export default class InventoryValuationReport extends FinancialSheet {
* @param {IInventoryValuationItem} item * @param {IInventoryValuationItem} item
* @returns * @returns
*/ */
private itemSectionMapper = (item: IItem): IInventoryValuationItem => { private itemSectionMapper = (item: IItem): IPurchasesByItemsItem => {
const meta = this.getItemTransaction(item.id); const meta = this.getItemTransaction(item.id);
return { return {
@@ -145,9 +143,9 @@ export default class InventoryValuationReport extends FinancialSheet {
/** /**
* Retrieve the items sections. * Retrieve the items sections.
* @returns {IInventoryValuationItem[]} * @returns {IPurchasesByItemsItem[]}
*/ */
private itemsSection = (): IInventoryValuationItem[] => { private itemsSection = (): IPurchasesByItemsItem[] => {
return R.compose( return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter), R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper this.itemsMapper
@@ -156,10 +154,10 @@ export default class InventoryValuationReport extends FinancialSheet {
/** /**
* Retrieve the total section of the sheet. * Retrieve the total section of the sheet.
* @param {IInventoryValuationItem[]} items * @param {IPurchasesByItemsItem[]} items
* @returns {IInventoryValuationTotal} * @returns {IPurchasesByItemsTotal}
*/ */
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal { private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal {
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased); const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
const purchaseCost = sumBy(items, (item) => item.purchaseCost); const purchaseCost = sumBy(items, (item) => item.purchaseCost);
@@ -176,12 +174,12 @@ export default class InventoryValuationReport extends FinancialSheet {
/** /**
* Retrieve the sheet data. * Retrieve the sheet data.
* @returns * @returns {IInventoryValuationStatement}
*/ */
reportData(): IInventoryValuationStatement { public reportData(): IPurchasesByItemsSheetData {
const items = this.itemsSection(); const items = this.itemsSection();
const total = this.totalSection(items); const total = this.totalSection(items);
return items.length > 0 ? { items, total } : {}; return { items, total };
} }
} }

View File

@@ -0,0 +1,73 @@
import { Service, Inject } from 'typedi';
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService';
@Service()
export class PurcahsesByItemsApplication {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
@Inject()
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
@Inject()
private purchasesByItemsExport: PurchasesByItemsExport;
/**
* Retrieves the purchases by items in json format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns
*/
public sheet(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
return this.purchasesByItemsSheet.purchasesByItems(tenantId, query);
}
/**
* Retrieves the purchases by items in table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<IPurchasesByItemsTable>}
*/
public table(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
return this.purchasesByItemsTable.table(tenantId, query);
}
/**
* Retrieves the purchases by items in csv format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<string>}
*/
public csv(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<string> {
return this.purchasesByItemsExport.csv(tenantId, query);
}
/**
* Retrieves the purchases by items in xlsx format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<Buffer> {
return this.purchasesByItemsExport.xlsx(tenantId, query);
}
}

View File

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

View File

@@ -1,24 +1,24 @@
import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { import { Service, Inject } from 'typedi';
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import PurchasesByItems from './PurchasesByItems'; import { PurchasesByItems } from './PurchasesByItems';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsSheetMeta,
} from '@/interfaces/PurchasesByItemsSheet';
@Service() @Service()
export default class InventoryValuationReportService { export class PurchasesByItemsService {
@Inject() @Inject()
private tenancy: TenancyService; private tenancy: TenancyService;
/** /**
* Defaults balance sheet filter query. * Defaults purchases by items filter query.
* @return {IBalanceSheetQuery} * @return {IPurchasesByItemsReportQuery}
*/ */
get defaultQuery(): IInventoryValuationReportQuery { get defaultQuery(): IPurchasesByItemsReportQuery {
return { return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'), fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'), toDate: moment().format('YYYY-MM-DD'),
@@ -40,7 +40,7 @@ export default class InventoryValuationReportService {
* @param {number} tenantId - * @param {number} tenantId -
* @returns {IBalanceSheetMeta} * @returns {IBalanceSheetMeta}
*/ */
reportMetadata(tenantId: number): IInventoryValuationSheetMeta { reportMetadata(tenantId: number): IPurchasesByItemsSheetMeta {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({ const organizationName = settings.get({
@@ -62,18 +62,13 @@ export default class InventoryValuationReportService {
* Retrieve balance sheet statement. * Retrieve balance sheet statement.
* ------------- * -------------
* @param {number} tenantId * @param {number} tenantId
* @param {IBalanceSheetQuery} query * @param {IPurchasesByItemsReportQuery} query
* * @return {Promise<IPurchasesByItemsSheet>}
* @return {IBalanceSheetStatement}
*/ */
public async purchasesByItems( public async purchasesByItems(
tenantId: number, tenantId: number,
query: IInventoryValuationReportQuery query: IPurchasesByItemsReportQuery
): Promise<{ ): Promise<IPurchasesByItemsSheet> {
data: IInventoryValuationStatement;
query: IInventoryValuationReportQuery;
meta: IInventoryValuationSheetMeta;
}> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId); const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query() const tenant = await Tenant.query()
@@ -106,7 +101,6 @@ export default class InventoryValuationReportService {
builder.modify('filterDateRange', filter.fromDate, filter.toDate); builder.modify('filterDateRange', filter.fromDate, filter.toDate);
} }
); );
const purchasesByItemsInstance = new PurchasesByItems( const purchasesByItemsInstance = new PurchasesByItems(
filter, filter,
inventoryItems, inventoryItems,

View File

@@ -0,0 +1,111 @@
import * as R from 'ramda';
import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces';
import { ROW_TYPE } from './_types';
import { tableRowMapper } from '@/utils';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import FinancialSheet from '../FinancialSheet';
import {
IPurchasesByItemsItem,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
export class PurchasesByItemsTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IPurchasesByItemsSheetData;
/**
* Constructor method.
* @param data
*/
constructor(data) {
super();
this.data = data;
}
/**
* Retrieves thge common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private commonTableAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity_purchases', accessor: 'quantityPurchasedFormatted' },
{ key: 'purchase_amount', accessor: 'purchaseCostFormatted' },
{ key: 'average_cost', accessor: 'averageCostPriceFormatted' },
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonTableColumns(): ITableColumn[] {
return [
{ label: 'Item name', key: 'item_name' },
{ label: 'Quantity Purchased', key: 'quantity_purchases' },
{ label: 'Purchase Amount', key: 'purchase_amount' },
{ label: 'Average Price', key: 'average_cost' },
];
}
/**
* Maps the given item node to table row.
* @param {IPurchasesByItemsItem} item
* @returns {ITableRow}
*/
private itemMap = (item: IPurchasesByItemsItem): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, columns, meta);
};
/**
* Maps the given items nodes to table rows.
* @param {IPurchasesByItemsItem[]} items - Items nodes.
* @returns {ITableRow[]}
*/
private itemsMap = (items: IPurchasesByItemsItem[]): ITableRow[] => {
return R.map(this.itemMap)(items);
};
/**
* Maps the given total node to table rows.
* @param {IPurchasesByItemsTotal} total
* @returns {ITableRow}
*/
private totalNodeMap = (total: IPurchasesByItemsTotal): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, columns, meta);
};
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonTableColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
const itemsRows = this.itemsMap(this.data.items);
const totalRow = this.totalNodeMap(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)(itemsRows) as ITableRow[];
}
}

View File

@@ -0,0 +1,38 @@
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { Inject, Service } from 'typedi';
import { PurchasesByItemsService } from './PurchasesByItemsService';
import { PurchasesByItemsTable } from './PurchasesByItemsTable';
@Service()
export class PurchasesByItemsTableInjectable {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
/**
* Retrieves the purchases by items table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} filter
* @returns {Promise<IPurchasesByItemsTable>}
*/
public async table(
tenantId: number,
filter: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
const { data, query, meta } =
await this.purchasesByItemsSheet.purchasesByItems(tenantId, filter);
const table = new PurchasesByItemsTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
meta,
query,
};
}
}

View File

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

View File

@@ -18,6 +18,7 @@ import withPurchasesByItems from './withPurchasesByItems';
import withPurchasesByItemsActions from './withPurchasesByItemsActions'; import withPurchasesByItemsActions from './withPurchasesByItemsActions';
import { compose, saveInvoke } from '@/utils'; import { compose, saveInvoke } from '@/utils';
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider'; import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
import { PurchasesByItemsExportMenu } from './components';
function PurchasesByItemsActionsBar({ function PurchasesByItemsActionsBar({
// #withPurchasesByItems // #withPurchasesByItems
@@ -106,11 +107,18 @@ function PurchasesByItemsActionsBar({
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Popover
content={<PurchasesByItemsExportMenu />}
interactionKind={PopoverInteractionKind.CLICK}
placement="bottom-start"
minimal
>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />} icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />} text={<T id={'export'} />}
/> />
</Popover>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import FinancialReportPage from '../FinancialReportPage'; import FinancialReportPage from '../FinancialReportPage';
import { usePurchasesByItems } from '@/hooks/query'; import { usePurchasesByItemsTable } from '@/hooks/query';
import { transformFilterFormToQuery } from '../common'; import { transformFilterFormToQuery } from '../common';
const PurchasesByItemsContext = createContext(); const PurchasesByItemsContext = createContext();
@@ -13,7 +13,7 @@ function PurchasesByItemsProvider({ query, ...props }) {
isFetching, isFetching,
isLoading, isLoading,
refetch, refetch,
} = usePurchasesByItems( } = usePurchasesByItemsTable(
{ {
...transformFilterFormToQuery(query), ...transformFilterFormToQuery(query),
}, },
@@ -26,7 +26,6 @@ function PurchasesByItemsProvider({ query, ...props }) {
purchaseByItems, purchaseByItems,
isFetching, isFetching,
isLoading, isLoading,
refetchSheet: refetch, refetchSheet: refetch,
}; };
return ( return (

View File

@@ -6,10 +6,10 @@ import styled from 'styled-components';
import { ReportDataTable, FinancialSheet } from '@/components'; import { ReportDataTable, FinancialSheet } from '@/components';
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider'; import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
import { usePurchasesByItemsTableColumns } from './components';
import { tableRowTypesToClassnames } from '@/utils'; import { tableRowTypesToClassnames } from '@/utils';
import { TableStyle } from '@/constants'; import { TableStyle } from '@/constants';
import { usePurchasesByItemsTableColumns } from './dynamicColumns';
/** /**
* Purchases by items data table. * Purchases by items data table.
@@ -17,7 +17,7 @@ import { TableStyle } from '@/constants';
export default function PurchasesByItemsTable({ companyName }) { export default function PurchasesByItemsTable({ companyName }) {
// Purchases by items context. // Purchases by items context.
const { const {
purchaseByItems: { tableRows, query }, purchaseByItems: { table, query },
} = usePurchaseByItemsContext(); } = usePurchaseByItemsContext();
// Purchases by items table columns. // Purchases by items table columns.
@@ -32,7 +32,7 @@ export default function PurchasesByItemsTable({ companyName }) {
> >
<PurchasesByItemsDataTable <PurchasesByItemsDataTable
columns={columns} columns={columns}
data={tableRows} data={table.rows}
expandable={true} expandable={true}
expandToggleColumn={1} expandToggleColumn={1}
expandColumnSpace={1} expandColumnSpace={1}
@@ -58,7 +58,7 @@ const PurchasesByItemsDataTable = styled(ReportDataTable)`
padding-top: 0.36rem; padding-top: 0.36rem;
padding-bottom: 0.36rem; padding-bottom: 0.36rem;
} }
.tr.row_type--total .td { .tr.row_type--TOTAL .td {
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
font-weight: 500; font-weight: 500;
border-bottom: 3px double #000; border-bottom: 3px double #000;

View File

@@ -1,69 +1,22 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import { useRef } from 'react';
import intl from 'react-intl-universal'; import classNames from 'classnames';
import {
Classes,
Intent,
Menu,
MenuItem,
ProgressBar,
Text,
} from '@blueprintjs/core';
import { If } from '@/components'; import { AppToaster, If, Stack } from '@/components';
import { Align } from '@/constants';
import { CellTextSpan } from '@/components/Datatable/Cells';
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider'; import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
import { getColumnWidth } from '@/utils';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
import {
/** usePurchasesByItemsCsvExport,
* Retrieve purchases by items table columns. usePurchasesByItemsXlsxExport,
*/ } from '@/hooks/query';
export const usePurchasesByItemsTableColumns = () => {
// purchases by items context.
const {
purchaseByItems: { tableRows },
} = usePurchaseByItemsContext();
return React.useMemo(
() => [
{
Header: intl.get('item_name'),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
width: 180,
textOverview: true,
},
{
Header: intl.get('quantity_purchased'),
accessor: 'quantity_purchased_formatted',
Cell: CellTextSpan,
className: 'quantity_purchased_formatted',
width: getColumnWidth(tableRows, `quantity_purchased_formatted`, {
minWidth: 150,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('purchase_amount'),
accessor: 'purchase_cost_formatted',
Cell: CellTextSpan,
className: 'purchase_cost_formatted',
width: getColumnWidth(tableRows, `purchase_cost_formatted`, {
minWidth: 150,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('average_price'),
accessor: 'average_cost_price_formatted',
Cell: CellTextSpan,
className: 'average_cost_price_formatted',
width: getColumnWidth(tableRows, `average_cost_price_formatted`, {
minWidth: 180,
}),
textOverview: true,
align: Align.Right,
},
],
[tableRows],
);
};
/** /**
* Purchases by items progress loading bar. * Purchases by items progress loading bar.
@@ -77,3 +30,88 @@ export function PurchasesByItemsLoadingBar() {
</If> </If>
); );
} }
/**
* Retrieves the purchases by items export menu.
* @returns {JSX.Element}
*/
export const PurchasesByItemsExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { query } = usePurchaseByItemsContext();
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 } = usePurchasesByItemsXlsxExport(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 } = usePurchasesByItemsCsvExport(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,89 @@
// @ts-nocheck
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { Align } from '@/constants';
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => {
return getColumnWidth(
data,
accessor,
{ magicSpacing: 10, minWidth: 100 },
headerText,
);
};
/**
* Account name column mapper.
*/
const commonColumnMapper = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.key,
textOverview: true,
};
});
/**
* 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: 180,
};
});
const dynamiColumnMapper = R.curry((data, column) => {
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_purchases'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'purchase_amount'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'average_cost'), _numericColumnAccessor),
commonColumnMapper(data),
)(column);
});
/**
* Composes the dynamic columns that fetched from request to columns to table component.
*/
export const dynamicColumns = R.curry((data, columns) => {
return R.map(dynamiColumnMapper(data), columns);
});
/**
* Retrieves the purchases by items sheet table columns for table component.
*/
export const usePurchasesByItemsTableColumns = () => {
const { purchaseByItems } = usePurchaseByItemsContext();
if (!purchaseByItems) {
throw new Error('Purchases by items context not found');
}
const { table } = purchaseByItems;
return dynamicColumns(table.rows, table.columns);
};

View File

@@ -426,20 +426,58 @@ export function usePurchasesByItems(query, props) {
params: query, params: query,
}, },
{ {
select: (res) => ({ select: (res) => res.data,
tableRows: purchasesByItemsReducer(res.data.data),
...res.data,
}),
defaultData: {
tableRows: [],
data: [],
query: {},
},
...props, ...props,
}, },
); );
} }
export function usePurchasesByItemsTable(query, props) {
return useRequestQuery(
[t.FINANCIAL_REPORT, t.PURCHASES_BY_ITEMS, query],
{
method: 'get',
url: '/financial_statements/purchases-by-items',
params: query,
headers: {
accept: 'application/json+table',
},
},
{
select: (res) => res.data,
...props,
},
);
}
export const usePurchasesByItemsCsvExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/purchases-by-items',
config: {
headers: {
accept: 'application/csv',
},
params: query,
},
filename: 'purchases_by_items.csv',
...args,
});
};
export const usePurchasesByItemsXlsxExport = (query, args) => {
return useDownloadFile({
url: '/financial_statements/purchases-by-items',
config: {
headers: {
accept: 'application/xlsx',
},
params: query,
},
filename: 'purchases_by_items.xlsx',
...args,
});
};
/** /**
* Retrieve sales by items. * Retrieve sales by items.
*/ */