From 5062d891e18181e3c4202338d98c74729911f23c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 1 Jan 2024 20:43:16 +0200 Subject: [PATCH 1/9] feat: journal sheet export --- .../server/src/api/controllers/Sales/PaymentReceives.ts | 3 ++- packages/server/src/models/Contact.ts | 3 +++ .../JournalSheet/JournalSheetApplication.ts | 0 .../JournalSheet/JournalSheetRepository.ts | 0 .../JournalSheet/JournalSheetTable.ts | 0 .../JournalSheet/JournalSheetTableInjectable.ts | 0 .../SalesByItems/SalesByItemsApplication.ts | 0 .../SalesByItems/SalesByItemsTable.ts | 0 .../Sales/Invoices/SendSaleInvoiceMailReminder.ts | 9 +++++---- .../PaymentReceives/PaymentReceiveMailNotificationJob.ts | 3 --- .../services/Sales/Receipts/SaleReceiptApplication.ts | 1 + packages/webapp/src/components/Dashboard/Dashboard.tsx | 2 +- .../webapp/src/containers/Accounts/AccountsDataTable.tsx | 2 +- .../src/containers/Authentication/Authentication.tsx | 2 +- 14 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetRepository.ts create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts create mode 100644 packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts create mode 100644 packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 0bef1e60d..823a13f95 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -531,7 +531,6 @@ export default class PaymentReceivesController extends BaseController { * @param {Request} req * @param {Response} res * @param {NextFunction} next - * @returns */ public sendPaymentReceiveByMail = async ( req: Request, @@ -546,6 +545,8 @@ export default class PaymentReceivesController extends BaseController { includeOptionals: false, } ); + console.log(req.params); + try { await this.paymentReceiveApplication.notifyPaymentByMail( tenantId, diff --git a/packages/server/src/models/Contact.ts b/packages/server/src/models/Contact.ts index d63a2ea60..69661f639 100644 --- a/packages/server/src/models/Contact.ts +++ b/packages/server/src/models/Contact.ts @@ -2,6 +2,9 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class Contact extends TenantModel { + email: string; + displayName: string; + /** * Table name */ diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetRepository.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetRepository.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index b5389a8a0..f16db0172 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -7,6 +7,7 @@ import { DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, } from './constants'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendInvoiceMailReminder { @@ -66,10 +67,10 @@ export class SendInvoiceMailReminder { ) { const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId); - const messageOpts = { - ...localMessageOpts, - ...messageOptions, - }; + const messageOpts = parseAndValidateMailOptions( + localMessageOpts, + messageOptions + ); const mail = new Mail() .setSubject(messageOpts.subject) .setTo(messageOpts.to) diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts index 236a33758..b29570d42 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts @@ -1,5 +1,4 @@ import Container, { Service } from 'typedi'; -import events from '@/subscribers/events'; import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification'; @Service() @@ -22,8 +21,6 @@ export class PaymentReceiveMailNotificationJob { const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data; const paymentMail = Container.get(SendPaymentReceiveMailNotification); - console.log(tenantId, paymentReceiveId, messageDTO); - try { await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO); done(); diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 459d9c62e..d4c87df29 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -177,6 +177,7 @@ export class SaleReceiptApplication { * Sends the receipt mail of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId + * @param {SaleReceiptMailOptsDTO} messageOpts * @returns {Promise} */ public sendSaleReceiptMail( diff --git a/packages/webapp/src/components/Dashboard/Dashboard.tsx b/packages/webapp/src/components/Dashboard/Dashboard.tsx index 2637128cf..467963849 100644 --- a/packages/webapp/src/components/Dashboard/Dashboard.tsx +++ b/packages/webapp/src/components/Dashboard/Dashboard.tsx @@ -51,8 +51,8 @@ export default function Dashboard() { - + diff --git a/packages/webapp/src/containers/Accounts/AccountsDataTable.tsx b/packages/webapp/src/containers/Accounts/AccountsDataTable.tsx index dfa4b90be..edcfe2e18 100644 --- a/packages/webapp/src/containers/Accounts/AccountsDataTable.tsx +++ b/packages/webapp/src/containers/Accounts/AccountsDataTable.tsx @@ -32,7 +32,7 @@ function AccountsDataTable({ // #withAlertsDialog openAlert, - // #withDial + // #withDialog openDialog, // #withDrawerActions diff --git a/packages/webapp/src/containers/Authentication/Authentication.tsx b/packages/webapp/src/containers/Authentication/Authentication.tsx index 8d2887849..8b05570c4 100644 --- a/packages/webapp/src/containers/Authentication/Authentication.tsx +++ b/packages/webapp/src/containers/Authentication/Authentication.tsx @@ -8,9 +8,9 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'; import authenticationRoutes from '@/routes/authentication'; import { Icon, FormattedMessage as T } from '@/components'; import { useIsAuthenticated } from '@/hooks/state'; +import { AuthMetaBootProvider } from './AuthMetaBoot'; import '@/style/pages/Authentication/Auth.scss'; -import { AuthMetaBootProvider } from './AuthMetaBoot'; export function Authentication() { const to = { pathname: '/' }; From 276ef1c907a068e24f9a3e1cbaa2368a9dac7090 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 2 Jan 2024 21:53:37 +0200 Subject: [PATCH 2/9] feat(server): journal sheet csv/xlsx export --- .../JournalSheet/JournalSheetApplication.ts | 59 ++++++++++++ .../JournalSheet/JournalSheetExport.ts | 43 +++++++++ .../JournalSheet/JournalSheetService.ts | 10 +- .../JournalSheet/JournalSheetTable.ts | 95 +++++++++++++++++++ .../JournalSheetTableInjectable.ts | 39 ++++++++ 5 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts index e69de29bb..4c403ff58 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts @@ -0,0 +1,59 @@ +import { Inject } from 'typedi'; +import { JournalSheetService } from './JournalSheetService'; +import { JournalSheetTableInjectable } from './JournalSheetTableInjectable'; +import { IJournalReportQuery, IJournalTable } from '@/interfaces'; +import { JournalSheetExportInjectable } from './JournalSheetExport'; + +export class JournalSheetApplication { + @Inject() + private journalSheetTable: JournalSheetTableInjectable; + + @Inject() + private journalSheet: JournalSheetService; + + @Inject() + private journalExport: JournalSheetExportInjectable; + + /** + * Retrieves the journal sheet. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns {} + */ + public sheet(tenantId: number, query: IJournalReportQuery) { + return this.journalSheet.journalSheet(tenantId, query); + } + + /** + * Retrieves the journal sheet in table format. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns {Promise} + */ + public table( + tenantId: number, + query: IJournalReportQuery + ): Promise { + return this.journalSheetTable.table(tenantId, query); + } + + /** + * Retrieves the journal sheet in xlsx format. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns + */ + public xlsx(tenantId: number, query: IJournalReportQuery) { + return this.journalExport.xlsx(tenantId, query); + } + + /** + * Retrieves the journal sheet in csv format. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns + */ + public csv(tenantId: number, query: IJournalReportQuery) { + return this.journalExport.csv(tenantId, query); + } +} diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts new file mode 100644 index 000000000..815c0a308 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts @@ -0,0 +1,43 @@ +import { Inject, Service } from 'typedi'; +import { TableSheet } from '@/lib/Xlsx/TableSheet'; +import { IJournalReportQuery } from '@/interfaces'; +import { JournalSheetTableInjectable } from './JournalSheetTableInjectable'; + +@Service() +export class JournalSheetExportInjectable { + @Inject() + private journalSheetTable: JournalSheetTableInjectable; + + /** + * Retrieves the trial balance sheet in XLSX format. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns {Promise} + */ + public async xlsx(tenantId: number, query: IJournalReportQuery) { + const table = await this.journalSheetTable.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 {IJournalReportQuery} query + * @returns {Promise} + */ + public async csv( + tenantId: number, + query: IJournalReportQuery + ): Promise { + const table = await this.journalSheetTable.table(tenantId, query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index fe0a071ff..afb0b10f3 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -1,16 +1,15 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces'; - import JournalSheet from './JournalSheet'; import TenancyService from '@/services/Tenancy/TenancyService'; import Journal from '@/services/Accounting/JournalPoster'; import InventoryService from '@/services/Inventory/Inventory'; -import { parseBoolean, transformToMap } from 'utils'; import { Tenant } from '@/system/models'; +import { parseBoolean, transformToMap } from 'utils'; @Service() -export default class JournalSheetService { +export class JournalSheetService { @Inject() tenancy: TenancyService; @@ -80,11 +79,6 @@ export default class JournalSheetService { ...this.defaultQuery, ...query, }; - this.logger.info('[journal] trying to calculate the report.', { - tenantId, - filter, - }); - const tenant = await Tenant.query() .findById(tenantId) .withGraphFetched('metadata'); diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts index e69de29bb..2a376a854 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts @@ -0,0 +1,95 @@ +import * as R from 'ramda'; +import { + IJournalReport, + IJournalReportEntriesGroup, + IJournalReportQuery, + IJournalTableData, + ITableColumn, + ITableColumnAccessor, + ITableRow, +} from '@/interfaces'; +import { tableRowMapper } from '@/utils'; +import { FinancialTable } from '../FinancialTable'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; +import FinancialSheet from '../FinancialSheet'; + +export class JournalSheetTable extends R.compose( + FinancialTable, + FinancialSheetStructure +)(FinancialSheet) { + private data: IJournalTableData; + private query: IJournalReportQuery; + private i18n: any; + + constructor(data: IJournalTableData, query: IJournalReportQuery, i18n: any) { + super(); + this.data = data; + this.query = query; + this.i18n = i18n; + } + + /** + * 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' }, + ]; + }; + + private commonColumns(): ITableColumn[] { + return [ + { key: 'date', label: 'Date' }, + { key: 'reference_type', label: 'Reference Type' }, + { key: 'reference_type', label: 'Reference Number' }, + { key: 'currency_code', label: 'Currency Code' }, + { key: 'credit', label: 'Credit' }, + { key: 'debit', label: 'Debit' }, + ]; + } + + /** + * + */ + private entryGroupMapper = (group: IJournalReportEntriesGroup) => { + const columns = this.commonColumnsAccessors(); + + return tableRowMapper(group, columns, {}); + }; + + /** + * + */ + private entryMapper = () => {}; + + /** + * + */ + private entriesGroupsMapper = (entries: IJournalReportEntriesGroup[]) => { + return R.compose(R.map(this.entryGroupMapper))(entries); + }; + + /** + * Retrieves the table data rows. + * @returns {ITableRow[]} + */ + public tableData(): ITableRow[] { + return R.compose(this.entriesGroupsMapper)(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/JournalSheet/JournalSheetTableInjectable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts index e69de29bb..0754d78f8 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts @@ -0,0 +1,39 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject } from 'typedi'; +import { JournalSheetService } from './JournalSheetService'; +import { IJournalReportQuery, IJournalTable } from '@/interfaces'; +import { JournalSheetTable } from './JournalSheetTable'; + +export class JournalSheetTableInjectable { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private journalSheetService: JournalSheetService; + + /** + * Retrieves the journal sheet in table format. + * @param {number} tenantId + * @param {IJournalReportQuery} query + * @returns {Promise} + */ + public async table( + tenantId: number, + query: IJournalReportQuery + ): Promise { + const journal = await this.journalSheetService.journalSheet( + tenantId, + query + ); + const table = new JournalSheetTable(journal.data, journal.query, {}); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableData(), + }, + query: journal.query, + meta: journal.meta, + }; + } +} From e6a3daa2c37b47514eabf7afd0ecc75f5c9c7b0c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 2 Jan 2024 21:54:10 +0200 Subject: [PATCH 3/9] 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 ac3f002a7..2401edf9d 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 ebd6074f4..674d7eb3c 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 bf1662086..5141ddb47 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 9786e1634..2a8625f9d 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 000000000..924b0da8c --- /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 000000000..f05c817c2 --- /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 012a05b94..3ce5461be 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 000000000..938052ceb --- /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 000000000..467e8bf4c --- /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, + }; + } +} From 60b1bc9ed71ecbb32a28f56a9bf767b1c5d843a7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 4 Jan 2024 17:22:13 +0200 Subject: [PATCH 4/9] feat(server): wip journal and general ledger table layer --- .../src/interfaces/GeneralLedgerSheet.ts | 1 + .../server/src/interfaces/JournalReport.ts | 8 +- .../GeneralLedger/GeneralLedger.ts | 2 + .../GeneralLedger/GeneralLedgerTable.ts | 66 +++++++-- .../JournalSheet/JournalSheet.ts | 6 +- .../JournalSheet/JournalSheetService.ts | 22 +-- .../JournalSheet/JournalSheetTable.ts | 136 +++++++++++++++--- .../FinancialStatements/JournalSheet/types.ts | 6 + 8 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/JournalSheet/types.ts diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts index 5141ddb47..9951bf3b2 100644 --- a/packages/server/src/interfaces/GeneralLedgerSheet.ts +++ b/packages/server/src/interfaces/GeneralLedgerSheet.ts @@ -37,6 +37,7 @@ export interface IGeneralLedgerSheetAccountTransaction { referenceType?: string, date: Date|string, + dateFormatted: string; }; export interface IGeneralLedgerSheetAccountBalance { diff --git a/packages/server/src/interfaces/JournalReport.ts b/packages/server/src/interfaces/JournalReport.ts index 2a8625f9d..823e8b887 100644 --- a/packages/server/src/interfaces/JournalReport.ts +++ b/packages/server/src/interfaces/JournalReport.ts @@ -18,6 +18,7 @@ export interface IJournalReportQuery { export interface IJournalReportEntriesGroup { id: string; + dateFormatted: string; entries: IJournalEntry[]; currencyCode: string; credit: number; @@ -41,5 +42,10 @@ export interface IJournalTable extends IFinancialTable { meta: IJournalSheetMeta; } +export type IJournalTableData = IJournalReportEntriesGroup[]; -export type IJournalTableData = IJournalReportEntriesGroup[]; \ No newline at end of file +export interface IJournalSheet { + data: IJournalTableData; + query: IJournalReportQuery; + meta: IJournalSheetMeta; +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 512ed37d7..507b9deb4 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -10,6 +10,7 @@ import { IContact, } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; +import moment from 'moment'; /** * General ledger sheet. @@ -88,6 +89,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { const newEntry = { date: entry.date, + dateFromatted: moment(entry.date).format('YYYY/MM/DD'), entryId: entry.id, referenceType: entry.referenceType, diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 938052ceb..2529105f7 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { IGeneralLedgerSheetAccount, + IGeneralLedgerSheetAccountTransaction, IGeneralLedgerSheetData, IGeneralLedgerSheetQuery, ITableColumn, @@ -35,15 +36,34 @@ export class GeneralLedgerTable extends R.compose( * Retrieves the common table accessors. * @returns {ITableColumnAccessor[]} */ - private commonColumnsAccessors(): ITableColumnAccessor[] { + private accountColumnsAccessors(): ITableColumnAccessor[] { + return [ + { key: 'date', accessor: 'name' }, + { key: 'account_name', accessor: '_empty_' }, + { key: 'reference_type', accessor: '_empty_' }, + { key: 'reference_number', accessor: '_empty_' }, + { key: 'description', accessor: 'description' }, + { key: 'credit', accessor: '_empty_' }, + { key: 'debit', accessor: '_empty_' }, + { key: 'amount', accessor: 'amount.formattedAmount' }, + { key: 'running_balance', accessor: 'openingBalance.formattedAmount' }, + ]; + } + + /** + * Retrieves the transaction column accessors. + * @returns {ITableColumnAccessor[]} + */ + private transactionColumnAccessors(): ITableColumnAccessor[] { return [ { key: 'date', accessor: 'date' }, + { key: 'account_name', accessor: 'name' }, { key: 'reference_type', accessor: 'referenceTypeFormatted' }, - { key: 'reference_number', accessor: 'reference_number' }, + { key: 'reference_number', accessor: 'referenceNumber' }, { key: 'currency_code', accessor: 'currencyCode' }, { key: 'credit', accessor: 'formattedCredit' }, { key: 'debit', accessor: 'formattedDebit' }, - { key: 'running_balance', accessor: 'formattedRunningBalance' }, + { key: 'running_balance', accessor: 'runningBalance.formattedAmount' }, ]; } @@ -54,24 +74,52 @@ export class GeneralLedgerTable extends R.compose( 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: 'account_name', label: 'Account Name' }, + { key: 'reference_type', label: 'Transaction Type' }, + { key: 'reference_number', label: 'Transaction Number' }, + { key: 'description', label: 'Description' }, { key: 'credit', label: 'Credit' }, { key: 'debit', label: 'Debit' }, + { key: 'amount', label: 'Amount' }, { key: 'running_balance', label: 'Running Balance' }, ]; } + /** + * Maps the given transaction node to table row. + * @param {IGeneralLedgerSheetAccountTransaction} transaction + * @returns {ITableRow} + */ + private transactionMapper = ( + transaction: IGeneralLedgerSheetAccountTransaction + ): ITableRow => { + const columns = this.transactionColumnAccessors(); + + return tableRowMapper(transaction, columns, {}); + }; + + /** + * Maps the given transactions nodes to table rows. + * @param {IGeneralLedgerSheetAccountTransaction[]} transactions + * @returns {ITableRow[]} + */ + private transactionsMapper = ( + transactions: IGeneralLedgerSheetAccountTransaction[] + ): ITableRow[] => { + return R.map(this.transactionMapper)(transactions); + }; + /** * Maps the given account node to the table rows. * @param {IGeneralLedgerSheetAccount} account * @returns {ITableRow} */ private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => { - const columns = this.commonColumnsAccessors(); + const columns = this.accountColumnsAccessors(); + const row = tableRowMapper(account, columns, {}); + const transactions = this.transactionsMapper(account.transactions); - return tableRowMapper(account, columns, {}); + return R.assoc('children', transactions)(row); }; /** @@ -82,7 +130,7 @@ export class GeneralLedgerTable extends R.compose( private accountsMapper = ( accounts: IGeneralLedgerSheetAccount[] ): ITableRow[] => { - return R.compose(R.map(this.accountMapper))(accounts); + return this.mapNodesDeep(accounts, this.accountMapper)l }; /** diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts index ee184a5a1..ec3d5f8ef 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts @@ -6,8 +6,10 @@ import { IJournalReportQuery, IJournalReport, IContact, + IJournalTableData, } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; +import moment from 'moment'; export default class JournalSheet extends FinancialSheet { readonly tenantId: number; @@ -96,6 +98,8 @@ export default class JournalSheet extends FinancialSheet { return { date: groupEntry.date, + dateFormatted: moment(groupEntry.date).format('YYYY/MM/DD'), + referenceType: groupEntry.referenceType, referenceId: groupEntry.referenceId, referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted), @@ -131,7 +135,7 @@ export default class JournalSheet extends FinancialSheet { * Retrieve journal report. * @return {IJournalReport} */ - reportData(): IJournalReport { + reportData(): IJournalTableData { return this.entriesWalker(this.journal.entries); } } diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index afb0b10f3..69a319ff3 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -1,6 +1,11 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; -import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces'; +import { + IJournalReportQuery, + IJournalSheet, + IJournalSheetMeta, + IJournalTableData, +} from '@/interfaces'; import JournalSheet from './JournalSheet'; import TenancyService from '@/services/Tenancy/TenancyService'; import Journal from '@/services/Accounting/JournalPoster'; @@ -11,13 +16,10 @@ import { parseBoolean, transformToMap } from 'utils'; @Service() export class JournalSheetService { @Inject() - tenancy: TenancyService; + private tenancy: TenancyService; @Inject() - inventoryService: InventoryService; - - @Inject('logger') - logger: any; + private inventoryService: InventoryService; /** * Default journal sheet filter queyr. @@ -66,9 +68,13 @@ export class JournalSheetService { /** * Journal sheet. * @param {number} tenantId - * @param {IJournalSheetFilterQuery} query + * @param {IJournalReportQuery} query + * @returns {Promise} */ - async journalSheet(tenantId: number, query: IJournalReportQuery) { + async journalSheet( + tenantId: number, + query: IJournalReportQuery + ): Promise { const i18n = this.tenancy.i18n(tenantId); const { accountRepository, transactionsRepository, contactRepository } = this.tenancy.repositories(tenantId); diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts index 2a376a854..2854154e2 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts @@ -1,5 +1,6 @@ import * as R from 'ramda'; import { + IJournalEntry, IJournalReport, IJournalReportEntriesGroup, IJournalReportQuery, @@ -12,6 +13,7 @@ import { tableRowMapper } from '@/utils'; import { FinancialTable } from '../FinancialTable'; import { FinancialSheetStructure } from '../FinancialSheetStructure'; import FinancialSheet from '../FinancialSheet'; +import { first } from 'lodash'; export class JournalSheetTable extends R.compose( FinancialTable, @@ -21,6 +23,12 @@ export class JournalSheetTable extends R.compose( private query: IJournalReportQuery; private i18n: any; + /** + * Constructor method. + * @param {IJournalTableData} data + * @param {IJournalReportQuery} query + * @param i18n + */ constructor(data: IJournalTableData, query: IJournalReportQuery, i18n: any) { super(); this.data = data; @@ -32,47 +40,141 @@ export class JournalSheetTable extends R.compose( * Retrieves the common table accessors. * @returns {ITableColumnAccessor[]} */ - private commonColumnsAccessors = (): ITableColumnAccessor[] => { + private groupColumnsAccessors = (): ITableColumnAccessor[] => { return [ { key: 'date', accessor: 'date' }, - { key: 'reference_type', accessor: 'referenceTypeFormatted' }, - { key: 'reference_number', accessor: 'reference_number' }, - { key: 'currency_code', accessor: 'currencyCode' }, + { key: 'transaction_type', accessor: 'referenceTypeFormatted' }, + { key: 'transaction_number', accessor: 'referenceNumber' }, + { key: 'description', accessor: 'entry.description' }, + { key: 'account_code', accessor: 'entry.accountCode' }, + { key: 'account_name', accessor: 'entry.accountName' }, + { key: 'credit', accessor: 'entry.formattedCredit' }, + { key: 'debit', accessor: 'entry.formattedDebit' }, + ]; + }; + + /** + * Retrieves the group entry accessors. + * @returns {ITableColumnAccessor[]} + */ + private entryColumnsAccessors = (): ITableColumnAccessor[] => { + return [ + { key: 'date', accessor: '_empty_' }, + { key: 'transaction_type', accessor: '_empty_' }, + { key: 'transaction_number', accessor: '_empty_' }, + { key: 'description', accessor: 'description' }, + { key: 'account_code', accessor: 'accountCode' }, + { key: 'account_name', accessor: 'accountName' }, { key: 'credit', accessor: 'formattedCredit' }, { key: 'debit', accessor: 'formattedDebit' }, ]; }; + private totalEntryColumnAccessors = (): ITableColumnAccessor[] => { + return [ + { key: 'date', accessor: '_empty_' }, + { key: 'transaction_type', accessor: '_empty_' }, + { key: 'transaction_number', accessor: '_empty_' }, + { key: 'description', accessor: '_empty_' }, + { key: 'account_code', accessor: '_empty_' }, + { key: 'account_name', accessor: '_empty_' }, + { key: 'credit', accessor: 'formattedCredit' }, + { key: 'debit', accessor: 'formattedDebit' }, + ]; + }; + + /** + * Retrieves the common columns. + * @returns {ITableColumn[]} + */ private commonColumns(): ITableColumn[] { return [ { key: 'date', label: 'Date' }, - { key: 'reference_type', label: 'Reference Type' }, - { key: 'reference_type', label: 'Reference Number' }, - { key: 'currency_code', label: 'Currency Code' }, + { key: 'transaction_type', label: 'Transaction Type' }, + { key: 'transaction_number', label: 'Num.' }, + { key: 'description', label: 'Description' }, + { key: 'account_code', label: 'Acc. Code' }, + { key: 'account_name', label: 'Account' }, { key: 'credit', label: 'Credit' }, { key: 'debit', label: 'Debit' }, ]; } /** - * + * Maps the group and first entry to table row. + * @param {IJournalReportEntriesGroup} group + * @returns {ITableRow} */ - private entryGroupMapper = (group: IJournalReportEntriesGroup) => { - const columns = this.commonColumnsAccessors(); + private firstEntryGroupMapper = ( + group: IJournalReportEntriesGroup + ): ITableRow => { + const meta = { + rowTypes: [ROW_TYPE.ENTRY], + }; + const computedGroup = { ...group, entry: first(group.entries) }; + const columns = this.groupColumnsAccessors(); + return tableRowMapper(computedGroup, columns, meta); + }; - return tableRowMapper(group, columns, {}); + /** + * Maps the given group entry to table rows. + * @param {IJournalEntry} entry + * @returns {ITableRow} + */ + private entryMapper = (entry: IJournalEntry): ITableRow => { + const columns = this.entryColumnsAccessors(); + const meta = { + rowTypes: [ROW_TYPE.ENTRY], + }; + return tableRowMapper(entry, columns, meta); }; /** - * + * Maps the given group entries to table rows. + * @param {IJournalReportEntriesGroup} group + * @returns {ITableRow[]} */ - private entryMapper = () => {}; + private entriesMapper = (group: IJournalReportEntriesGroup): ITableRow[] => { + const entries = R.remove(0, 1, group.entries); + + return R.map(this.entryMapper, entries); + }; /** - * + * Maps the given group entry to total table row. + * @param {IJournalReportEntriesGroup} group + * @returns {ITableRow} + */ + public totalEntryMapper = (group: IJournalReportEntriesGroup): ITableRow => { + const total = this.totalEntryColumnAccessors(); + const meta = { + rowTypes: [ROW_TYPE.TOTAL], + }; + return tableRowMapper(group, total, meta); + }; + + /** + * Maps the entry group to table rows. + * @param {IJournalReportEntriesGroup} group - + * @returns {ITableRow} + */ + private groupMapper = (group: IJournalReportEntriesGroup): ITableRow[] => { + const firstRow = this.firstEntryGroupMapper(group); + const lastRows = this.entriesMapper(group); + const totalRow = this.totalEntryMapper(group); + + return [firstRow, ...lastRows, totalRow]; + }; + + /** + * Maps the given group entries to table rows. + * @param {IJournalReportEntriesGroup[]} entries - + * @returns {ITableRow[]} */ - private entriesGroupsMapper = (entries: IJournalReportEntriesGroup[]) => { - return R.compose(R.map(this.entryGroupMapper))(entries); + private groupsMapper = ( + entries: IJournalReportEntriesGroup[] + ): ITableRow[] => { + return R.compose(R.flatten, R.map(this.groupMapper))(entries); }; /** @@ -80,7 +182,7 @@ export class JournalSheetTable extends R.compose( * @returns {ITableRow[]} */ public tableData(): ITableRow[] { - return R.compose(this.entriesGroupsMapper)(this.data); + return R.compose(this.groupsMapper)(this.data); } /** diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/types.ts b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts new file mode 100644 index 000000000..f71970251 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts @@ -0,0 +1,6 @@ + + +enum ROW_TYPE { + ENTRY = 'ENTRY', + TOTAL = 'TOTAL' +}; \ No newline at end of file From c71836ec27921ecbf60b5087cb914389f138b2e2 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 4 Jan 2024 21:43:57 +0200 Subject: [PATCH 5/9] feat: wip general ledger table --- .../GeneralLedger/GeneralLedger.ts | 2 +- .../GeneralLedger/GeneralLedgerTable.ts | 113 +++++++++++++++--- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 507b9deb4..6a96ce24c 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -89,7 +89,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { const newEntry = { date: entry.date, - dateFromatted: moment(entry.date).format('YYYY/MM/DD'), + dateFormatted: moment(entry.date).format('YYYY/MM/DD'), entryId: entry.id, referenceType: entry.referenceType, diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 2529105f7..e9f3c68b2 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -1,5 +1,6 @@ import * as R from 'ramda'; import { + IColumnMapperMeta, IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccountTransaction, IGeneralLedgerSheetData, @@ -46,7 +47,7 @@ export class GeneralLedgerTable extends R.compose( { key: 'credit', accessor: '_empty_' }, { key: 'debit', accessor: '_empty_' }, { key: 'amount', accessor: 'amount.formattedAmount' }, - { key: 'running_balance', accessor: 'openingBalance.formattedAmount' }, + { key: 'running_balance', accessor: 'closingBalance.formattedAmount' }, ]; } @@ -56,14 +57,51 @@ export class GeneralLedgerTable extends R.compose( */ private transactionColumnAccessors(): ITableColumnAccessor[] { return [ - { key: 'date', accessor: 'date' }, - { key: 'account_name', accessor: 'name' }, + { key: 'date', accessor: 'dateFormatted' }, + { key: 'account_name', accessor: 'account.name' }, { key: 'reference_type', accessor: 'referenceTypeFormatted' }, { key: 'reference_number', accessor: 'referenceNumber' }, - { key: 'currency_code', accessor: 'currencyCode' }, + { key: 'description', accessor: 'description' }, { key: 'credit', accessor: 'formattedCredit' }, { key: 'debit', accessor: 'formattedDebit' }, - { key: 'running_balance', accessor: 'runningBalance.formattedAmount' }, + { key: 'amount', accessor: 'formattedAmount' }, + { key: 'running_balance', accessor: 'formattedRunningBalance' }, + ]; + } + + /** + * Retrieves the opening row column accessors. + * @returns {ITableRowIColumnMapperMeta[]} + */ + private openingBalanceColumnsAccessors(): IColumnMapperMeta[] { + return [ + { key: 'date', accessor: 'dateFormatted' }, + { key: 'account_name', value: 'Opening Balance' }, + { key: 'reference_type', accessor: '_empty_' }, + { key: 'reference_number', accessor: '_empty_' }, + { key: 'description', accessor: 'description' }, + { key: 'credit', accessor: '_empty_' }, + { key: 'debit', accessor: '_empty_' }, + { key: 'amount', accessor: 'openingBalance.formattedAmount' }, + { key: 'running_balance', accessor: '_empty' }, + ]; + } + + /** + * Closing balance row column accessors. + * @returns {ITableColumnAccessor[]} + */ + private closingBalanceColumnAccessors(): IColumnMapperMeta[] { + return [ + { key: 'date', accessor: 'dateFormatted' }, + { key: 'account_name', value: 'Closing Balance' }, + { key: 'reference_type', accessor: '_empty_' }, + { key: 'reference_number', accessor: '_empty_' }, + { key: 'description', accessor: '_empty_' }, + { key: 'credit', accessor: '_empty_' }, + { key: 'debit', accessor: '_empty_' }, + { key: 'amount', accessor: 'closingBalance.formattedAmount' }, + { key: 'running_balance', accessor: '_empty_' }, ]; } @@ -90,13 +128,17 @@ export class GeneralLedgerTable extends R.compose( * @param {IGeneralLedgerSheetAccountTransaction} transaction * @returns {ITableRow} */ - private transactionMapper = ( - transaction: IGeneralLedgerSheetAccountTransaction - ): ITableRow => { - const columns = this.transactionColumnAccessors(); + private transactionMapper = R.curry( + ( + account: IGeneralLedgerSheetAccount, + transaction: IGeneralLedgerSheetAccountTransaction + ): ITableRow => { + const columns = this.transactionColumnAccessors(); + const data = { ...transaction, account }; - return tableRowMapper(transaction, columns, {}); - }; + return tableRowMapper(data, columns, {}); + } + ); /** * Maps the given transactions nodes to table rows. @@ -104,9 +146,50 @@ export class GeneralLedgerTable extends R.compose( * @returns {ITableRow[]} */ private transactionsMapper = ( - transactions: IGeneralLedgerSheetAccountTransaction[] + account: IGeneralLedgerSheetAccount ): ITableRow[] => { - return R.map(this.transactionMapper)(transactions); + const transactionMapper = this.transactionMapper(account); + + return R.map(transactionMapper)(account.transactions); + }; + + /** + * Maps the given account node to opening balance table row. + * @param {IGeneralLedgerSheetAccount} account + * @returns {ITableRow} + */ + private openingBalanceMapper = ( + account: IGeneralLedgerSheetAccount + ): ITableRow => { + const columns = this.openingBalanceColumnsAccessors(); + + return tableRowMapper(account, columns, {}); + }; + + /** + * Maps the given account node to closing balance table row. + * @param {IGeneralLedgerSheetAccount} account + * @returns {ITableRow} + */ + private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => { + const columns = this.closingBalanceColumnAccessors(); + + return tableRowMapper(account, columns, {}); + }; + + /** + * Maps the given account node to transactions table rows. + * @param {IGeneralLedgerSheetAccount} account + * @returns {ITableRow[]} + */ + private transactionsNode = ( + account: IGeneralLedgerSheetAccount + ): ITableRow[] => { + const openingBalance = this.openingBalanceMapper(account); + const transactions = this.transactionsMapper(account); + const closingBalance = this.closingBalanceMapper(account); + + return [openingBalance, ...transactions, closingBalance]; }; /** @@ -117,7 +200,7 @@ export class GeneralLedgerTable extends R.compose( private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => { const columns = this.accountColumnsAccessors(); const row = tableRowMapper(account, columns, {}); - const transactions = this.transactionsMapper(account.transactions); + const transactions = this.transactionsNode(account); return R.assoc('children', transactions)(row); }; @@ -130,7 +213,7 @@ export class GeneralLedgerTable extends R.compose( private accountsMapper = ( accounts: IGeneralLedgerSheetAccount[] ): ITableRow[] => { - return this.mapNodesDeep(accounts, this.accountMapper)l + return this.mapNodesDeep(accounts, this.accountMapper); }; /** From 79f3f1b63d891c78e692897b1802274e1d461568 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 6 Jan 2024 20:16:22 +0200 Subject: [PATCH 6/9] feat: wip journal and general ledger dyanmic columns --- .../JournalSheet/JournalSheetTable.ts | 40 +++- .../FinancialStatements/JournalSheet/types.ts | 3 +- .../GeneralLedger/GeneralLedgerActionsBar.tsx | 18 +- ...ralLedgerHeaderDimensionsPanelProvider.tsx | 2 +- .../GeneralLedger/GeneralLedgerProvider.tsx | 1 + .../GeneralLedger/GeneralLedgerTable.tsx | 10 +- .../GeneralLedger/components.tsx | 210 ++++++++++-------- .../GeneralLedger/dynamicColumns.ts | 117 ++++++++++ .../Journal/JournalActionsBar.tsx | 18 +- .../Journal/JournalProvider.tsx | 1 + .../Journal/JournalTable.tsx | 12 +- .../Journal/components.tsx | 174 +++++++++------ .../Journal/dynamicColumns.ts | 134 +++++++++++ .../FinancialStatements/Journal/utils.tsx | 1 - .../src/hooks/query/financialReports.tsx | 88 ++++++-- 15 files changed, 615 insertions(+), 214 deletions(-) create mode 100644 packages/webapp/src/containers/FinancialStatements/GeneralLedger/dynamicColumns.ts create mode 100644 packages/webapp/src/containers/FinancialStatements/Journal/dynamicColumns.ts diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts index 2854154e2..e1f4ec8fe 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts @@ -1,7 +1,8 @@ import * as R from 'ramda'; +import { first } from 'lodash'; import { + IColumnMapperMeta, IJournalEntry, - IJournalReport, IJournalReportEntriesGroup, IJournalReportQuery, IJournalTableData, @@ -13,7 +14,7 @@ import { tableRowMapper } from '@/utils'; import { FinancialTable } from '../FinancialTable'; import { FinancialSheetStructure } from '../FinancialSheetStructure'; import FinancialSheet from '../FinancialSheet'; -import { first } from 'lodash'; +import { ROW_TYPE } from './types'; export class JournalSheetTable extends R.compose( FinancialTable, @@ -70,6 +71,10 @@ export class JournalSheetTable extends R.compose( ]; }; + /** + * Retrieves the total entry column accessors. + * @returns {ITableColumnAccessor[]} + */ private totalEntryColumnAccessors = (): ITableColumnAccessor[] => { return [ { key: 'date', accessor: '_empty_' }, @@ -83,6 +88,23 @@ export class JournalSheetTable extends R.compose( ]; }; + /** + * Retrieves the total entry column accessors. + * @returns {IColumnMapperMeta[]} + */ + private blankEnrtyColumnAccessors = (): IColumnMapperMeta[] => { + return [ + { key: 'date', value: '' }, + { key: 'transaction_type', value: '' }, + { key: 'transaction_number', value: '' }, + { key: 'description', value: '' }, + { key: 'account_code', value: '' }, + { key: 'account_name', value: '' }, + { key: 'credit', value: '' }, + { key: 'debit', value: '' }, + ]; + }; + /** * Retrieves the common columns. * @returns {ITableColumn[]} @@ -113,6 +135,7 @@ export class JournalSheetTable extends R.compose( }; const computedGroup = { ...group, entry: first(group.entries) }; const columns = this.groupColumnsAccessors(); + return tableRowMapper(computedGroup, columns, meta); }; @@ -153,6 +176,16 @@ export class JournalSheetTable extends R.compose( return tableRowMapper(group, total, meta); }; + /** + * Retrieves the blank entry row. + * @returns {ITableRow} + */ + private blankEntryMapper = (): ITableRow => { + const columns = this.blankEnrtyColumnAccessors(); + const meta = {}; + return tableRowMapper({} as IJournalEntry, columns, meta); + }; + /** * Maps the entry group to table rows. * @param {IJournalReportEntriesGroup} group - @@ -162,8 +195,9 @@ export class JournalSheetTable extends R.compose( const firstRow = this.firstEntryGroupMapper(group); const lastRows = this.entriesMapper(group); const totalRow = this.totalEntryMapper(group); + const blankRow = this.blankEntryMapper(); - return [firstRow, ...lastRows, totalRow]; + return [firstRow, ...lastRows, totalRow, blankRow]; }; /** diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/types.ts b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts index f71970251..6eff84957 100644 --- a/packages/server/src/services/FinancialStatements/JournalSheet/types.ts +++ b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts @@ -1,6 +1,5 @@ - -enum ROW_TYPE { +export enum ROW_TYPE { ENTRY = 'ENTRY', TOTAL = 'TOTAL' }; \ No newline at end of file diff --git a/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.tsx b/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.tsx index 971da31d9..e450c78bb 100644 --- a/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.tsx +++ b/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.tsx @@ -12,6 +12,7 @@ import { import classNames from 'classnames'; import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components'; +import { GeneralLedgerSheetExportMenu } from './components'; import { useGeneralLedgerContext } from './GeneralLedgerProvider'; import { compose } from '@/utils'; @@ -84,11 +85,18 @@ function GeneralLedgerActionsBar({ icon={} text={} /> -