From e6a3daa2c37b47514eabf7afd0ecc75f5c9c7b0c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 2 Jan 2024 21:54:10 +0200 Subject: [PATCH] feat(server): general ledger exporting to csv/xlsx --- .../FinancialStatements/GeneralLedger.ts | 52 ++++++--- .../FinancialStatements/JournalSheet.ts | 52 ++++++--- .../src/interfaces/GeneralLedgerSheet.ts | 10 +- .../server/src/interfaces/JournalReport.ts | 55 +++++---- .../GeneralLedger/GeneralLedgerApplication.ts | 66 +++++++++++ .../GeneralLedger/GeneralLedgerExport.ts | 43 +++++++ .../GeneralLedger/GeneralLedgerService.ts | 2 +- .../GeneralLedger/GeneralLedgerTable.ts | 105 ++++++++++++++++++ .../GeneralLedgerTableInjectable.ts | 45 ++++++++ 9 files changed, 376 insertions(+), 54 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts diff --git a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts index ac3f002a7c..2401edf9d2 100644 --- a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts +++ b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts @@ -2,15 +2,16 @@ import { Router, Request, Response, NextFunction } from 'express'; import { query, ValidationChain } from 'express-validator'; import { Inject, Service } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService'; import BaseFinancialReportController from './BaseFinancialReportController'; import { AbilitySubject, ReportsAction } from '@/interfaces'; import CheckPolicies from '@/api/middleware/CheckPolicies'; +import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { GeneralLedgerApplication } from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication'; @Service() export default class GeneralLedgerReportController extends BaseFinancialReportController { @Inject() - generalLedgetService: GeneralLedgerService; + private generalLedgerApplication: GeneralLedgerApplication; /** * Router constructor. @@ -61,20 +62,43 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo * @param {Response} res - */ async generalLedger(req: Request, res: Response, next: NextFunction) { - const { tenantId, settings } = req; + const { tenantId } = req; const filter = this.matchedQueryData(req); + const accept = this.accepts(req); - try { - const { data, query, meta } = - await this.generalLedgetService.generalLedger(tenantId, filter); - - return res.status(200).send({ - meta: this.transfromToResponse(meta), - data: this.transfromToResponse(data), - query: this.transfromToResponse(query), - }); - } catch (error) { - next(error); + const acceptType = accept.types([ + ACCEPT_TYPE.APPLICATION_JSON, + ACCEPT_TYPE.APPLICATION_JSON_TABLE, + ACCEPT_TYPE.APPLICATION_XLSX, + ACCEPT_TYPE.APPLICATION_CSV, + ]); + // Retrieves the table format. + if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) { + const table = await this.generalLedgerApplication.table(tenantId, filter); + + return res.status(200).send(table); + // Retrieves the csv format. + } else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { + const buffer = await this.generalLedgerApplication.csv(tenantId, filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(buffer); + // Retrieves the xlsx format. + } else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) { + const buffer = this.generalLedgerApplication.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 sheet = await this.generalLedgerApplication.sheet(tenantId, filter); + return res.status(200).send(sheet); } } } diff --git a/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts index ebd6074f47..674d7eb3c4 100644 --- a/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts +++ b/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts @@ -3,14 +3,15 @@ import { Request, Response, Router, NextFunction } from 'express'; import { castArray } from 'lodash'; import { query, oneOf } from 'express-validator'; import BaseFinancialReportController from './BaseFinancialReportController'; -import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService'; import { AbilitySubject, ReportsAction } from '@/interfaces'; import CheckPolicies from '@/api/middleware/CheckPolicies'; +import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { JournalSheetApplication } from '@/services/FinancialStatements/JournalSheet/JournalSheetApplication'; @Service() export default class JournalSheetController extends BaseFinancialReportController { @Inject() - journalService: JournalSheetService; + private journalSheetApp: JournalSheetApplication; /** * Router constructor. @@ -57,28 +58,49 @@ export default class JournalSheetController extends BaseFinancialReportControlle * @param {Request} req - * @param {Response} res - */ - async journal(req: Request, res: Response, next: NextFunction) { - const { tenantId, settings } = req; + private async journal(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; let filter = this.matchedQueryData(req); filter = { ...filter, accountsIds: castArray(filter.accountsIds), }; + 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, + ]); - try { - const { data, query, meta } = await this.journalService.journalSheet( - tenantId, - filter + // Retrieves the json table format. + if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) { + const table = await this.journalSheetApp.table(tenantId, filter); + return res.status(200).send(table); + // Retrieves the csv format. + } else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { + const buffer = this.journalSheetApp.csv(tenantId, filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(buffer); + // Retrieves the xlsx format. + } else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) { + const buffer = await this.journalSheetApp.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 sheet = await this.journalSheetApp.sheet(tenantId, filter); - return res.status(200).send({ - data: this.transfromToResponse(data), - query: this.transfromToResponse(query), - meta: this.transfromToResponse(meta), - }); - } catch (error) { - next(error); + return res.status(200).send(sheet); } } } diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts index bf16620867..5141ddb479 100644 --- a/packages/server/src/interfaces/GeneralLedgerSheet.ts +++ b/packages/server/src/interfaces/GeneralLedgerSheet.ts @@ -1,3 +1,4 @@ +import { IFinancialTable } from "./Table"; export interface IGeneralLedgerSheetQuery { @@ -56,6 +57,8 @@ export interface IGeneralLedgerSheetAccount { closingBalance: IGeneralLedgerSheetAccountBalance, } +export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[]; + export interface IAccountTransaction { id: number, index: number, @@ -78,4 +81,9 @@ export interface IGeneralLedgerMeta { isCostComputeRunning: boolean, organizationName: string, baseCurrency: string, -}; \ No newline at end of file +}; + +export interface IGeneralLedgerTableData extends IFinancialTable { + meta: IGeneralLedgerMeta; + query: IGeneralLedgerSheetQuery; +} \ No newline at end of file diff --git a/packages/server/src/interfaces/JournalReport.ts b/packages/server/src/interfaces/JournalReport.ts index 9786e16348..2a8625f9d0 100644 --- a/packages/server/src/interfaces/JournalReport.ts +++ b/packages/server/src/interfaces/JournalReport.ts @@ -1,36 +1,45 @@ import { IJournalEntry } from './Journal'; +import { IFinancialTable } from './Table'; export interface IJournalReportQuery { - fromDate: Date | string, - toDate: Date | string, + fromDate: Date | string; + toDate: Date | string; numberFormat: { - noCents: boolean, - divideOn1000: boolean, - }, - transactionType: string, - transactionId: string, - - accountsIds: number | number[], - fromRange: number, - toRange: number, + noCents: boolean; + divideOn1000: boolean; + }; + transactionType: string; + transactionId: string; + + accountsIds: number | number[]; + fromRange: number; + toRange: number; } export interface IJournalReportEntriesGroup { - id: string, - entries: IJournalEntry[], - currencyCode: string, - credit: number, - debit: number, - formattedCredit: string, - formattedDebit: string, + id: string; + entries: IJournalEntry[]; + currencyCode: string; + credit: number; + debit: number; + formattedCredit: string; + formattedDebit: string; } export interface IJournalReport { - entries: IJournalReportEntriesGroup[], + entries: IJournalReportEntriesGroup[]; } export interface IJournalSheetMeta { - isCostComputeRunning: boolean, - organizationName: string, - baseCurrency: string, -} \ No newline at end of file + isCostComputeRunning: boolean; + organizationName: string; + baseCurrency: string; +} + +export interface IJournalTable extends IFinancialTable { + query: IJournalReportQuery; + meta: IJournalSheetMeta; +} + + +export type IJournalTableData = IJournalReportEntriesGroup[]; \ No newline at end of file diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts new file mode 100644 index 0000000000..924b0da8c2 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts @@ -0,0 +1,66 @@ +import { Inject } from 'typedi'; +import { + IGeneralLedgerSheetQuery, + IGeneralLedgerTableData, +} from '@/interfaces'; +import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable'; +import { GeneralLedgerExportInjectable } from './GeneralLedgerExport'; +import { GeneralLedgerService } from './GeneralLedgerService'; + +export class GeneralLedgerApplication { + @Inject() + private GLTable: GeneralLedgerTableInjectable; + + @Inject() + private GLExport: GeneralLedgerExportInjectable; + + @Inject() + private GLSheet: GeneralLedgerService; + + /** + * Retrieves the G/L sheet in json format. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + */ + public sheet(tenantId: number, query: IGeneralLedgerSheetQuery) { + return this.GLSheet.generalLedger(tenantId, query); + } + + /** + * Retrieves the G/L sheet in table format. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + * @returns {Promise} + */ + public table( + tenantId: number, + query: IGeneralLedgerSheetQuery + ): Promise { + return this.GLTable.table(tenantId, query); + } + + /** + * Retrieves the G/L sheet in xlsx format. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + * @returns {} + */ + public xlsx( + tenantId: number, + query: IGeneralLedgerSheetQuery + ): Promise { + return this.GLExport.xlsx(tenantId, query); + } + + /** + * Retrieves the G/L sheet in csv format. + * @param {number} tenantId - + * @param {IGeneralLedgerSheetQuery} query - + */ + public csv( + tenantId: number, + query: IGeneralLedgerSheetQuery + ): Promise { + return this.GLExport.csv(tenantId, query); + } +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts new file mode 100644 index 0000000000..f05c817c2e --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts @@ -0,0 +1,43 @@ +import { IGeneralLedgerSheetQuery } from '@/interfaces'; +import { TableSheet } from '@/lib/Xlsx/TableSheet'; +import { Inject, Service } from 'typedi'; +import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable'; + +@Service() +export class GeneralLedgerExportInjectable { + @Inject() + private generalLedgerTable: GeneralLedgerTableInjectable; + + /** + * Retrieves the general ledger sheet in XLSX format. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + * @returns {Promise} + */ + public async xlsx(tenantId: number, query: IGeneralLedgerSheetQuery) { + const table = await this.generalLedgerTable.table(tenantId, query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the general ledger sheet in CSV format. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + * @returns {Promise} + */ + public async csv( + tenantId: number, + query: IGeneralLedgerSheetQuery + ): Promise { + const table = await this.generalLedgerTable.table(tenantId, query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 012a05b947..3ce5461be0 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -15,7 +15,7 @@ const ERRORS = { }; @Service() -export default class GeneralLedgerService { +export class GeneralLedgerService { @Inject() tenancy: TenancyService; diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts new file mode 100644 index 0000000000..938052ceb3 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -0,0 +1,105 @@ +import * as R from 'ramda'; +import { + IGeneralLedgerSheetAccount, + IGeneralLedgerSheetData, + IGeneralLedgerSheetQuery, + ITableColumn, + ITableColumnAccessor, + ITableRow, +} from '@/interfaces'; +import FinancialSheet from '../FinancialSheet'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; +import { FinancialTable } from '../FinancialTable'; +import { tableRowMapper } from '@/utils'; + +export class GeneralLedgerTable extends R.compose( + FinancialTable, + FinancialSheetStructure +)(FinancialSheet) { + private data: IGeneralLedgerSheetData; + private query: IGeneralLedgerSheetQuery; + + /** + * Creates an instance of `GeneralLedgerTable`. + * @param {IGeneralLedgerSheetData} data + * @param {IGeneralLedgerSheetQuery} query + */ + constructor(data: IGeneralLedgerSheetData, query: IGeneralLedgerSheetQuery) { + super(); + + this.data = data; + this.query = query; + } + + /** + * Retrieves the common table accessors. + * @returns {ITableColumnAccessor[]} + */ + private commonColumnsAccessors(): ITableColumnAccessor[] { + return [ + { key: 'date', accessor: 'date' }, + { key: 'reference_type', accessor: 'referenceTypeFormatted' }, + { key: 'reference_number', accessor: 'reference_number' }, + { key: 'currency_code', accessor: 'currencyCode' }, + { key: 'credit', accessor: 'formattedCredit' }, + { key: 'debit', accessor: 'formattedDebit' }, + { key: 'running_balance', accessor: 'formattedRunningBalance' }, + ]; + } + + /** + * Retrieves the common table columns. + * @returns {ITableColumn[]} + */ + private commonColumns(): ITableColumn[] { + return [ + { key: 'date', label: 'Date' }, + { key: 'reference_type', label: 'Reference Type' }, + { key: 'reference_number', label: 'Reference Number' }, + { key: 'currency_code', label: 'Currency Code' }, + { key: 'credit', label: 'Credit' }, + { key: 'debit', label: 'Debit' }, + { key: 'running_balance', label: 'Running Balance' }, + ]; + } + + /** + * Maps the given account node to the table rows. + * @param {IGeneralLedgerSheetAccount} account + * @returns {ITableRow} + */ + private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => { + const columns = this.commonColumnsAccessors(); + + return tableRowMapper(account, columns, {}); + }; + + /** + * Maps the given account node to table rows. + * @param {IGeneralLedgerSheetAccount[]} accounts + * @returns {ITableRow[]} + */ + private accountsMapper = ( + accounts: IGeneralLedgerSheetAccount[] + ): ITableRow[] => { + return R.compose(R.map(this.accountMapper))(accounts); + }; + + /** + * Retrieves the table rows. + * @returns {ITableRow[]} + */ + public tableRows(): ITableRow[] { + return R.compose(this.accountsMapper)(this.data); + } + + /** + * Retrieves the table columns. + * @returns {ITableColumn[]} + */ + public tableColumns(): ITableColumn[] { + const columns = this.commonColumns(); + + return R.compose(this.tableColumnsCellIndexing)(columns); + } +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts new file mode 100644 index 0000000000..467e8bf4c2 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts @@ -0,0 +1,45 @@ +import { + IGeneralLedgerSheetQuery, + IGeneralLedgerTableData, +} from '@/interfaces'; +import { Inject, Service } from 'typedi'; +import { GeneralLedgerService } from './GeneralLedgerService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { GeneralLedgerTable } from './GeneralLedgerTable'; + +@Service() +export class GeneralLedgerTableInjectable { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private GLSheet: GeneralLedgerService; + + /** + * Retrieves the G/L table. + * @param {number} tenantId + * @param {IGeneralLedgerSheetQuery} query + * @returns {Promise} + */ + public async table( + tenantId: number, + query: IGeneralLedgerSheetQuery + ): Promise { + const { + data: sheetData, + query: sheetQuery, + meta: sheetMeta, + } = await this.GLSheet.generalLedger(tenantId, query); + + const table = new GeneralLedgerTable(sheetData, sheetQuery); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableRows(), + }, + query: sheetQuery, + meta: sheetMeta, + }; + } +}