refactor: financial reports to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-18 22:32:45 +02:00
parent 6dd854178d
commit dfc5674088
151 changed files with 5264 additions and 1296 deletions

View File

@@ -49,6 +49,7 @@
"cache-manager-redis-store": "^3.0.1", "cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"deepdash": "^5.3.9",
"express-validator": "^7.2.0", "express-validator": "^7.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fp-ts": "^2.16.9", "fp-ts": "^2.16.9",
@@ -75,6 +76,7 @@
"ramda": "^0.30.1", "ramda": "^0.30.1",
"redis": "^4.7.0", "redis": "^4.7.0",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"remeda": "^2.19.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"serialize-interceptor": "^1.1.7", "serialize-interceptor": "^1.1.7",
"strategy": "^1.1.1", "strategy": "^1.1.1",
@@ -82,7 +84,8 @@
"uuid": "^10.0.0", "uuid": "^10.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yup": "^0.28.1", "yup": "^0.28.1",
"zod": "^3.23.8" "zod": "^3.23.8",
"mathjs": "^9.4.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
@@ -93,12 +96,14 @@
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@types/yup": "^0.29.13", "@types/yup": "^0.29.13",
"@types/mathjs": "^6.0.12",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"mustache": "^3.0.3",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
@@ -106,8 +111,7 @@
"ts-loader": "^9.4.3", "ts-loader": "^9.4.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3", "typescript": "^5.1.3"
"mustache": "^3.0.3"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [

View File

@@ -180,6 +180,7 @@ export const events = {
* Sales estimates service. * Sales estimates service.
*/ */
saleEstimate: { saleEstimate: {
onViewed: 'onSaleEstimateViewed',
onPdfViewed: 'onSaleEstimatePdfViewed', onPdfViewed: 'onSaleEstimatePdfViewed',
onCreating: 'onSaleEstimateCreating', onCreating: 'onSaleEstimateCreating',
@@ -212,9 +213,7 @@ export const events = {
onPreMailSend: 'onSaleEstimatePreMailSend', onPreMailSend: 'onSaleEstimatePreMailSend',
onMailSend: 'onSaleEstimateMailSend', onMailSend: 'onSaleEstimateMailSend',
onMailSent: 'onSaleEstimateMailSend', onMailSent: 'onSaleEstimateMailSent',
onViewed: 'onSaleEstimateViewed',
}, },
/** /**
@@ -753,4 +752,24 @@ export const events = {
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted', onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
onAccountUpdated: 'onStripeAccountUpdated', onAccountUpdated: 'onStripeAccountUpdated',
}, },
// Reports
reports: {
onBalanceSheetViewed: 'onBalanceSheetViewed',
onTrialBalanceSheetView: 'onTrialBalanceSheetViewed',
onProfitLossSheetViewed: 'onProfitLossSheetViewed',
onCashflowStatementViewed: 'onCashflowStatementViewed',
onGeneralLedgerViewed: 'onGeneralLedgerViewed',
onJournalViewed: 'onJounralViewed',
onReceivableAgingViewed: 'onReceivableAgingViewed',
onPayableAgingViewed: 'onPayableAgingViewed',
onCustomerBalanceSummaryViewed: 'onInventoryValuationViewed',
onVendorBalanceSummaryViewed: 'onVendorBalanceSummaryViewed',
onInventoryValuationViewed: 'onCustomerBalanceSummaryViewed',
onCustomerTransactionsViewed: 'onCustomerTransactionsViewed',
onVendorTransactionsViewed: 'onVendorTransactionsViewed',
onSalesByItemViewed: 'onSalesByItemViewed',
onPurchasesByItemViewed: 'onPurchasesByItemViewed',
},
}; };

View File

@@ -1,2 +1,3 @@
export type Constructor = new (...args: any[]) => {}; export type Constructor = new (...args: any[]) => {};
export type GConstructor<T> = new (...args: any[]) => T;

View File

@@ -14,6 +14,7 @@ import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
import { Model } from 'objection'; import { Model } from 'objection';
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem'; import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
// import AccountSettings from './Account.Settings'; // import AccountSettings from './Account.Settings';
// import { DEFAULT_VIEWS } from '@/modules/Accounts/constants'; // import { DEFAULT_VIEWS } from '@/modules/Accounts/constants';
// import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder'; // import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
@@ -406,10 +407,10 @@ export class Account extends TenantBaseModel {
* @param {Object} options * @param {Object} options
*/ */
static toNestedArray(accounts, options = { children: 'children' }) { static toNestedArray(accounts, options = { children: 'children' }) {
// return flatToNestedArray(accounts, { return flatToNestedArray(accounts, {
// id: 'id', id: 'id',
// parentId: 'parentAccountId', parentId: 'parentAccountId',
// }); });
} }
/** /**

View File

@@ -1,6 +1,6 @@
import { Controller, Param, Post } from '@nestjs/common'; import { Controller, Param, Post } from '@nestjs/common';
import { BankAccountsApplication } from './BankAccountsApplication.service'; import { BankAccountsApplication } from './BankAccountsApplication.service';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('banking/accounts') @Controller('banking/accounts')
@ApiTags('banking-accounts') @ApiTags('banking-accounts')
@@ -11,6 +11,14 @@ export class BankAccountsController {
@ApiOperation({ @ApiOperation({
summary: 'Disconnect the bank connection of the given bank account.', summary: 'Disconnect the bank connection of the given bank account.',
}) })
@ApiResponse({
status: 200,
description: 'Bank account disconnected successfully.',
})
@ApiResponse({
status: 404,
description: 'Bank account not found.',
})
async disconnectBankAccount(@Param('id') bankAccountId: number) { async disconnectBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.disconnectBankAccount(bankAccountId); return this.bankAccountsApplication.disconnectBankAccount(bankAccountId);
} }
@@ -19,6 +27,14 @@ export class BankAccountsController {
@ApiOperation({ @ApiOperation({
summary: 'Refresh the bank account transactions.', summary: 'Refresh the bank account transactions.',
}) })
@ApiResponse({
status: 200,
description: 'Bank account transactions refreshed successfully.',
})
@ApiResponse({
status: 404,
description: 'Bank account not found.',
})
async refreshBankAccount(@Param('id') bankAccountId: number) { async refreshBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.refreshBankAccount(bankAccountId); return this.bankAccountsApplication.refreshBankAccount(bankAccountId);
} }
@@ -27,6 +43,14 @@ export class BankAccountsController {
@ApiOperation({ @ApiOperation({
summary: 'Pause transactions syncing of the given bank account.', summary: 'Pause transactions syncing of the given bank account.',
}) })
@ApiResponse({
status: 200,
description: 'Bank account transactions paused successfully.',
})
@ApiResponse({
status: 404,
description: 'Bank account not found.',
})
async pauseBankAccount(@Param('id') bankAccountId: number) { async pauseBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.pauseBankAccount(bankAccountId); return this.bankAccountsApplication.pauseBankAccount(bankAccountId);
} }
@@ -35,6 +59,14 @@ export class BankAccountsController {
@ApiOperation({ @ApiOperation({
summary: 'Resume transactions syncing of the given bank account.', summary: 'Resume transactions syncing of the given bank account.',
}) })
@ApiResponse({
status: 200,
description: 'Bank account transactions resumed successfully.',
})
@ApiResponse({
status: 404,
description: 'Bank account not found.',
})
async resumeBankAccount(@Param('id') bankAccountId: number) { async resumeBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.resumeBankAccount(bankAccountId); return this.bankAccountsApplication.resumeBankAccount(bankAccountId);
} }

View File

@@ -1,9 +1,16 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TableSheetPdf } from './TableSheetPdf';
import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByItems.module'; import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByItems.module';
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
//
@Module({ @Module({
providers: [TableSheetPdf], providers: [],
imports: [PurchasesByItemsModule], imports: [
PurchasesByItemsModule,
CustomerBalanceSummaryModule,
SalesByItemsModule,
GeneralLedgerModule
],
}) })
export class FinancialStatementsModule {} export class FinancialStatementsModule {}

View File

@@ -3,14 +3,16 @@ import { memoize } from 'lodash';
import { import {
IAccountTransactionsGroupBy, IAccountTransactionsGroupBy,
IFinancialDatePeriodsUnit, IFinancialDatePeriodsUnit,
IFinancialSheetTotalPeriod,
IFormatNumberSettings, IFormatNumberSettings,
} from '../types/Report.types'; } from '../types/Report.types';
import { dateRangeFromToCollection } from '@/utils/date-range-collection'; import { dateRangeFromToCollection } from '@/utils/date-range-collection';
import { FinancialDateRanges } from './FinancialDateRanges'; import { FinancialDateRanges } from './FinancialDateRanges';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialDatePeriods = <T extends Constructor>(Base: T) => export const FinancialDatePeriods = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.compose(FinancialDateRanges)(Base) { class extends R.compose(FinancialDateRanges)(Base) {
/** /**
* Retrieves the date ranges from the given from date to the given to date. * Retrieves the date ranges from the given from date to the given to date.
@@ -19,9 +21,9 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
* @param {string} unit * @param {string} unit
*/ */
public getDateRanges = memoize( public getDateRanges = memoize(
(fromDate: Date, toDate: Date, unit: string) => { (fromDate: Date, toDate: Date, unit: moment.unitOfTime.StartOf) => {
return dateRangeFromToCollection(fromDate, toDate, unit); return dateRangeFromToCollection(fromDate, toDate, unit);
} },
); );
/** /**
@@ -35,7 +37,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
total: number, total: number,
fromDate: Date, fromDate: Date,
toDate: Date, toDate: Date,
overrideSettings?: IFormatNumberSettings overrideSettings?: IFormatNumberSettings,
): IFinancialSheetTotalPeriod => { ): IFinancialSheetTotalPeriod => {
return { return {
fromDate: this.getDateMeta(fromDate), fromDate: this.getDateMeta(fromDate),
@@ -55,7 +57,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
total: number, total: number,
fromDate: Date, fromDate: Date,
toDate: Date, toDate: Date,
overrideSettings: IFormatNumberSettings = {} overrideSettings: IFormatNumberSettings = {},
) => { ) => {
return this.getDatePeriodMeta(total, fromDate, toDate, { return this.getDatePeriodMeta(total, fromDate, toDate, {
money: true, money: true,
@@ -79,8 +81,8 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
node: any, node: any,
fromDate: Date, fromDate: Date,
toDate: Date, toDate: Date,
index: number index: number,
) => any ) => any,
) => { ) => {
const curriedCallback = R.curry(callback)(node); const curriedCallback = R.curry(callback)(node);
// Retrieves memorized date ranges. // Retrieves memorized date ranges.
@@ -88,7 +90,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
return dateRanges.map((dateRange, index) => { return dateRanges.map((dateRange, index) => {
return curriedCallback(dateRange.fromDate, dateRange.toDate, index); return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
}); });
} },
); );
/** /**
* Retrieve the accounts transactions group type from display columns by. * Retrieve the accounts transactions group type from display columns by.
@@ -96,7 +98,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
* @returns {IAccountTransactionsGroupBy} * @returns {IAccountTransactionsGroupBy}
*/ */
public getGroupByFromDisplayColumnsBy = ( public getGroupByFromDisplayColumnsBy = (
columnsBy: IFinancialDatePeriodsUnit columnsBy: IFinancialDatePeriodsUnit,
): IAccountTransactionsGroupBy => { ): IAccountTransactionsGroupBy => {
const paris = { const paris = {
week: IAccountTransactionsGroupBy.Day, week: IAccountTransactionsGroupBy.Day,

View File

@@ -1,14 +1,17 @@
import moment from 'moment'; import moment from 'moment';
import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types'; import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialDateRanges = <T extends Constructor>(Base: T) => export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends Base { class extends Base {
/** /**
* Retrieve previous period (PP) date of the given date. * Retrieve previous period (PP) date of the given date.
* @param {Date} fromDate - * @param {Date} date - Date.
* @param {Date} toDate - * @param {number} value - Value.
* @param {IFinancialDatePeriodsUnit} unit - * @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @returns {Date} * @returns {Date}
*/ */
public getPreviousPeriodDate = ( public getPreviousPeriodDate = (
@@ -20,10 +23,10 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
}; };
/** /**
* Retrieves the different * Retrieves the different between two dates.
* @param {Date} fromDate * @param {Date} fromDate
* @param {Date} toDate * @param {Date} toDate
* @returns * @returns {number}
*/ */
public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => { public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => {
return moment(toDate).diff(fromDate, 'days') + 1; return moment(toDate).diff(fromDate, 'days') + 1;
@@ -31,8 +34,11 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
/** /**
* Retrieves the periods period dates. * Retrieves the periods period dates.
* @param {Date} fromDate - * @param {Date} fromDate - From date.
* @param {Date} toDate - * @param {Date} toDate - To date.
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @param {number} amount - Amount of time.
* @returns {IDateRange}
*/ */
public getPreviousPeriodDateRange = ( public getPreviousPeriodDateRange = (
fromDate: Date, fromDate: Date,
@@ -65,9 +71,9 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
/** /**
* Retrieves the previous period (PP) date range of date periods columns. * Retrieves the previous period (PP) date range of date periods columns.
* @param {Date} fromDate - * @param {Date} fromDate - From date.
* @param {Date} toDate - * @param {Date} toDate - To date.
* @param {IFinancialDatePeriodsUnit} * @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @returns {IDateRange} * @returns {IDateRange}
*/ */
public getPPDatePeriodDateRange = ( public getPPDatePeriodDateRange = (

View File

@@ -1,12 +1,16 @@
import * as mathjs from 'mathjs'; import * as mathjs from 'mathjs';
import * as R from 'ramda'; import * as R from 'ramda';
import { compose } from 'lodash/fp';
import { omit, get, mapValues } from 'lodash'; import { omit, get, mapValues } from 'lodash';
import { FinancialSheetStructure } from './FinancialSheetStructure'; import { FinancialSheetStructure } from './FinancialSheetStructure';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialEvaluateEquation = <T extends Constructor>(Base: T) => export const FinancialEvaluateEquation = <
class extends compose(FinancialSheetStructure)(Base) { T extends GConstructor<FinancialSheet>,
>(
Base: T
) =>
class FinancialEvaluateEquation extends R.compose(FinancialSheetStructure)(Base) {
/** /**
* Evauluate equaation string with the given scope table. * Evauluate equaation string with the given scope table.
* @param {string} equation - * @param {string} equation -
@@ -34,7 +38,6 @@ export const FinancialEvaluateEquation = <T extends Constructor>(Base: T) =>
} }
return acc; return acc;
}, },
{}
); );
}; };

View File

@@ -1,9 +1,12 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { get, isEmpty } from 'lodash'; import { get, isEmpty } from 'lodash';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialHorizTotals = <T extends Constructor>(Base: T) => export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
class extends Base { Base: T,
) =>
class FinancialHorizTotals extends Base {
/** /**
* *
*/ */

View File

@@ -4,9 +4,14 @@ import {
IFinancialNodeWithPreviousPeriod, IFinancialNodeWithPreviousPeriod,
} from '../types/Report.types'; } from '../types/Report.types';
import * as R from 'ramda'; import * as R from 'ramda';
import { Constructor, GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { FinancialDatePeriods } from './FinancialDatePeriods';
export const FinancialPreviousPeriod = (Base) => export const FinancialPreviousPeriod = <T extends GConstructor<FinancialSheet>>(
class extends Base { Base: T,
) =>
class extends R.compose(FinancialDatePeriods)(Base) {
// --------------------------- // ---------------------------
// # Common Node. // # Common Node.
// --------------------------- // ---------------------------
@@ -16,16 +21,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod} * @returns {IFinancialNodeWithPreviousPeriod}
*/ */
public assocPreviousPeriodPercentageNode = ( public assocPreviousPeriodPercentageNode = (
accountNode: IProfitLossSheetAccountNode accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => { ): IFinancialNodeWithPreviousPeriod => {
const percentage = this.getPercentageBasis( const percentage = this.getPercentageBasis(
accountNode.previousPeriod.amount, accountNode.previousPeriod.amount,
accountNode.previousPeriodChange.amount accountNode.previousPeriodChange.amount,
); );
return R.assoc( return R.assoc(
'previousPeriodPercentage', 'previousPeriodPercentage',
this.getPercentageAmountMeta(percentage), this.getPercentageAmountMeta(percentage),
accountNode accountNode,
); );
}; };
@@ -35,16 +40,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod} * @returns {IFinancialNodeWithPreviousPeriod}
*/ */
public assocPreviousPeriodChangeNode = ( public assocPreviousPeriodChangeNode = (
accountNode: IProfitLossSheetAccountNode accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => { ): IFinancialNodeWithPreviousPeriod => {
const change = this.getAmountChange( const change = this.getAmountChange(
accountNode.total.amount, accountNode.total.amount,
accountNode.previousPeriod.amount accountNode.previousPeriod.amount,
); );
return R.assoc( return R.assoc(
'previousPeriodChange', 'previousPeriodChange',
this.getAmountMeta(change), this.getAmountMeta(change),
accountNode accountNode,
); );
}; };
@@ -57,16 +62,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod} * @returns {IFinancialNodeWithPreviousPeriod}
*/ */
public assocPreviousPeriodTotalPercentageNode = ( public assocPreviousPeriodTotalPercentageNode = (
accountNode: IProfitLossSheetAccountNode accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => { ): IFinancialNodeWithPreviousPeriod => {
const percentage = this.getPercentageBasis( const percentage = this.getPercentageBasis(
accountNode.previousPeriod.amount, accountNode.previousPeriod.amount,
accountNode.previousPeriodChange.amount accountNode.previousPeriodChange.amount,
); );
return R.assoc( return R.assoc(
'previousPeriodPercentage', 'previousPeriodPercentage',
this.getPercentageTotalAmountMeta(percentage), this.getPercentageTotalAmountMeta(percentage),
accountNode accountNode,
); );
}; };
@@ -76,16 +81,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod} * @returns {IFinancialNodeWithPreviousPeriod}
*/ */
public assocPreviousPeriodTotalChangeNode = ( public assocPreviousPeriodTotalChangeNode = (
accountNode: any accountNode: any,
): IFinancialNodeWithPreviousPeriod => { ): IFinancialNodeWithPreviousPeriod => {
const change = this.getAmountChange( const change = this.getAmountChange(
accountNode.total.amount, accountNode.total.amount,
accountNode.previousPeriod.amount accountNode.previousPeriod.amount,
); );
return R.assoc( return R.assoc(
'previousPeriodChange', 'previousPeriodChange',
this.getTotalAmountMeta(change), this.getTotalAmountMeta(change),
accountNode accountNode,
); );
}; };
@@ -97,19 +102,19 @@ export const FinancialPreviousPeriod = (Base) =>
public assocPreviousPeriodHorizNodeFromToDates = R.curry( public assocPreviousPeriodHorizNodeFromToDates = R.curry(
( (
periodUnit: IFinancialDatePeriodsUnit, periodUnit: IFinancialDatePeriodsUnit,
horizNode: any horizNode: any,
): IFinancialNodeWithPreviousPeriod => { ): IFinancialNodeWithPreviousPeriod => {
const { fromDate: PPFromDate, toDate: PPToDate } = const { fromDate: PPFromDate, toDate: PPToDate } =
this.getPreviousPeriodDateRange( this.getPreviousPeriodDateRange(
horizNode.fromDate.date, horizNode.fromDate.date,
horizNode.toDate.date, horizNode.toDate.date,
periodUnit periodUnit,
); );
return R.compose( return R.compose(
R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)), R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)),
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate)) R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate)),
)(horizNode); )(horizNode);
} },
); );
/** /**
@@ -121,7 +126,7 @@ export const FinancialPreviousPeriod = (Base) =>
public getPPHorizNodesTotalSumation = (index: number, node): number => { public getPPHorizNodesTotalSumation = (index: number, node): number => {
return sumBy( return sumBy(
node.children, node.children,
`horizontalTotals[${index}].previousPeriod.amount` `horizontalTotals[${index}].previousPeriod.amount`,
); );
}; };
}; };

View File

@@ -1,50 +1,54 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { sumBy } from 'lodash' import { sumBy } from 'lodash';
import { import {
IFinancialCommonHorizDatePeriodNode, IFinancialCommonHorizDatePeriodNode,
IFinancialCommonNode, IFinancialCommonNode,
IFinancialNodeWithPreviousYear, IFinancialNodeWithPreviousYear,
} from '../types/Report.types'; } from '../types/Report.types';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { FinancialDatePeriods } from './FinancialDatePeriods';
export const FinancialPreviousYear = (Base) => export const FinancialPreviousYear = <T extends GConstructor<FinancialSheet>>(
class extends Base { Base: T,
) =>
class extends R.compose(FinancialDatePeriods)(Base) {
// --------------------------- // ---------------------------
// # Common Node // # Common Node
// --------------------------- // ---------------------------
/** /**
* Assoc previous year change attribute to account node. * Assoc previous year change attribute to account node.
* @param {IProfitLossSheetAccountNode} accountNode * @param {IFinancialCommonNode & IFinancialNodeWithPreviousYear} accountNode
* @returns {IProfitLossSheetAccountNode} * @returns {IFinancialNodeWithPreviousYear}
*/ */
public assocPreviousYearChangetNode = ( public assocPreviousYearChangetNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => { ): IFinancialNodeWithPreviousYear => {
const change = this.getAmountChange( const change = this.getAmountChange(
node.total.amount, node.total.amount,
node.previousYear.amount node.previousYear.amount,
); );
return R.assoc('previousYearChange', this.getAmountMeta(change), node); return R.assoc('previousYearChange', this.getAmountMeta(change), node);
}; };
/** /**
* Assoc previous year percentage attribute to account node. * Assoc previous year percentage attribute to account node.
*
* % increase = Increase ÷ Original Number × 100. * % increase = Increase ÷ Original Number × 100.
* *
* @param {IProfitLossSheetAccountNode} accountNode * @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode} * @returns {IProfitLossSheetAccountNode}
*/ */
public assocPreviousYearPercentageNode = ( public assocPreviousYearPercentageNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => { ): IFinancialNodeWithPreviousYear => {
const percentage = this.getPercentageBasis( const percentage = this.getPercentageBasis(
node.previousYear.amount, node.previousYear.amount,
node.previousYearChange.amount node.previousYearChange.amount,
); );
return R.assoc( return R.assoc(
'previousYearPercentage', 'previousYearPercentage',
this.getPercentageAmountMeta(percentage), this.getPercentageAmountMeta(percentage),
node node,
); );
}; };
@@ -54,16 +58,16 @@ export const FinancialPreviousYear = (Base) =>
* @returns {IProfitLossSheetAccountNode} * @returns {IProfitLossSheetAccountNode}
*/ */
public assocPreviousYearTotalChangeNode = ( public assocPreviousYearTotalChangeNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => { ): IFinancialNodeWithPreviousYear => {
const change = this.getAmountChange( const change = this.getAmountChange(
node.total.amount, node.total.amount,
node.previousYear.amount node.previousYear.amount,
); );
return R.assoc( return R.assoc(
'previousYearChange', 'previousYearChange',
this.getTotalAmountMeta(change), this.getTotalAmountMeta(change),
node node,
); );
}; };
@@ -73,16 +77,16 @@ export const FinancialPreviousYear = (Base) =>
* @returns {IProfitLossSheetAccountNode} * @returns {IProfitLossSheetAccountNode}
*/ */
public assocPreviousYearTotalPercentageNode = ( public assocPreviousYearTotalPercentageNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => { ): IFinancialNodeWithPreviousYear => {
const percentage = this.getPercentageBasis( const percentage = this.getPercentageBasis(
node.previousYear.amount, node.previousYear.amount,
node.previousYearChange.amount node.previousYearChange.amount,
); );
return R.assoc( return R.assoc(
'previousYearPercentage', 'previousYearPercentage',
this.getPercentageTotalAmountMeta(percentage), this.getPercentageTotalAmountMeta(percentage),
node node,
); );
}; };
@@ -92,27 +96,27 @@ export const FinancialPreviousYear = (Base) =>
* @returns * @returns
*/ */
public assocPreviousYearHorizNodeFromToDates = ( public assocPreviousYearHorizNodeFromToDates = (
horizNode: IFinancialCommonHorizDatePeriodNode horizNode: IFinancialCommonHorizDatePeriodNode,
) => { ) => {
const PYFromDate = this.getPreviousYearDate(horizNode.fromDate.date); const PYFromDate = this.getPreviousYearDate(horizNode.fromDate.date);
const PYToDate = this.getPreviousYearDate(horizNode.toDate.date); const PYToDate = this.getPreviousYearDate(horizNode.toDate.date);
return R.compose( return R.compose(
R.assoc('previousYearToDate', this.getDateMeta(PYToDate)), R.assoc('previousYearToDate', this.getDateMeta(PYToDate)),
R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate)) R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate)),
)(horizNode); )(horizNode);
}; };
/** /**
* Retrieves PP total sumation of the given horiz index node. * Retrieves PP total sumation of the given horiz index node.
* @param {number} index * @param {number} index
* @param {} node * @param {} node
* @returns {number} * @returns {number}
*/ */
public getPYHorizNodesTotalSumation = (index: number, node): number => { public getPYHorizNodesTotalSumation = (index: number, node): number => {
return sumBy( return sumBy(
node.children, node.children,
`horizontalTotals[${index}].previousYear.amount` `horizontalTotals[${index}].previousYear.amount`,
) );
} };
}; };

View File

@@ -1,9 +1,12 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { FinancialSheetStructure } from './FinancialSheetStructure'; import { FinancialSheetStructure } from './FinancialSheetStructure';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSchema = <T extends Constructor>(Base: T) => export const FinancialSchema = <T extends GConstructor<FinancialSheet>>(
class extends R.compose(FinancialSheetStructure)(Base) { Base: T
) =>
class FinancialSchema extends R.compose(FinancialSheetStructure)(Base) {
/** /**
* *
* @returns * @returns
@@ -17,7 +20,7 @@ export const FinancialSchema = <T extends Constructor>(Base: T) =>
* @param {string|number} id * @param {string|number} id
* @returns * @returns
*/ */
publicgetSchemaNodeById = (id: string | number) => { public getSchemaNodeById = (id: string | number) => {
const schema = this.getSchema(); const schema = this.getSchema();
return this.findNodeDeep(schema, (node) => node.id === id); return this.findNodeDeep(schema, (node) => node.id === id);

View File

@@ -1,20 +1,21 @@
import moment from 'moment'; import moment from 'moment';
import { import {
ICashFlowStatementTotal,
IFormatNumberSettings, IFormatNumberSettings,
INumberFormatQuery, INumberFormatQuery,
} from '../types/Report.types'; } from '../types/Report.types';
import { formatNumber } from '@/utils/format-number'; import { formatNumber } from '@/utils/format-number';
import { IFinancialTableTotal } from '../types/Table.types';
export default class FinancialSheet { export class FinancialSheet {
readonly numberFormat: INumberFormatQuery = { public numberFormat: INumberFormatQuery = {
precision: 2, precision: 2,
divideOn1000: false, divideOn1000: false,
showZero: false, showZero: false,
formatMoney: 'total', formatMoney: 'total',
negativeFormat: 'mines', negativeFormat: 'mines',
}; };
readonly baseCurrency: string; public baseCurrency: string;
/** /**
* Transformes the number format query to settings * Transformes the number format query to settings
@@ -109,7 +110,7 @@ export default class FinancialSheet {
protected getAmountMeta( protected getAmountMeta(
amount: number, amount: number,
overrideSettings?: IFormatNumberSettings overrideSettings?: IFormatNumberSettings
): ICashFlowStatementTotal { ): IFinancialTableTotal {
return { return {
amount, amount,
formattedAmount: this.formatNumber(amount, overrideSettings), formattedAmount: this.formatNumber(amount, overrideSettings),
@@ -125,7 +126,7 @@ export default class FinancialSheet {
protected getTotalAmountMeta( protected getTotalAmountMeta(
amount: number, amount: number,
title?: string title?: string
): ICashFlowStatementTotal { ): IFinancialTableTotal {
return { return {
...(title ? { title } : {}), ...(title ? { title } : {}),
amount, amount,

View File

@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { FinancialSheetMeta } from './FinancialSheetMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TableSheetPdf } from './TableSheetPdf';
import { TemplateInjectableModule } from '@/modules/TemplateInjectable/TemplateInjectable.module';
import { ChromiumlyTenancyModule } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.module';
@Module({
imports: [TemplateInjectableModule, ChromiumlyTenancyModule],
providers: [
FinancialSheetMeta,
TenancyContext,
TableSheetPdf,
],
exports: [FinancialSheetMeta, TableSheetPdf],
})
export class FinancialSheetCommonModule {}

View File

@@ -4,10 +4,7 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class FinancialSheetMeta { export class FinancialSheetMeta {
constructor( constructor(private readonly tenancyContext: TenancyContext) {}
private readonly inventoryService: InventoryService,
private readonly tenancyContext: TenancyContext,
) {}
/** /**
* Retrieves the common meta data of the financial sheet. * Retrieves the common meta data of the financial sheet.
@@ -20,8 +17,10 @@ export class FinancialSheetMeta {
const baseCurrency = tenantMetadata.baseCurrency; const baseCurrency = tenantMetadata.baseCurrency;
const dateFormat = tenantMetadata.dateFormat; const dateFormat = tenantMetadata.dateFormat;
const isCostComputeRunning = // const isCostComputeRunning =
this.inventoryService.isItemsCostComputeRunning(tenantId); // this.inventoryService.isItemsCostComputeRunning();
const isCostComputeRunning = false;
return { return {
organizationName, organizationName,

View File

@@ -4,16 +4,20 @@ import {
mapValuesDeepReverse, mapValuesDeepReverse,
mapValuesDeep, mapValuesDeep,
mapValues, mapValues,
condense,
filterDeep, filterDeep,
reduceDeep, reduceDeep,
findValueDeep, findValueDeep,
filterNodesDeep, filterNodesDeep,
} from 'utils/deepdash'; } from '@/utils/deepdash';
import { Constructor } from '@/common/types/Constructor'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSheetStructure = <T extends Constructor>(Base: T) => export const FinancialSheetStructure = <
class extends Base { T extends GConstructor<FinancialSheet>,
>(
Base: T
) =>
class FinancialSheetStructure extends Base {
/** /**
* *
* @param nodes * @param nodes

View File

@@ -2,10 +2,21 @@ import * as R from 'ramda';
import { isEmpty, clone, cloneDeep, omit } from 'lodash'; import { isEmpty, clone, cloneDeep, omit } from 'lodash';
import { increment } from '@/utils/increment'; import { increment } from '@/utils/increment';
import { ITableRow, ITableColumn } from '../types/Table.types'; import { ITableRow, ITableColumn } from '../types/Table.types';
import { IROW_TYPE } from './BalanceSheet/constants'; import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheetStructure } from './FinancialSheetStructure';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheet } from './FinancialSheet';
enum IROW_TYPE {
TOTAL = 'TOTAL',
}
export const FinancialTable = <T extends GConstructor<FinancialSheet>>(
Base: T
) =>
class extends R.pipe(FinancialSheetStructure)(Base) {
public readonly i18n: I18nService;
export const FinancialTable = (Base) =>
class extends Base {
/** /**
* Table columns cell indexing. * Table columns cell indexing.
* @param {ITableColumn[]} columns * @param {ITableColumn[]} columns
@@ -23,13 +34,15 @@ export const FinancialTable = (Base) =>
}); });
}; };
addTotalRow = (node: ITableRow) => { public addTotalRow = (node: ITableRow) => {
const clonedNode = clone(node); const clonedNode = clone(node);
if (clonedNode.children) { if (clonedNode.children) {
const cells = cloneDeep(node.cells); const cells = cloneDeep(node.cells);
cells[0].value = this.i18n.__('financial_sheet.total_row', { cells[0].value = this.i18n.t('financial_sheet.total_row', {
value: cells[0].value, args: {
value: cells[0].value,
},
}); });
clonedNode.children.push({ clonedNode.children.push({

View File

@@ -1,8 +1,18 @@
import moment from 'moment'; import moment from 'moment';
import { ITableColumn, IDateRange, ITableColumnAccessor } from '../types/Table.types'; import { ITableColumn, ITableColumnAccessor } from '../types/Table.types';
import { IDateRange } from '../types/Report.types';
import { Constructor, GConstructor } from '@/common/types/Constructor';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheet } from './FinancialSheet';
export const FinancialTablePreviousPeriod = (Base) => export const FinancialTablePreviousPeriod = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends Base { class extends Base {
public readonly i18n: I18nService;
getTotalPreviousPeriod = () => { getTotalPreviousPeriod = () => {
return this.query.PPToDate; return this.query.PPToDate;
}; };
@@ -24,8 +34,8 @@ export const FinancialTablePreviousPeriod = (Base) =>
return { return {
key: 'previous_period', key: 'previous_period',
label: this.i18n.__(`financial_sheet.previoud_period_date`, { label: this.i18n.t(`financial_sheet.previoud_period_date`, {
date: PPFormatted, args: { date: PPFormatted, }
}), }),
}; };
}; };
@@ -37,7 +47,7 @@ export const FinancialTablePreviousPeriod = (Base) =>
public getPreviousPeriodChangeColumn = (): ITableColumn => { public getPreviousPeriodChangeColumn = (): ITableColumn => {
return { return {
key: 'previous_period_change', key: 'previous_period_change',
label: this.i18n.__('fianncial_sheet.previous_period_change'), label: this.i18n.t('fianncial_sheet.previous_period_change'),
}; };
}; };
@@ -48,7 +58,7 @@ export const FinancialTablePreviousPeriod = (Base) =>
public getPreviousPeriodPercentageColumn = (): ITableColumn => { public getPreviousPeriodPercentageColumn = (): ITableColumn => {
return { return {
key: 'previous_period_percentage', key: 'previous_period_percentage',
label: this.i18n.__('financial_sheet.previous_period_percentage'), label: this.i18n.t('financial_sheet.previous_period_percentage'),
}; };
}; };

View File

@@ -1,18 +1,32 @@
import moment from 'moment'; import moment from 'moment';
import { ITableColumn, ITableColumnAccessor } from '../types/Table.types'; import { ITableColumn, ITableColumnAccessor } from '../types/Table.types';
import { IDateRange } from '../types/Report.types'; import { IDateRange } from '../types/Report.types';
import { GConstructor } from '@/common/types/Constructor';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheet } from './FinancialSheet';
export const FinancialTablePreviousYear = (Base) => export const FinancialTablePreviousYear = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends Base { class extends Base {
getTotalPreviousYear = () => { public readonly i18n: I18nService;
/**
* Retrieves the total previous year date.
* @returns {Date}
*/
public getTotalPreviousYear = () => {
return this.query.PYToDate; return this.query.PYToDate;
}; };
// ------------------------------------ // ------------------------------------
// # Columns. // # Columns.
// ------------------------------------ // ------------------------------------
/** /**
* Retrive previous year total column. * Retrive previous year total column.
* @param {DateRange} previousYear - * @param {DateRange} previousYear -
* @returns {ITableColumn} * @returns {ITableColumn}
*/ */
public getPreviousYearTotalColumn = ( public getPreviousYearTotalColumn = (
@@ -23,8 +37,8 @@ export const FinancialTablePreviousYear = (Base) =>
return { return {
key: 'previous_year', key: 'previous_year',
label: this.i18n.__('financial_sheet.previous_year_date', { label: this.i18n.t('financial_sheet.previous_year_date', {
date: PYFormatted, args: { date: PYFormatted },
}), }),
}; };
}; };
@@ -36,7 +50,7 @@ export const FinancialTablePreviousYear = (Base) =>
public getPreviousYearChangeColumn = (): ITableColumn => { public getPreviousYearChangeColumn = (): ITableColumn => {
return { return {
key: 'previous_year_change', key: 'previous_year_change',
label: this.i18n.__('financial_sheet.previous_year_change'), label: this.i18n.t('financial_sheet.previous_year_change'),
}; };
}; };
@@ -47,7 +61,7 @@ export const FinancialTablePreviousYear = (Base) =>
public getPreviousYearPercentageColumn = (): ITableColumn => { public getPreviousYearPercentageColumn = (): ITableColumn => {
return { return {
key: 'previous_year_percentage', key: 'previous_year_percentage',
label: this.i18n.__('financial_sheet.previous_year_percentage'), label: this.i18n.t('financial_sheet.previous_year_percentage'),
}; };
}; };
@@ -89,7 +103,7 @@ export const FinancialTablePreviousYear = (Base) =>
/** /**
* Retrieves previous year total horizontal column accessor. * Retrieves previous year total horizontal column accessor.
* @param {number} index * @param {number} index - Index.
* @returns {ITableColumnAccessor} * @returns {ITableColumnAccessor}
*/ */
public getPreviousYearTotalHorizAccessor = ( public getPreviousYearTotalHorizAccessor = (
@@ -103,7 +117,7 @@ export const FinancialTablePreviousYear = (Base) =>
/** /**
* Retrieves previous previous year change horizontal column accessor. * Retrieves previous previous year change horizontal column accessor.
* @param {number} index * @param {number} index
* @returns {ITableColumnAccessor} * @returns {ITableColumnAccessor}
*/ */
public getPreviousYearChangeHorizAccessor = ( public getPreviousYearChangeHorizAccessor = (
@@ -117,7 +131,7 @@ export const FinancialTablePreviousYear = (Base) =>
/** /**
* Retrieves previous year percentage horizontal column accessor. * Retrieves previous year percentage horizontal column accessor.
* @param {number} index * @param {number} index
* @returns {ITableColumnAccessor} * @returns {ITableColumnAccessor}
*/ */
public getPreviousYearPercentageHorizAccessor = ( public getPreviousYearPercentageHorizAccessor = (

View File

@@ -1,13 +1,17 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { ITableColumn, ITableData, ITableRow } from './types/Table.types'; import { ITableColumn, ITableData, ITableRow } from '../types/Table.types';
import { FinancialTableStructure } from './common/FinancialTableStructure'; import { FinancialTableStructure } from './FinancialTableStructure';
import { tableClassNames } from './utils'; import { tableClassNames } from '../utils';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service'; import { TemplateInjectable } from '../../TemplateInjectable/TemplateInjectable.service';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '../../ChromiumlyTenancy/ChromiumlyTenancy.service';
@Injectable() @Injectable()
export class TableSheetPdf { export class TableSheetPdf {
/**
* @param {TemplateInjectable} templateInjectable - The template injectable service.
* @param {ChromiumlyTenancy} chromiumlyTenancy - The chromiumly tenancy service.
*/
constructor( constructor(
private readonly templateInjectable: TemplateInjectable, private readonly templateInjectable: TemplateInjectable,
private readonly chromiumlyTenancy: ChromiumlyTenancy, private readonly chromiumlyTenancy: ChromiumlyTenancy,
@@ -60,8 +64,8 @@ export class TableSheetPdf {
/** /**
* Converts the table rows to pdf rows. * Converts the table rows to pdf rows.
* @param {ITableRow[]} rows - * @param {ITableRow[]} rows - The table rows to be converted.
* @returns {ITableRow[]} * @returns {ITableRow[]} - The converted table rows.
*/ */
private tablePdfRows = (rows: ITableRow[]): ITableRow[] => { private tablePdfRows = (rows: ITableRow[]): ITableRow[] => {
const curriedFlatNestedTree = R.curry( const curriedFlatNestedTree = R.curry(
@@ -70,6 +74,8 @@ export class TableSheetPdf {
const flatNestedTree = curriedFlatNestedTree(R.__, { const flatNestedTree = curriedFlatNestedTree(R.__, {
nestedPrefix: '<span style="padding-left: 15px;"></span>', nestedPrefix: '<span style="padding-left: 15px;"></span>',
}); });
// @ts-ignore
return R.compose(tableClassNames, flatNestedTree)(rows); return R.compose(tableClassNames, flatNestedTree)(rows);
}; };
} }

View File

@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
import { BalanceSheetApplication } from './BalanceSheetApplication';
@Module({
providers: [BalanceSheetInjectable, BalanceSheetApplication],
exports: [BalanceSheetInjectable],
})
export class BalanceSheetModule {}

View File

@@ -1,101 +0,0 @@
import moment from 'moment';
import {
IBalanceSheetStatementService,
IBalanceSheetQuery,
IBalanceSheetStatement,
} from './BalanceSheet.types';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class BalanceSheetInjectable {
constructor(
private readonly balanceSheetMeta: BalanceSheetMetaInjectable,
private readonly eventPublisher: EventEmitter2,
) {}
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): IBalanceSheetQuery {
return {
displayColumnsType: 'total',
displayColumnsBy: 'month',
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneZero: false,
noneTransactions: false,
basis: 'cash',
accountIds: [],
percentageOfColumn: false,
percentageOfRow: false,
previousPeriod: false,
previousPeriodAmountChange: false,
previousPeriodPercentageChange: false,
previousYear: false,
previousYearAmountChange: false,
previousYearPercentageChange: false,
};
}
/**
* Retrieve balance sheet statement.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
* @return {IBalanceSheetStatement}
*/
public async balanceSheet(
tenantId: number,
query: IBalanceSheetQuery,
): Promise<IBalanceSheetStatement> {
const filter = {
...this.defaultQuery,
...query,
};
const balanceSheetRepo = new BalanceSheetRepository(models, filter);
// Loads all resources.
await balanceSheetRepo.asyncInitialize();
// Balance sheet report instance.
const balanceSheetInstanace = new BalanceSheetStatementService(
filter,
balanceSheetRepo,
tenant.metadata.baseCurrency,
i18n,
);
// Balance sheet data.
const data = balanceSheetInstanace.reportData();
// Balance sheet meta.
const meta = await this.balanceSheetMeta.meta(tenantId, filter);
// Triggers `onBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
query,
});
return {
query: filter,
data,
meta,
};
}
}

View File

@@ -1,154 +0,0 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import TenancyService from '@/services/Tenancy/TenancyService';
import FinancialSheet from '../FinancialSheet';
import {
ICashFlowStatementService,
ICashFlowStatementQuery,
ICashFlowStatementDOO,
IAccountTransaction,
ICashFlowStatementMeta,
} from '@/interfaces';
import CashFlowStatement from './CashFlow';
import Ledger from '@/services/Accounting/Ledger';
import CashFlowRepository from './CashFlowRepository';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { CashflowSheetMeta } from './CashflowSheetMeta';
@Service()
export default class CashFlowStatementService
extends FinancialSheet
implements ICashFlowStatementService
{
@Inject()
tenancy: TenancyService;
@Inject()
cashFlowRepo: CashFlowRepository;
@Inject()
inventoryService: InventoryService;
@Inject()
private cashflowSheetMeta: CashflowSheetMeta;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): ICashFlowStatementQuery {
return {
displayColumnsType: 'total',
displayColumnsBy: 'day',
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneZero: false,
noneTransactions: false,
basis: 'cash',
};
}
/**
* Retrieve cash at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @retrun {Promise<IAccountTransaction[]>}
*/
private async cashAtBeginningTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const appendPeriodsOperToChain = (trans) =>
R.append(
this.cashFlowRepo.cashAtBeginningPeriodTransactions(tenantId, filter),
trans
);
const promisesChain = R.pipe(
R.append(
this.cashFlowRepo.cashAtBeginningTotalTransactions(tenantId, filter)
),
R.when(
R.always(R.equals(filter.displayColumnsType, 'date_periods')),
appendPeriodsOperToChain
)
)([]);
const promisesResults = await Promise.all(promisesChain);
const transactions = R.flatten(promisesResults);
return transactions;
}
/**
* Retrieve the cash flow sheet statement.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<ICashFlowStatementDOO>}
*/
public async cashFlow(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementDOO> {
const i18n = this.tenancy.i18n(tenantId);
// Retrieve all accounts on the storage.
const accounts = await this.cashFlowRepo.cashFlowAccounts(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const filter = {
...this.defaultQuery,
...query,
};
// Retrieve the accounts transactions.
const transactions = await this.cashFlowRepo.getAccountsTransactions(
tenantId,
filter
);
// Retrieve the net income transactions.
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(
tenantId,
filter
);
// Retrieve the cash at beginning transactions.
const cashAtBeginningTransactions = await this.cashAtBeginningTransactions(
tenantId,
filter
);
// Transformes the transactions to ledgers.
const ledger = Ledger.fromTransactions(transactions);
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
const netIncomeLedger = Ledger.fromTransactions(netIncome);
// Cash flow statement.
const cashFlowInstance = new CashFlowStatement(
accounts,
ledger,
cashLedger,
netIncomeLedger,
filter,
tenant.metadata.baseCurrency,
i18n
);
// Retrieve the cashflow sheet meta.
const meta = await this.cashflowSheetMeta.meta(tenantId, filter);
return {
data: cashFlowInstance.reportData(),
query: filter,
meta,
};
}
}

View File

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

View File

@@ -1,75 +0,0 @@
import { Inject, Service } from 'typedi';
import { CashflowExportInjectable } from './CashflowExportInjectable';
import { ICashFlowStatementQuery } from '@/interfaces';
import CashFlowStatementService from './CashFlowService';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
@Service()
export class CashflowSheetApplication {
@Inject()
private cashflowExport: CashflowExportInjectable;
@Inject()
private cashflowSheet: CashFlowStatementService;
@Inject()
private cashflowTable: CashflowTableInjectable;
@Inject()
private cashflowPdf: CashflowTablePdfInjectable;
/**
* Retrieves the cashflow sheet
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
*/
public async sheet(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowSheet.cashFlow(tenantId, query);
}
/**
* Retrieves the cashflow sheet in table format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
*/
public async table(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowTable.table(tenantId, query);
}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowExport.xlsx(tenantId, query);
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<string> {
return this.cashflowExport.csv(tenantId, query);
}
/**
* Retrieves the cashflow sheet in pdf format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async pdf(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<Buffer> {
return this.cashflowPdf.pdf(tenantId, query);
}
}

View File

@@ -1,37 +0,0 @@
import { Inject, Service } from "typedi";
import { ICashFlowStatementQuery, ICashFlowStatementTable } from "@/interfaces";
import HasTenancyService from "@/services/Tenancy/TenancyService";
import CashFlowTable from "./CashFlowTable";
import CashFlowStatementService from "./CashFlowService";
@Service()
export class CashflowTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private cashflowSheet: CashFlowStatementService;
/**
* Retrieves the cash flow table.
* @returns {Promise<ICashFlowStatementTable>}
*/
public async table(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementTable> {
const i18n = this.tenancy.i18n(tenantId);
const cashflowDOO = await this.cashflowSheet.cashFlow(tenantId, query);
const cashflowTable = new CashFlowTable(cashflowDOO, i18n);
return {
table: {
columns: cashflowTable.tableColumns(),
rows: cashflowTable.tableRows(),
},
query: cashflowDOO.query,
meta: cashflowDOO.meta,
};
}
}

View File

@@ -0,0 +1,204 @@
import { sumBy, isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IContactBalanceSummaryContact,
IContactBalanceSummaryTotal,
IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage,
IContactBalanceSummaryQuery,
} from './ContactBalanceSummary.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Ledger } from '@/modules/Ledger/Ledger';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
export class ContactBalanceSummaryReport extends FinancialSheet {
readonly baseCurrency: string;
readonly ledger: Ledger;
readonly filter: IContactBalanceSummaryQuery;
/**
* Calculates the contact percentage of column.
* @param {number} customerBalance - Contact balance.
* @param {number} totalBalance - Total contacts balance.
* @returns {number}
*/
protected getContactPercentageOfColumn = (
customerBalance: number,
totalBalance: number
): number => {
return totalBalance / customerBalance;
};
/**
* Retrieve the contacts total.
* @param {IContactBalanceSummaryContact} contacts
* @returns {number}
*/
protected getContactsTotal = (
contacts: IContactBalanceSummaryContact[]
): number => {
return sumBy(
contacts,
(contact: IContactBalanceSummaryContact) => contact.total.amount
);
};
/**
* Assoc total percentage of column.
* @param {IContactBalanceSummaryTotal} node
* @returns {IContactBalanceSummaryTotal}
*/
protected assocTotalPercentageOfColumn = (
node: IContactBalanceSummaryTotal
): IContactBalanceSummaryTotal => {
return R.assoc('percentageOfColumn', this.getPercentageMeta(1), node);
};
/**
* Retrieve the contacts total section.
* @param {IContactBalanceSummaryContact[]} contacts
* @returns {IContactBalanceSummaryTotal}
*/
protected getContactsTotalSection = (
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryTotal => {
const customersTotal = this.getContactsTotal(contacts);
const node = {
total: this.getTotalFormat(customersTotal),
};
return R.compose(
R.when(
R.always(this.filter.percentageColumn),
this.assocTotalPercentageOfColumn
)
)(node);
};
/**
* Retrieve the contact summary section with percentage of column.
* @param {number} total
* @param {IContactBalanceSummaryContact} contact
* @returns {IContactBalanceSummaryContact}
*/
private contactCamparsionPercentageOfColumnMapper = (
total: number,
contact: IContactBalanceSummaryContact
): IContactBalanceSummaryContact => {
const amount = this.getContactPercentageOfColumn(
total,
contact.total.amount
);
return {
...contact,
percentageOfColumn: this.getPercentageMeta(amount),
};
};
/**
* Mappes the contacts summary sections with percentage of column.
* @param {IContactBalanceSummaryContact[]} contacts -
* @return {IContactBalanceSummaryContact[]}
*/
protected contactCamparsionPercentageOfColumn = (
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryContact[] => {
const customersTotal = this.getContactsTotal(contacts);
const camparsionPercentageOfColummn = R.curry(
this.contactCamparsionPercentageOfColumnMapper
)(customersTotal);
return contacts.map(camparsionPercentageOfColummn);
};
/**
* Retrieve the contact total format.
* @param {number} amount -
* @return {IContactBalanceSummaryAmount}
*/
protected getContactTotalFormat = (
amount: number
): IContactBalanceSummaryAmount => {
return {
amount,
formattedAmount: this.formatNumber(amount, { money: true }),
currencyCode: this.baseCurrency,
};
};
/**
* Retrieve the total amount of contacts sections.
* @param {number} amount
* @returns {IContactBalanceSummaryAmount}
*/
protected getTotalFormat = (amount: number): IContactBalanceSummaryAmount => {
return {
amount,
formattedAmount: this.formatTotalNumber(amount, { money: true }),
currencyCode: this.baseCurrency,
};
};
/**
* Retrieve the percentage amount object.
* @param {number} amount
* @returns {IContactBalanceSummaryPercentage}
*/
protected getPercentageMeta = (
amount: number
): IContactBalanceSummaryPercentage => {
return {
amount,
formattedAmount: this.formatPercentage(amount),
};
};
/**
* Filters customer has none transactions.
* @param {ICustomerBalanceSummaryCustomer} customer -
* @returns {boolean}
*/
private filterContactNoneTransactions = (
contact: IContactBalanceSummaryContact
): boolean => {
const entries = this.ledger.whereContactId(contact.id).getEntries();
return !isEmpty(entries);
};
/**
* Filters the customer that has zero total amount.
* @param {ICustomerBalanceSummaryCustomer} customer
* @returns {boolean}
*/
private filterContactNoneZero = (
node: IContactBalanceSummaryContact
): boolean => {
return node.total.amount !== 0;
};
/**
* Filters the given customer node;
* @param {ICustomerBalanceSummaryCustomer} customer
*/
private contactNodeFilter = (contact: IContactBalanceSummaryContact) => {
const { noneTransactions, noneZero } = this.filter;
// Conditions pair filter detarminer.
const condsPairFilters = [
[noneTransactions, this.filterContactNoneTransactions],
[noneZero, this.filterContactNoneZero],
];
return allPassedConditionsPass(condsPairFilters)(contact);
};
/**
* Filters the given customers nodes.
* @param {ICustomerBalanceSummaryCustomer[]} nodes
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
protected contactsFilter = (
nodes: IContactBalanceSummaryContact[]
): IContactBalanceSummaryContact[] => {
return nodes.filter(this.contactNodeFilter);
};
}

View File

@@ -0,0 +1,47 @@
import { INumberFormatQuery } from "../../types/Report.types";
export interface IContactBalanceSummaryQuery {
asDate: Date;
numberFormat: INumberFormatQuery;
percentageColumn: boolean;
noneTransactions: boolean;
noneZero: boolean;
}
export interface IContactBalanceSummaryAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IContactBalanceSummaryPercentage {
amount: number;
formattedAmount: string;
}
export interface IContactBalanceSummaryContact {
total: IContactBalanceSummaryAmount;
percentageOfColumn?: IContactBalanceSummaryPercentage;
}
export interface IContactBalanceSummaryTotal {
total: IContactBalanceSummaryAmount;
percentageOfColumn?: IContactBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryData {
customers: IContactBalanceSummaryContact[];
total: IContactBalanceSummaryTotal;
}
export interface ICustomerBalanceSummaryStatement {
data: ICustomerBalanceSummaryData;
columns: {};
query: IContactBalanceSummaryQuery;
}
export interface ICustomerBalanceSummaryService {
customerBalanceSummary(
tenantId: number,
query: IContactBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement>;
}

View File

@@ -0,0 +1,57 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryApplication } from './CustomerBalanceSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/customer-balance-summary')
@ApiTags('reports')
export class CustomerBalanceSummaryController {
constructor(
private readonly customerBalanceSummaryApp: CustomerBalanceSummaryApplication,
) {}
@Get()
@ApiResponse({ status: 200, description: 'Customer balance summary report' })
async customerBalanceSummary(
@Query() filter: ICustomerBalanceSummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the xlsx format.
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.customerBalanceSummaryApp.xlsx(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 csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.customerBalanceSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.customerBalanceSummaryApp.table(filter);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const buffer = await this.customerBalanceSummaryApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
});
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.customerBalanceSummaryApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { CustomerBalanceSummaryApplication } from './CustomerBalanceSummaryApplication';
import { CustomerBalanceSummaryExportInjectable } from './CustomerBalanceSummaryExportInjectable';
import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
import { CustomerBalanceSummaryPdf } from './CustomerBalanceSummaryPdf';
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { CustomerBalanceSummaryController } from './CustomerBalanceSummary.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { CustomerBalanceSummaryRepository } from './CustomerBalanceSummaryRepository';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
imports: [
FinancialSheetCommonModule,
],
controllers: [CustomerBalanceSummaryController],
providers: [
CustomerBalanceSummaryApplication,
CustomerBalanceSummaryExportInjectable,
CustomerBalanceSummaryMeta,
CustomerBalanceSummaryPdf,
CustomerBalanceSummaryService,
CustomerBalanceSummaryTableInjectable,
CustomerBalanceSummaryRepository,
TenancyContext
],
})
export class CustomerBalanceSummaryModule {}

View File

@@ -0,0 +1,112 @@
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import {
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryData,
} from './CustomerBalanceSummary.types';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { ModelObject } from 'objection';
import { INumberFormatQuery } from '../../types/Report.types';
import { Customer } from '@/modules/Customers/models/Customer';
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly ledger: ILedger;
readonly baseCurrency: string;
readonly customers: ModelObject<Customer>[];
readonly filter: ICustomerBalanceSummaryQuery;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
* @param {IJournalPoster} receivableLedger
* @param {ICustomer[]} customers
* @param {ICustomerBalanceSummaryQuery} filter
* @param {string} baseCurrency
*/
constructor(
ledger: ILedger,
customers: ModelObject<Customer>[],
filter: ICustomerBalanceSummaryQuery,
baseCurrency: string
) {
super();
this.ledger = ledger;
this.baseCurrency = baseCurrency;
this.customers = customers;
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
}
/**
* Customer section mapper.
* @param {ModelObject<Customer>} customer
* @returns {ICustomerBalanceSummaryCustomer}
*/
private customerMapper = (
customer: ModelObject<Customer>
): ICustomerBalanceSummaryCustomer => {
const closingBalance = this.ledger
.whereContactId(customer.id)
.getClosingBalance();
return {
id: customer.id,
customerName: customer.displayName,
total: this.getContactTotalFormat(closingBalance),
};
};
/**
* Mappes the customer model object to customer balance summary section.
* @param {ModelObject<Customer>[]} customers - Customers.
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private customersMapper = (
customers: ModelObject<Customer>[]
): ICustomerBalanceSummaryCustomer[] => {
return customers.map(this.customerMapper);
};
/**
* Detarmines whether the customers post filter is active.
* @returns {boolean}
*/
private isCustomersPostFilter = () => {
return isEmpty(this.filter.customersIds);
};
/**
* Retrieve the customers sections of the report.
* @param {ModelObject<Customer>[]} customers
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private getCustomersSection = (
customers: ModelObject<Customer>[]
): ICustomerBalanceSummaryCustomer[] => {
return R.compose(
R.when(this.isCustomersPostFilter, this.contactsFilter),
R.when(
R.always(this.filter.percentageColumn),
this.contactCamparsionPercentageOfColumn
),
this.customersMapper
)(customers);
};
/**
* Retrieve the report statement data.
* @returns {ICustomerBalanceSummaryData}
*/
public reportData = (): ICustomerBalanceSummaryData => {
const customersSections = this.getCustomersSection(this.customers);
const customersTotal = this.getContactsTotalSection(customersSections);
return {
customers: customersSections,
total: customersTotal,
};
};
}

View File

@@ -0,0 +1,60 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
import {
IContactBalanceSummaryQuery,
IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage,
IContactBalanceSummaryTotal,
} from '../ContactBalanceSummary/ContactBalanceSummary.types';
export interface ICustomerBalanceSummaryQuery
extends IContactBalanceSummaryQuery {
customersIds?: number[];
}
export interface ICustomerBalanceSummaryAmount
extends IContactBalanceSummaryAmount {}
export interface ICustomerBalanceSummaryPercentage
extends IContactBalanceSummaryPercentage {}
export interface ICustomerBalanceSummaryCustomer {
id: number;
customerName: string;
total: ICustomerBalanceSummaryAmount;
percentageOfColumn?: ICustomerBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryTotal
extends IContactBalanceSummaryTotal {
total: ICustomerBalanceSummaryAmount;
percentageOfColumn?: ICustomerBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryData {
customers: ICustomerBalanceSummaryCustomer[];
total: ICustomerBalanceSummaryTotal;
}
export interface ICustomerBalanceSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
formattedDateRange: string;
}
export interface ICustomerBalanceSummaryStatement {
data: ICustomerBalanceSummaryData;
query: ICustomerBalanceSummaryQuery;
meta: ICustomerBalanceSummaryMeta;
}
export interface ICustomerBalanceSummaryService {
customerBalanceSummary(
tenantId: number,
query: ICustomerBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement>;
}
export interface ICustomerBalanceSummaryTable extends IFinancialTable {
query: ICustomerBalanceSummaryQuery;
meta: ICustomerBalanceSummaryMeta;
}

View File

@@ -0,0 +1,62 @@
import { CustomerBalanceSummaryExportInjectable } from './CustomerBalanceSummaryExportInjectable';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import { CustomerBalanceSummaryPdf } from './CustomerBalanceSummaryPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CustomerBalanceSummaryApplication {
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
private readonly customerBalanceSummaryExport: CustomerBalanceSummaryExportInjectable,
private readonly customerBalanceSummarySheet: CustomerBalanceSummaryService,
private readonly customerBalanceSummaryPdf: CustomerBalanceSummaryPdf,
) {}
/**
* Retrieves the customer balance sheet in json format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummarySheet>}
*/
public sheet(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummarySheet.customerBalanceSummary(query);
}
/**
* Retrieves the customer balance sheet in json format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummaryTable>}
*/
public table(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryTable.table(query);
}
/**
* Retrieves the customer balance sheet in XLSX format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryExport.xlsx(query);
}
/**
* Retrieves the customer balance sheet in CSV format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public csv(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryExport.csv(query);
}
/**
* Retrieves the customer balance sheet in PDF format.
* @param {number} tenantId
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryPdf.pdf(query);
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class CustomerBalanceSummaryExportInjectable {
/**
* Constructor method.
* @param {CustomerBalanceSummaryTableInjectable} customerBalanceSummaryTable
*/
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ICustomerBalanceSummaryQuery) {
const table = await this.customerBalanceSummaryTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {ICustomerBalanceSummaryQuery} query - Query.
* @returns {Promise<Buffer>}
*/
public async csv(query: ICustomerBalanceSummaryQuery): Promise<string> {
const table = await this.customerBalanceSummaryTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,32 @@
import moment from 'moment';
import {
ICustomerBalanceSummaryMeta,
ICustomerBalanceSummaryQuery,
} from './CustomerBalanceSummary.types';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class CustomerBalanceSummaryMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the customer balance summary meta.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummaryMeta>}
*/
async meta(
query: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedDateRange = `As ${formattedAsDate}`;
return {
...commonMeta,
sheetName: 'Customer Balance Summary',
formattedAsDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CustomerBalanceSummaryPdf {
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given customer balance summary sheet table to pdf.
* @param {IAPAgingSummaryQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: ICustomerBalanceSummaryQuery): Promise<Buffer> {
const table = await this.customerBalanceSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,74 @@
import { map, isEmpty } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Customer } from '@/modules/Customers/models/Customer';
import { ModelObject } from 'objection';
import { ACCOUNT_TYPE } from '@/constants/accounts';
@Injectable()
export class CustomerBalanceSummaryRepository {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction,
@Inject(Customer.name)
private readonly customerModel: typeof Customer,
) {}
/**
* Retrieve the report customers.
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
public async getCustomers(
customersIds: number[],
): Promise<ModelObject<Customer>[]> {
return await this.customerModel
.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
}
/**
* Retrieve the A/R accounts.
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(): Promise<ModelObject<Account>[]> {
return await this.accountModel
.query()
.where('accountType', ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE);
}
/**
* Retrieve the customers credit/debit totals
* @returns
*/
public async getCustomersTransactions(
asDate: any,
): Promise<ModelObject<AccountTransaction>[]> {
// Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await this.accountTransactionModel
.query()
.onBuild((query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
});
return customersTranasctions;
}
}

View File

@@ -0,0 +1,89 @@
import * as R from 'ramda';
import {
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement,
} from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import { CustomerBalanceSummaryRepository } from './CustomerBalanceSummaryRepository';
import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { events } from '@/common/events/events';
import { getCustomerBalanceSummaryDefaultQuery } from './_utils';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class CustomerBalanceSummaryService {
constructor(
private readonly reportRepository: CustomerBalanceSummaryRepository,
private readonly customerBalanceSummaryMeta: CustomerBalanceSummaryMeta,
private readonly eventPublisher: EventEmitter2,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Retrieve the customers ledger entries mapped from accounts transactions.
* @param {Date|string} asDate - The date to retrieve the ledger entries.
* @returns {Promise<ILedgerEntry[]>}
*/
private async getReportCustomersEntries(
asDate: Date | string,
): Promise<ILedgerEntry[]> {
const transactions =
await this.reportRepository.getCustomersTransactions(asDate);
const commonProps = { accountNormal: 'debit', date: asDate };
return R.map(R.merge(commonProps))(transactions);
}
/**
* Retrieve the statment of customer balance summary report.
* @param {ICustomerBalanceSummaryQuery} query - The customer balance summary query.
* @return {Promise<ICustomerBalanceSummaryStatement>}
*/
public async customerBalanceSummary(
query: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryStatement> {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
// Merges the default query and request query.
const filter = { ...getCustomerBalanceSummaryDefaultQuery(), ...query };
// Retrieve the customers list ordered by the display name.
const customers = await this.reportRepository.getCustomers(
query.customersIds,
);
// Retrieve the customers debit/credit totals.
const customersEntries = await this.getReportCustomersEntries(
filter.asDate,
);
// Ledger query.
const ledger = new Ledger(customersEntries);
// Report instance.
const report = new CustomerBalanceSummaryReport(
ledger,
customers,
filter,
tenantMetadata.baseCurrency,
);
// Retrieve the customer balance summary meta.
const meta = await this.customerBalanceSummaryMeta.meta(filter);
// Triggers `onCustomerBalanceSummaryViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerBalanceSummaryViewed,
{
query,
},
);
return {
data: report.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,38 @@
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import {
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryTable,
} from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTable } from './CustomerBalanceSummaryTableRows';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class CustomerBalanceSummaryTableInjectable {
constructor(
private readonly customerBalanceSummaryService: CustomerBalanceSummaryService,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the customer balance sheet in table format.
* @param {ICustomerBalanceSummaryQuery} filter - The customer balance summary query.
* @returns {Promise<ICustomerBalanceSummaryTable>}
*/
public async table(
filter: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryTable> {
const { data, query, meta } =
await this.customerBalanceSummaryService.customerBalanceSummary(filter);
const table = new CustomerBalanceSummaryTable(data, filter, this.i18n);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,156 @@
import * as R from 'ramda';
import { I18nService } from 'nestjs-i18n';
import {
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryTotal,
ICustomerBalanceSummaryQuery,
} from './CustomerBalanceSummary.types';
import {
IColumnMapperMeta,
ITableColumn,
ITableRow,
} from '../../types/Table.types';
import { tableMapper, tableRowMapper } from '../../utils/Table.utils';
enum TABLE_ROWS_TYPES {
CUSTOMER = 'CUSTOMER',
TOTAL = 'TOTAL',
}
export class CustomerBalanceSummaryTable {
public readonly report: ICustomerBalanceSummaryData;
public readonly query: ICustomerBalanceSummaryQuery;
public readonly i18n: I18nService;
/**
* Constructor method.
* @param {ICustomerBalanceSummaryData} report - The report object.
* @param {ICustomerBalanceSummaryQuery} query - The query object.
* @param {I18nService} i18n - The i18n service.
*/
constructor(
report: ICustomerBalanceSummaryData,
query: ICustomerBalanceSummaryQuery,
i18n: I18nService,
) {
this.report = report;
this.i18n = i18n;
this.query = query;
}
/**
* Retrieve percentage columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getPercentageColumnsAccessor = (): IColumnMapperMeta[] => {
return [
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
};
/**
* Retrieve customer node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getCustomerColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', accessor: 'customerName' },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor()),
),
)([]);
};
/**
* Transformes the customers to table rows.
* @param {ICustomerBalanceSummaryCustomer[]} customers
* @returns {ITableRow[]}
*/
private customersTransformer(
customers: ICustomerBalanceSummaryCustomer[],
): ITableRow[] {
const columns = this.getCustomerColumnsAccessor();
return tableMapper(customers, columns, {
rowTypes: [TABLE_ROWS_TYPES.CUSTOMER],
});
}
/**
* Retrieve total node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', value: this.i18n.t('Total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor()),
),
)([]);
};
/**
* Transformes the total to table row.
* @param {ICustomerBalanceSummaryTotal} total
* @returns {ITableRow}
*/
private totalTransformer = (
total: ICustomerBalanceSummaryTotal,
): ITableRow => {
const columns = this.getTotalColumnsAccessor();
return tableRowMapper(total, columns, {
rowTypes: [TABLE_ROWS_TYPES.TOTAL],
});
};
/**
* Transformes the customer balance summary to table rows.
* @param {ICustomerBalanceSummaryData} customerBalanceSummary
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const customers = this.customersTransformer(this.report.customers);
const total = this.totalTransformer(this.report.total);
return customers.length > 0 ? [...customers, total] : [];
}
/**
* Retrieve the report statement columns
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
const columns = [
{
key: 'name',
label: this.i18n.t('contact_summary_balance.account_name'),
},
{ key: 'total', label: this.i18n.t('contact_summary_balance.total') },
];
return R.compose(
R.when(
R.always(this.query.percentageColumn),
R.append({
key: 'percentage_of_column',
label: this.i18n.t('contact_summary_balance.percentage_column'),
}),
),
R.concat(columns),
)([]);
};
}

View File

@@ -0,0 +1,16 @@
export const getCustomerBalanceSummaryDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
percentageColumn: false,
noneZero: false,
noneTransactions: true,
};
};

View File

@@ -0,0 +1,14 @@
export const HtmlTableCustomCss = `
table tr.row-type--total td {
font-weight: 600;
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
table .column--name {
width: 65%;
}
table .column--total,
table .cell--total {
text-align: right;
}
`;

View File

@@ -0,0 +1,61 @@
import { Controller, Get } from '@nestjs/common';
import { Headers, Query, Res } from '@nestjs/common';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/general-ledger')
@ApiTags('reports')
export class GeneralLedgerController {
constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication,
) {}
@Get()
@ApiResponse({ status: 200, description: 'General ledger report' })
public async getGeneralLedger(
@Query() query: IGeneralLedgerSheetQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.generalLedgerApplication.table(query);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.generalLedgerApplication.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.generalLedgerApplication.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(buffer);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.generalLedgerApplication.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.generalLedgerApplication.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,31 @@
import { Module } from '@nestjs/common';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { GeneralLedgerPdf } from './GeneralLedgerPdf';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerController } from './GeneralLedger.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
imports: [
FinancialSheetCommonModule,
AccountsModule
],
providers: [
GeneralLedgerRepository,
GeneralLedgerApplication,
GeneralLedgerPdf,
GeneralLedgerExportInjectable,
GeneralLedgerTableInjectable,
GeneralLedgerService,
GeneralLedgerMeta,
TenancyContext
],
controllers: [GeneralLedgerController],
})
export class GeneralLedgerModule {}

View File

@@ -0,0 +1,390 @@
import { isEmpty, get, last, head } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountBalance,
IGeneralLedgerSheetAccountTransaction,
} from './GeneralLedger.types';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { calculateRunningBalance } from './_utils';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import { I18nService } from 'nestjs-i18n';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ModelObject } from 'objection';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
FinancialSheet,
) {
public query: IGeneralLedgerSheetQuery;
public baseCurrency: string;
public i18n: I18nService;
public repository: GeneralLedgerRepository;
/**
* Constructor method.
* @param {IGeneralLedgerSheetQuery} query -
* @param {GeneralLedgerRepository} repository -
* @param {I18nService} i18n -
*/
constructor(
query: IGeneralLedgerSheetQuery,
repository: GeneralLedgerRepository,
i18n,
) {
super();
this.query = query;
this.numberFormat = this.query.numberFormat;
this.repository = repository;
this.baseCurrency = this.repository.tenant.metadata.currencyCode;
this.i18n = i18n;
}
/**
* Entry mapper.
* @param {ILedgerEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction}
*/
private getEntryRunningBalance(
entry: ILedgerEntry,
openingBalance: number,
runningBalance?: number,
): number {
const lastRunningBalance = runningBalance || openingBalance;
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal,
);
return calculateRunningBalance(amount, lastRunningBalance);
}
/**
* Maps the given ledger entry to G/L transaction.
* @param {ILedgerEntry} entry
* @param {number} runningBalance
* @returns {IGeneralLedgerSheetAccountTransaction}
*/
private transactionMapper(
entry: ILedgerEntry,
runningBalance: number,
): IGeneralLedgerSheetAccountTransaction {
const contact = this.repository.contactsById.get(entry.contactId);
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal,
);
return {
id: entry.id,
date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
referenceType: entry.transactionType,
referenceId: entry.transactionId,
transactionNumber: entry.transactionNumber,
transactionTypeFormatted: this.i18n.t(
getTransactionTypeLabel(
entry.transactionType,
entry.transactionSubType,
),
),
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),
transactionType: entry.transactionType,
index: entry.index,
note: entry.note,
credit: entry.credit,
debit: entry.debit,
amount,
runningBalance,
formattedAmount: this.formatNumber(amount, { excerptZero: false }),
formattedCredit: this.formatNumber(entry.credit, { excerptZero: false }),
formattedDebit: this.formatNumber(entry.debit, { excerptZero: false }),
formattedRunningBalance: this.formatNumber(runningBalance, {
excerptZero: false,
}),
currencyCode: this.baseCurrency,
} as IGeneralLedgerSheetAccountTransaction;
}
/**
* Mapping the account transactions to general ledger transactions of the given account.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountTransaction[]}
*/
private accountTransactionsMapper(
account: ModelObject<Account>,
openingBalance: number,
): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.repository.transactionsLedger
.whereAccountId(account.id)
.getEntries();
return entries
.reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => {
const prevEntry = last(prev);
const prevRunningBalance = head(prevEntry) as number;
const amount = this.getEntryRunningBalance(
current,
openingBalance,
prevRunningBalance,
);
return [...prev, [amount, current]];
}, [])
.map((entryPair: [number, ILedgerEntry]) => {
const [runningBalance, entry] = entryPair;
return this.transactionMapper(entry, runningBalance);
});
}
/**
* Retrieves the given account opening balance.
* @param {number} accountId
* @returns {number}
*/
private accountOpeningBalance(accountId: number): number {
return this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
}
/**
* Retrieve the given account opening balance.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountOpeningBalanceTotal(
accountId: number,
): IGeneralLedgerSheetAccountBalance {
const amount = this.accountOpeningBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.fromDate;
return { amount, formattedAmount, currencyCode, date };
}
/**
* Retrieves the given account closing balance.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalance(accountId: number): number {
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
const transactionsBalance = this.repository.transactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
return openingBalance + transactionsBalance;
}
/**
* Retrieves the given account closing balance.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceTotal(
accountId: number,
): IGeneralLedgerSheetAccountBalance {
const amount = this.accountClosingBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
}
/**
* Retrieves the given account closing balance with subaccounts.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalanceWithSubaccounts = (
accountId: number,
): number => {
const depsAccountsIds =
this.repository.accountsGraph.dependenciesOf(accountId);
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const transactionsBalanceWithSubAccounts =
this.repository.transactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const closingBalance = openingBalance + transactionsBalanceWithSubAccounts;
return closingBalance;
};
/**
* Retrieves the closing balance with subaccounts total node.
* @param {number} accountId
* @returns {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceWithSubaccountsTotal = (
accountId: number,
): IGeneralLedgerSheetAccountBalance => {
const amount = this.accountClosingBalanceWithSubaccounts(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
};
/**
* Detarmines whether the closing balance subaccounts node should be exist.
* @param {number} accountId
* @returns {boolean}
*/
private isAccountNodeIncludesClosingSubaccounts = (accountId: number) => {
// Retrun early if there is no accounts in the filter so
// return closing subaccounts in all cases.
if (isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id includes transactions.
return this.repository.accountNodesIncludeTransactions.includes(accountId);
};
/**
* Retreive general ledger accounts sections.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccount}
*/
private accountMapper = (
account: ModelObject<Account>,
): IGeneralLedgerSheetAccount => {
const openingBalance = this.accountOpeningBalanceTotal(account.id);
const transactions = this.accountTransactionsMapper(
account,
openingBalance.amount,
);
const closingBalance = this.accountClosingBalanceTotal(account.id);
const closingBalanceSubaccounts =
this.accountClosingBalanceWithSubaccountsTotal(account.id);
const initialNode = {
id: account.id,
name: account.name,
code: account.code,
index: account.index,
parentAccountId: account.parentAccountId,
openingBalance,
transactions,
closingBalance,
};
return R.compose(
R.when(
() => this.isAccountNodeIncludesClosingSubaccounts(account.id),
R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts),
),
)(initialNode);
};
/**
* Maps over deep nodes to retrieve the G/L account node.
* @param {IAccount[]} accounts
* @returns {IGeneralLedgerSheetAccount[]}
*/
private accountNodesDeepMap = (
accounts: ModelObject<Account>[],
): IGeneralLedgerSheetAccount[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Transformes the flatten nodes to nested nodes.
* @param {ModelObject<Account>[]} flattenAccounts -
* @returns {ModelObject<Account>[]}
*/
private nestedAccountsNode = (
flattenAccounts: ModelObject<Account>[],
): ModelObject<Account>[] => {
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
};
/**
* Filters account nodes.
* @param {IGeneralLedgerSheetAccount[]} nodes
* @returns {IGeneralLedgerSheetAccount[]}
*/
private filterAccountNodesByTransactionsFilter = (
nodes: IGeneralLedgerSheetAccount[],
): IGeneralLedgerSheetAccount[] => {
return this.filterNodesDeep(
nodes,
(account: IGeneralLedgerSheetAccount) =>
!(account.transactions.length === 0 && this.query.noneTransactions),
);
};
/**
* Filters account nodes by the acounts filter.
* @param {IAccount[]} nodes
* @returns {IAccount[]}
*/
private filterAccountNodesByAccountsFilter = (
nodes: ModelObject<Account>[],
): ModelObject<Account>[] => {
return this.filterNodesDeep(nodes, (node: IGeneralLedgerSheetAccount) => {
if (R.isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id exists in the filter.
return this.repository.accountNodeInclude?.includes(node.id);
});
};
/**
* Retrieves mapped accounts with general ledger transactions and
* opeing/closing balance.
* @param {ModelObject<Account>[]} accounts -
* @return {IGeneralLedgerSheetAccount[]}
*/
private accountsWalker(
accounts: ModelObject<Account>[],
): IGeneralLedgerSheetAccount[] {
return R.compose(
R.defaultTo([]),
this.filterAccountNodesByTransactionsFilter,
this.accountNodesDeepMap,
R.defaultTo([]),
this.filterAccountNodesByAccountsFilter,
this.nestedAccountsNode,
)(accounts);
}
/**
* Retrieves general ledger report data.
* @return {IGeneralLedgerSheetAccount[]}
*/
public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.repository.accounts);
}
}

View File

@@ -0,0 +1,92 @@
import { IFinancialSheetCommonMeta } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types";
export interface IGeneralLedgerSheetQuery {
fromDate: Date | string;
toDate: Date | string;
basis: string;
numberFormat: {
noCents: boolean;
divideOn1000: boolean;
};
noneTransactions: boolean;
accountsIds: number[];
branchesIds?: number[];
}
export interface IGeneralLedgerSheetAccountTransaction {
id: number;
amount: number;
runningBalance: number;
credit: number;
debit: number;
formattedAmount: string;
formattedCredit: string;
formattedDebit: string;
formattedRunningBalance: string;
currencyCode: string;
note?: string;
transactionTypeFormatted: string;
transactionNumber: string;
referenceId?: number;
referenceType?: string;
date: Date | string;
dateFormatted: string;
}
export interface IGeneralLedgerSheetAccountBalance {
date: Date | string;
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IGeneralLedgerSheetAccount {
id: number;
name: string;
code: string;
index: number;
parentAccountId: number;
transactions: IGeneralLedgerSheetAccountTransaction[];
openingBalance: IGeneralLedgerSheetAccountBalance;
closingBalance: IGeneralLedgerSheetAccountBalance;
closingBalanceSubaccounts?: IGeneralLedgerSheetAccountBalance;
children?: IGeneralLedgerSheetAccount[];
}
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
export interface IAccountTransaction {
id: number;
index: number;
draft: boolean;
note: string;
accountId: number;
transactionType: string;
referenceType: string;
referenceId: number;
contactId: number;
contactType: string;
credit: number;
debit: number;
date: string | Date;
createdAt: string | Date;
updatedAt: string | Date;
}
export interface IGeneralLedgerMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}
export interface IGeneralLedgerTableData extends IFinancialTable {
meta: IGeneralLedgerMeta;
query: IGeneralLedgerSheetQuery;
}

View File

@@ -0,0 +1,70 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from './GeneralLedger.types';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerPdf } from './GeneralLedgerPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerApplication {
constructor(
private readonly GLTable: GeneralLedgerTableInjectable,
private readonly GLExport: GeneralLedgerExportInjectable,
private readonly GLSheet: GeneralLedgerService,
private readonly GLPdf: GeneralLedgerPdf,
) {}
/**
* Retrieves the G/L sheet in json format.
* @param {IGeneralLedgerSheetQuery} query
*/
public sheet(query: IGeneralLedgerSheetQuery) {
return this.GLSheet.generalLedger(query);
}
/**
* Retrieves the G/L sheet in table format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public table(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerTableData> {
return this.GLTable.table(query);
}
/**
* Retrieves the G/L sheet in xlsx format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {}
*/
public xlsx(
query: IGeneralLedgerSheetQuery,
): Promise<Buffer> {
return this.GLExport.xlsx(query);
}
/**
* Retrieves the G/L sheet in csv format.
* @param {IGeneralLedgerSheetQuery} query -
*/
public csv(
query: IGeneralLedgerSheetQuery,
): Promise<string> {
return this.GLExport.csv(query);
}
/**
* Retrieves the G/L sheet in pdf format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(
query: IGeneralLedgerSheetQuery,
): Promise<Buffer> {
return this.GLPdf.pdf(query);
}
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class GeneralLedgerExportInjectable {
constructor(
private readonly generalLedgerTable: GeneralLedgerTableInjectable
) {}
/**
* Retrieves the general ledger sheet in XLSX format.
* @param {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IGeneralLedgerSheetQuery) {
const table = await this.generalLedgerTable.table(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 {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async csv(
query: IGeneralLedgerSheetQuery
): Promise<string> {
const table = await this.generalLedgerTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,34 @@
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class GeneralLedgerMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the general ledger meta.
* @returns {IGeneralLedgerMeta}
*/
public async meta(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
return {
...commonMeta,
sheetName: 'Balance Sheet',
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerPdf {
constructor(
private readonly generalLedgerTable: GeneralLedgerTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the general ledger sheet table to pdf.
* @param {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IGeneralLedgerSheetQuery): Promise<Buffer> {
const table = await this.generalLedgerTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,180 @@
import moment from 'moment';
import * as R from 'ramda';
import {
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { flatten, isEmpty, uniq } from 'lodash';
import { ModelObject } from 'objection';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Contact } from '@/modules/Contacts/models/Contact';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Inject } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { transformToMap } from '@/utils/transform-to-key';
import { Ledger } from '@/modules/Ledger/Ledger';
import { TenantModel } from '@/modules/System/models/TenantModel';
export class GeneralLedgerRepository {
public filter: IGeneralLedgerSheetQuery;
public accounts: Account[];
public transactions: AccountTransaction[];
public openingBalanceTransactions: AccountTransaction[];
public transactionsLedger: Ledger;
public openingBalanceTransactionsLedger: Ledger;
public repositories: any;
public models: any;
public accountsGraph: any;
public contacts: ModelObject<Contact>[];
public contactsById: Map<number, ModelObject<Contact>>;
public tenantId: number;
public tenant: TenantModel;
public accountNodesIncludeTransactions: Array<number> = [];
public accountNodeInclude: Array<number> = [];
@Inject(AccountRepository)
private readonly accountRepository: AccountRepository;
@Inject(TenancyContext)
private readonly tenancyContext: TenancyContext;
/**
* Set the filter.
* @param {IGeneralLedgerSheetQuery} filter - The filter.
*/
setFilter(filter: IGeneralLedgerSheetQuery) {
this.filter = filter;
}
/**
* Initialize the G/L report.
*/
public async asyncInitialize() {
await this.initTenant();
await this.initAccounts();
await this.initAccountsGraph();
await this.initContacts();
await this.initAccountsOpeningBalance();
this.initAccountNodesIncludeTransactions();
await this.initTransactions();
this.initAccountNodesIncluded();
}
/**
* Initialize the tenant.
*/
public async initTenant() {
this.tenant = await this.tenancyContext.getTenant(true);
}
/**
* Initialize the accounts.
*/
public async initAccounts() {
this.accounts = await this.accountRepository
.all()
.orderBy('name', 'ASC');
}
/**
* Initialize the accounts graph.
*/
public async initAccountsGraph() {
this.accountsGraph =
await this.repositories.accountRepository.getDependencyGraph();
}
/**
* Initialize the contacts.
*/
public async initContacts() {
this.contacts = await this.repositories.contactRepository.all();
this.contactsById = transformToMap(this.contacts, 'id');
}
/**
* Initialize the G/L transactions from/to the given date.
*/
public async initTransactions() {
this.transactions = await this.repositories.transactionsRepository
.journal({
fromDate: this.filter.fromDate,
toDate: this.filter.toDate,
branchesIds: this.filter.branchesIds,
})
.orderBy('date', 'ASC')
.onBuild((query) => {
if (this.filter.accountsIds?.length > 0) {
query.whereIn('accountId', this.accountNodesIncludeTransactions);
}
});
// Transform array transactions to journal collection.
this.transactionsLedger = Ledger.fromTransactions(this.transactions);
}
/**
* Initialize the G/L accounts opening balance.
*/
public async initAccountsOpeningBalance() {
// Retreive opening balance credit/debit sumation.
this.openingBalanceTransactions =
await this.repositories.transactionsRepository.journal({
toDate: moment(this.filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: this.filter.branchesIds,
});
// Accounts opening transactions.
this.openingBalanceTransactionsLedger = Ledger.fromTransactions(
this.openingBalanceTransactions
);
}
/**
* Initialize the account nodes that should include transactions.
* @returns {void}
*/
public initAccountNodesIncludeTransactions() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const childrenNodeIds = this.filter.accountsIds?.map(
(accountId: number) => {
return this.accountsGraph.dependenciesOf(accountId);
}
);
const nodeIds = R.concat(this.filter.accountsIds, childrenNodeIds);
this.accountNodesIncludeTransactions = uniq(flatten(nodeIds));
}
/**
* Initialize the account node ids should be included,
* if the filter by acounts is presented.
* @returns {void}
*/
public initAccountNodesIncluded() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const nodeIds = this.filter.accountsIds.map((accountId) => {
const childrenIds = this.accountsGraph.dependenciesOf(accountId);
const parentIds = this.accountsGraph.dependantsOf(accountId);
return R.concat(childrenIds, parentIds);
});
this.accountNodeInclude = R.compose(
R.uniq,
R.flatten,
R.concat(this.filter.accountsIds)
)(nodeIds);
}
}

View File

@@ -0,0 +1,64 @@
import * as moment from 'moment';
import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { GeneralLedgerSheet } from './GeneralLedger';
import { events } from '@/common/events/events';
import { getGeneralLedgerReportQuery } from './_utils';
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { I18nService } from 'nestjs-i18n';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class GeneralLedgerService {
constructor(
private readonly generalLedgerMeta: GeneralLedgerMeta,
private readonly eventEmitter: EventEmitter2,
private readonly generalLedgerRepository: GeneralLedgerRepository,
private readonly i18n: I18nService,
) {}
/**
* Retrieve general ledger report statement.
* @param {IGeneralLedgerSheetQuery} query
* @return {Promise<IGeneralLedgerStatement>}
*/
public async generalLedger(query: IGeneralLedgerSheetQuery): Promise<{
data: any;
query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta;
}> {
const filter = {
...getGeneralLedgerReportQuery(),
...query,
};
this.generalLedgerRepository.setFilter(filter);
await this.generalLedgerRepository.asyncInitialize();
// General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet(
filter,
this.generalLedgerRepository,
this.i18n,
);
// Retrieve general ledger report data.
const reportData = generalLedgerInstance.reportData();
// Retrieve general ledger report metadata.
const meta = await this.generalLedgerMeta.meta(filter);
// Triggers `onGeneralLedgerViewed` event.
await this.eventEmitter.emitAsync(events.reports.onGeneralLedgerViewed, {
});
return {
data: reportData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,324 @@
import * as R from 'ramda';
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountTransaction,
IGeneralLedgerSheetData,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialTable } from '../../common/FinancialTable';
import { ROW_TYPE } from './utils';
import {
IColumnMapperMeta,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class GeneralLedgerTable extends R.compose(
FinancialTable,
FinancialSheetStructure,
)(FinancialSheet) {
private data: IGeneralLedgerSheetData;
private query: IGeneralLedgerSheetQuery;
private meta: IGeneralLedgerMeta;
/**
* Creates an instance of `GeneralLedgerTable`.
* @param {IGeneralLedgerSheetData} data
* @param {IGeneralLedgerSheetQuery} query
*/
constructor(
data: IGeneralLedgerSheetData,
query: IGeneralLedgerSheetQuery,
meta: IGeneralLedgerMeta,
) {
super();
this.data = data;
this.query = query;
this.meta = meta;
}
/**
* Retrieves the common table accessors.
* @returns {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: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the transaction column accessors.
* @returns {ITableColumnAccessor[]}
*/
private transactionColumnAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'account_name', accessor: 'account.name' },
{ key: 'reference_type', accessor: 'transactionTypeFormatted' },
{ key: 'reference_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'amount', accessor: 'formattedAmount' },
{ key: 'running_balance', accessor: 'formattedRunningBalance' },
];
}
/**
* Retrieves the opening row column accessors.
* @returns {ITableRowIColumnMapperMeta[]}
*/
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: 'Opening Balance' },
{ key: 'account_name', value: '' },
{ 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: 'openingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceColumnAccessors(
account: IGeneralLedgerSheetAccount,
): IColumnMapperMeta[] {
return [
{ key: 'date', value: `Closing balance for ${account.name}` },
{ key: 'account_name', value: `` },
{ 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: 'closingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceWithSubaccountsColumnAccessors(
account: IGeneralLedgerSheetAccount,
): IColumnMapperMeta[] {
return [
{
key: 'date',
value: `Closing Balance for ${account.name} with sub-accounts`,
},
{
key: 'account_name',
value: ``,
},
{ 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: 'closingBalanceSubaccounts.formattedAmount' },
{
key: 'running_balance',
accessor: 'closingBalanceSubaccounts.formattedAmount',
},
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'reference_type', label: 'Transaction Type' },
{ key: 'reference_number', label: 'Transaction #' },
{ 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 = R.curry(
(
account: IGeneralLedgerSheetAccount,
transaction: IGeneralLedgerSheetAccountTransaction,
): ITableRow => {
const columns = this.transactionColumnAccessors();
const data = { ...transaction, account };
const meta = {
rowTypes: [ROW_TYPE.TRANSACTION],
};
return tableRowMapper(data, columns, meta);
},
);
/**
* Maps the given transactions nodes to table rows.
* @param {IGeneralLedgerSheetAccountTransaction[]} transactions
* @returns {ITableRow[]}
*/
private transactionsMapper = (
account: IGeneralLedgerSheetAccount,
): ITableRow[] => {
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();
const meta = {
rowTypes: [ROW_TYPE.OPENING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to closing balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors(account);
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceWithSubaccountsMapper = (
account: IGeneralLedgerSheetAccount,
): ITableRow => {
const columns = this.closingBalanceWithSubaccountsColumnAccessors(account);
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* 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 R.when(
R.always(R.not(R.isEmpty(transactions))),
R.prepend(openingBalance),
)([...transactions, closingBalance]) as ITableRow[];
};
/**
* Maps the given account node to the table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => {
const columns = this.accountColumnsAccessors();
const transactions = this.transactionsNode(account);
const meta = {
rowTypes: [ROW_TYPE.ACCOUNT],
};
const row = tableRowMapper(account, columns, meta);
const closingBalanceWithSubaccounts =
this.closingBalanceWithSubaccountsMapper(account);
// Appends the closing balance with sub-accounts row if the account
// has children accounts and the node is define.
const isAppendClosingSubaccounts = () =>
account.children?.length > 0 && !!account.closingBalanceSubaccounts;
const children = R.compose(
R.when(
isAppendClosingSubaccounts,
R.append(closingBalanceWithSubaccounts),
),
R.concat(R.defaultTo([], transactions)),
R.when(
() => account?.children?.length > 0,
R.concat(R.defaultTo([], account.children)),
),
)([]);
return R.assoc('children', children)(row);
};
/**
* Maps the given account node to table rows.
* @param {IGeneralLedgerSheetAccount[]} accounts
* @returns {ITableRow[]}
*/
private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[],
): ITableRow[] => {
return this.mapNodesDeepReverse(accounts, this.accountMapper);
};
/**
* 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);
}
}

View File

@@ -0,0 +1,38 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from './GeneralLedger.types';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerTable } from './GeneralLedgerTable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerTableInjectable {
constructor(private readonly GLSheet: GeneralLedgerService) {}
/**
* Retrieves the G/L table.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public async table(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerTableData> {
const {
data: sheetData,
query: sheetQuery,
meta: sheetMeta,
} = await this.GLSheet.generalLedger(query);
const table = new GeneralLedgerTable(sheetData, sheetQuery, sheetMeta);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query: sheetQuery,
meta: sheetMeta,
};
}
}

View File

@@ -0,0 +1,28 @@
/**
* Calculate the running balance.
* @param {number} amount - Transaction amount.
* @param {number} lastRunningBalance - Last running balance.
* @param {number} openingBalance - Opening balance.
* @return {number} Running balance.
*/
export function calculateRunningBalance(
amount: number,
lastRunningBalance: number,
): number {
return amount + lastRunningBalance;
}
export const getGeneralLedgerReportQuery = (
) => {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
basis: 'cash',
numberFormat: {
noCents: false,
divideOn1000: false,
},
noneZero: false,
accountsIds: [],
};
};

View File

@@ -0,0 +1,29 @@
export const HtmlTableCustomCss = `
table tr:last-child td {
border-bottom: 1px solid #ececec;
}
table tr.row-type--account td,
table tr.row-type--opening-balance td,
table tr.row-type--closing-balance td{
font-weight: 600;
}
table tr.row-type--closing-balance td {
border-bottom: 1px solid #ececec;
}
table .column--debit,
table .column--credit,
table .column--amount,
table .column--running_balance,
table .cell--debit,
table .cell--credit,
table .cell--amount,
table .cell--running_balance{
text-align: right;
}
table tr.row-type--account .cell--date span,
table tr.row-type--opening-balance .cell--account_name span,
table tr.row-type--closing-balance .cell--account_name span{
white-space: nowrap;
}
`;

View File

@@ -0,0 +1,6 @@
export enum ROW_TYPE {
ACCOUNT = 'ACCOUNT',
OPENING_BALANCE = 'OPENING_BALANCE',
TRANSACTION = 'TRANSACTION',
CLOSING_BALANCE = 'CLOSING_BALANCE',
}

View File

@@ -3,14 +3,19 @@ import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication'; import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types'; import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/purchases-by-items') @Controller('/reports/purchases-by-items')
@PublicRoute()
@ApiTags('reports')
export class PurchasesByItemReportController { export class PurchasesByItemReportController {
constructor( constructor(
private readonly purchasesByItemsApp: PurchasesByItemsApplication, private readonly purchasesByItemsApp: PurchasesByItemsApplication,
) {} ) {}
@Get() @Get()
@ApiResponse({ status: 200, description: 'Purchases by items report' })
async purchasesByItems( async purchasesByItems(
@Query() filter: IPurchasesByItemsReportQuery, @Query() filter: IPurchasesByItemsReportQuery,
@Res() res: Response, @Res() res: Response,

View File

@@ -1,18 +1,25 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService'; import { PurchasesByItemsService } from './PurchasesByItems.service';
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf'; import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
import { PurchasesByItemsExport } from './PurchasesByItemsExport'; import { PurchasesByItemsExport } from './PurchasesByItemsExport';
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication'; import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
import { PurchasesByItemReportController } from './PurchasesByItems.controller'; import { PurchasesByItemReportController } from './PurchasesByItems.controller';
import { PurchasesByItemsMeta } from './PurchasesByItemsMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryCostModule } from '@/modules/InventoryCost/InventoryCost.module';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
@Module({ @Module({
imports: [InventoryCostModule, FinancialSheetCommonModule],
providers: [ providers: [
PurchasesByItemsTableInjectable, PurchasesByItemsTableInjectable,
PurchasesByItemsService, PurchasesByItemsService,
PurchasesByItemsExport, PurchasesByItemsExport,
PurchasesByItemsPdf, PurchasesByItemsPdf,
PurchasesByItemsMeta,
PurchasesByItemsApplication, PurchasesByItemsApplication,
TenancyContext,
], ],
exports: [PurchasesByItemsApplication], exports: [PurchasesByItemsApplication],
controllers: [PurchasesByItemReportController], controllers: [PurchasesByItemReportController],

View File

@@ -1,4 +1,3 @@
import moment from 'moment';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { PurchasesByItems } from './PurchasesByItems'; import { PurchasesByItems } from './PurchasesByItems';
import { import {
@@ -11,9 +10,17 @@ import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTr
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { getPurchasesByItemsDefaultQuery } from './utils';
@Injectable() @Injectable()
export class PurchasesByItemsService { export class PurchasesByItemsService {
/**
* @param {PurchasesByItemsMeta} purchasesByItemsMeta - The purchases by items meta.
* @param {EventEmitter2} eventPublisher - The event emitter.
* @param {TenancyContext} tenancyContext - The tenancy context.
* @param {typeof InventoryTransaction} inventoryTransactionModel - The inventory transaction model.
* @param {typeof Item} itemModel - The item model.
*/
constructor( constructor(
private readonly purchasesByItemsMeta: PurchasesByItemsMeta, private readonly purchasesByItemsMeta: PurchasesByItemsMeta,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
@@ -27,39 +34,16 @@ export class PurchasesByItemsService {
) {} ) {}
/** /**
* Defaults purchases by items filter query. * Retrieve purchases by items statement.
* @return {IPurchasesByItemsReportQuery} * @param {IPurchasesByItemsReportQuery} query - Purchases by items report query.
*/ * @return {Promise<IPurchasesByItemsSheet>} - Purchases by items sheet.
private get defaultQuery(): IPurchasesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'always',
negativeFormat: 'mines',
},
noneTransactions: true,
onlyActive: false,
};
}
/**
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @return {Promise<IPurchasesByItemsSheet>}
*/ */
public async purchasesByItems( public async purchasesByItems(
query: IPurchasesByItemsReportQuery, query: IPurchasesByItemsReportQuery,
): Promise<IPurchasesByItemsSheet> { ): Promise<IPurchasesByItemsSheet> {
const tenant = await this.tenancyContext.getTenant(); const tenantMetadata = await this.tenancyContext.getTenantMetadata();
const filter = { const filter = {
...this.defaultQuery, ...getPurchasesByItemsDefaultQuery(),
...query, ...query,
}; };
const inventoryItems = await this.itemModel.query().onBuild((q) => { const inventoryItems = await this.itemModel.query().onBuild((q) => {
@@ -88,7 +72,7 @@ export class PurchasesByItemsService {
filter, filter,
inventoryItems, inventoryItems,
inventoryTransactions, inventoryTransactions,
tenant.metadata.baseCurrency, tenantMetadata.baseCurrency,
); );
const purchasesByItemsData = purchasesByItemsInstance.reportData(); const purchasesByItemsData = purchasesByItemsInstance.reportData();

View File

@@ -7,7 +7,7 @@ import {
IPurchasesByItemsSheetData, IPurchasesByItemsSheetData,
IPurchasesByItemsTotal, IPurchasesByItemsTotal,
} from './types/PurchasesByItems.types'; } from './types/PurchasesByItems.types';
import FinancialSheet from '../../common/FinancialSheet'; import { FinancialSheet } from '../../common/FinancialSheet';
import { transformToMapBy } from '@/utils/transform-to-map-by'; import { transformToMapBy } from '@/utils/transform-to-map-by';
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction'; import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
@@ -48,7 +48,7 @@ export class PurchasesByItems extends FinancialSheet{
cost: number; cost: number;
average: number; average: number;
} { } {
const transaction = this.itemsTransactions.get(itemId); const transaction = this.itemsTransactions.get(itemId.toString());
const quantity = get(transaction, 'quantity', 0); const quantity = get(transaction, 'quantity', 0);
const cost = get(transaction, 'cost', 0); const cost = get(transaction, 'cost', 0);
@@ -105,14 +105,17 @@ export class PurchasesByItems extends FinancialSheet{
id: item.id, id: item.id,
name: item.name, name: item.name,
code: item.code, code: item.code,
quantityPurchased: meta.quantity, quantityPurchased: meta.quantity,
purchaseCost: meta.cost, purchaseCost: meta.cost,
averageCostPrice: meta.average, averageCostPrice: meta.average,
quantityPurchasedFormatted: this.formatNumber(meta.quantity, { quantityPurchasedFormatted: this.formatNumber(meta.quantity, {
money: false, money: false,
}), }),
purchaseCostFormatted: this.formatNumber(meta.cost), purchaseCostFormatted: this.formatNumber(meta.cost),
averageCostPriceFormatted: this.formatNumber(meta.average), averageCostPriceFormatted: this.formatNumber(meta.average),
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
}; };
}; };

View File

@@ -6,11 +6,17 @@ import {
IPurchasesByItemsTable, IPurchasesByItemsTable,
} from './types/PurchasesByItems.types'; } from './types/PurchasesByItems.types';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService'; import { PurchasesByItemsService } from './PurchasesByItems.service';
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf'; import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
@Injectable() @Injectable()
export class PurchasesByItemsApplication { export class PurchasesByItemsApplication {
/**
* @param {PurchasesByItemsService} purchasesByItemsSheetService - Purchases by items sheet service.
* @param {PurchasesByItemsTableInjectable} purchasesByItemsTableService - Purchases by items table service.
* @param {PurchasesByItemsExport} purchasesByItemsExportService - Purchases by items export service.
* @param {PurchasesByItemsPdf} purchasesByItemsPdfService - Purchases by items pdf service.
*/
constructor( constructor(
private readonly purchasesByItemsSheetService: PurchasesByItemsService, private readonly purchasesByItemsSheetService: PurchasesByItemsService,
private readonly purchasesByItemsTableService: PurchasesByItemsTableInjectable, private readonly purchasesByItemsTableService: PurchasesByItemsTableInjectable,
@@ -21,7 +27,7 @@ export class PurchasesByItemsApplication {
/** /**
* Retrieves the purchases by items in json format. * Retrieves the purchases by items in json format.
* @param {IPurchasesByItemsReportQuery} query * @param {IPurchasesByItemsReportQuery} query
* @returns * @returns {Promise<IPurchasesByItemsSheet>}
*/ */
public sheet( public sheet(
query: IPurchasesByItemsReportQuery, query: IPurchasesByItemsReportQuery,

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TableSheetPdf } from '../../TableSheetPdf'; import { TableSheetPdf } from '../../common/TableSheetPdf';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types'; import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
import { HtmlTableCustomCss } from './_types'; import { HtmlTableCustomCss } from './_types';

View File

@@ -8,7 +8,7 @@ import {
import { ITableColumn, ITableColumnAccessor, ITableRow } from '../../types/Table.types'; import { ITableColumn, ITableColumnAccessor, ITableRow } from '../../types/Table.types';
import { FinancialTable } from '../../common/FinancialTable'; import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import FinancialSheet from '../../common/FinancialSheet'; import { FinancialSheet } from '../../common/FinancialSheet';
import { tableRowMapper } from '../../utils/Table.utils'; import { tableRowMapper } from '../../utils/Table.utils';
export class PurchasesByItemsTable extends R.compose( export class PurchasesByItemsTable extends R.compose(

View File

@@ -2,7 +2,7 @@ import {
IPurchasesByItemsReportQuery, IPurchasesByItemsReportQuery,
IPurchasesByItemsTable, IPurchasesByItemsTable,
} from './types/PurchasesByItems.types'; } from './types/PurchasesByItems.types';
import { PurchasesByItemsService } from './PurchasesByItemsService'; import { PurchasesByItemsService } from './PurchasesByItems.service';
import { PurchasesByItemsTable } from './PurchasesByItemsTable'; import { PurchasesByItemsTable } from './PurchasesByItemsTable';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@@ -14,9 +14,8 @@ export class PurchasesByItemsTableInjectable {
/** /**
* Retrieves the purchases by items table format. * Retrieves the purchases by items table format.
* @param {number} tenantId * @param {IPurchasesByItemsReportQuery} filter - The filter to be used.
* @param {IPurchasesByItemsReportQuery} filter * @returns {Promise<IPurchasesByItemsTable>} - The purchases by items table.
* @returns {Promise<IPurchasesByItemsTable>}
*/ */
public async table( public async table(
filter: IPurchasesByItemsReportQuery, filter: IPurchasesByItemsReportQuery,

View File

@@ -23,15 +23,16 @@ export interface IPurchasesByItemsItem {
id: number; id: number;
name: string; name: string;
code: string; code: string;
soldCost: number;
averageSellPrice: number; purchaseCost: number;
averageSellPriceFormatted: string; purchaseCostFormatted: string;
averageCostPrice: number;
averageCostPriceFormatted: string;
quantityPurchased: number; quantityPurchased: number;
quantityPurchasedFormatted: string; quantityPurchasedFormatted: string;
soldCostFormatted: string;
currencyCode: string; currencyCode: string;
} }

View File

@@ -0,0 +1,15 @@
import * as moment from 'moment';
export const getPurchasesByItemsDefaultQuery = () => ({
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'always',
negativeFormat: 'mines',
},
noneTransactions: true,
onlyActive: false,
});

View File

@@ -0,0 +1,57 @@
import { Body, Controller, Get, Headers, Query, Req, Res } from '@nestjs/common';
import { ISalesByItemsReportQuery } from './SalesByItems.types';
import { AcceptType } from '@/constants/accept-type';
import { SalesByItemsApplication } from './SalesByItemsApplication';
import { Response } from 'express';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/sales-by-items')
@ApiTags('reports')
export class SalesByItemsController {
constructor(private readonly salesByItemsApp: SalesByItemsApplication) {}
@Get()
@ApiResponse({ status: 200, description: 'Sales by items report' })
public async salesByitems(
@Query() filter: ISalesByItemsReportQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the csv format.
if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.salesByItemsApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.salesByItemsApp.table(filter);
return res.status(200).send(table);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = this.salesByItemsApp.xlsx(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 if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.salesByItemsApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
} else {
const sheet = await this.salesByItemsApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { SalesByItemsApplication } from './SalesByItemsApplication';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
import { SalesByItemsPdfInjectable } from './SalesByItemsPdfInjectable';
import { SalesByItemsReportService } from './SalesByItemsService';
import { SalesByItemsExport } from './SalesByItemsExport';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { SalesByItemsMeta } from './SalesByItemsMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryCostModule } from '@/modules/InventoryCost/InventoryCost.module';
import { SalesByItemsController } from './SalesByItems.controller';
@Module({
providers: [
SalesByItemsApplication,
SalesByItemsTableInjectable,
SalesByItemsPdfInjectable,
SalesByItemsReportService,
SalesByItemsExport,
SalesByItemsMeta,
TenancyContext
],
controllers: [SalesByItemsController],
imports: [
FinancialSheetCommonModule,
InventoryCostModule
],
})
export class SalesByItemsModule {}

View File

@@ -0,0 +1,176 @@
import { get, sumBy } from 'lodash';
import * as R from 'ramda';
import {
ISalesByItemsReportQuery,
ISalesByItemsItem,
ISalesByItemsTotal,
ISalesByItemsSheetData,
} from './SalesByItems.types';
import { ModelObject } from 'objection';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Item } from '@/modules/Items/models/Item';
import { transformToMap } from '@/utils/transform-to-key';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
export class SalesByItemsReport extends FinancialSheet {
readonly baseCurrency: string;
readonly items: Item[];
readonly itemsTransactions: Map<number, ModelObject<InventoryTransaction>[]>;
readonly query: ISalesByItemsReportQuery;
/**
* Constructor method.
* @param {ISalesByItemsReportQuery} query
* @param {IItem[]} items
* @param {IAccountTransaction[]} itemsTransactions
* @param {string} baseCurrency
*/
constructor(
query: ISalesByItemsReportQuery,
items: Item[],
itemsTransactions: ModelObject<InventoryTransaction>[],
baseCurrency: string,
) {
super();
this.baseCurrency = baseCurrency;
this.items = items;
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
this.query = query;
this.numberFormat = this.query.numberFormat;
}
/**
* Retrieve the item purchase item, cost and average cost price.
* @param {number} itemId - Item id.
*/
getItemTransaction(itemId: number): {
quantity: number;
cost: number;
average: number;
} {
const transaction = this.itemsTransactions.get(itemId);
const quantity = get(transaction, 'quantity', 0);
const cost = get(transaction, 'cost', 0);
const average = cost / quantity;
return { quantity, cost, average };
}
/**
* Mapping the given item section.
* @param {ISalesByItemsItem} item
* @returns
*/
private itemSectionMapper = (item: Item): ISalesByItemsItem => {
const meta = this.getItemTransaction(item.id);
return {
id: item.id,
name: item.name,
code: item.code,
quantitySold: meta.quantity,
soldCost: meta.cost,
averageSellPrice: meta.average,
quantitySoldFormatted: this.formatNumber(meta.quantity, {
money: false,
}),
soldCostFormatted: this.formatNumber(meta.cost),
averageSellPriceFormatted: this.formatNumber(meta.average),
currencyCode: this.baseCurrency,
};
};
/**
* Detarmines whether the given sale node is has transactions.
* @param {ISalesByItemsItem} node -
* @returns {boolean}
*/
private filterSaleNoneTransactions = (node: ISalesByItemsItem) => {
return this.itemsTransactions.get(node.id);
};
/**
* Detarmines whether the given sale by item node is active.
* @param {ISalesByItemsItem} node
* @returns {boolean}
*/
private filterSaleOnlyActive = (node: ISalesByItemsItem): boolean => {
return node.quantitySold !== 0 || node.soldCost !== 0;
};
/**
* Filters sales by items nodes based on the report query.
* @param {ISalesByItemsItem} saleItem -
* @return {boolean}
*/
private itemSaleFilter = (saleItem: ISalesByItemsItem): boolean => {
const { noneTransactions, onlyActive } = this.query;
const conditions = [
[noneTransactions, this.filterSaleNoneTransactions],
[onlyActive, this.filterSaleOnlyActive],
];
return allPassedConditionsPass(conditions)(saleItem);
};
/**
* Mappes the given items to sales by items nodes.
* @param {IItem[]} items -
* @returns {ISalesByItemsItem[]}
*/
private itemsMapper = (items: Item[]): ISalesByItemsItem[] => {
return items.map(this.itemSectionMapper);
};
/**
* Filters sales by items sections.
* @param items
* @returns
*/
private itemsFilters = (nodes: ISalesByItemsItem[]): ISalesByItemsItem[] => {
return nodes.filter(this.itemSaleFilter);
};
/**
* Retrieve the items sections.
* @returns {ISalesByItemsItem[]}
*/
private itemsSection(): ISalesByItemsItem[] {
return R.compose(this.itemsFilters, this.itemsMapper)(this.items);
}
/**
* Retrieve the total section of the sheet.
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
*/
private totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
const quantitySold = sumBy(items, (item) => item.quantitySold);
const soldCost = sumBy(items, (item) => item.soldCost);
return {
quantitySold,
soldCost,
quantitySoldFormatted: this.formatTotalNumber(quantitySold, {
money: false,
}),
soldCostFormatted: this.formatTotalNumber(soldCost),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the sheet data.
* @returns {ISalesByItemsSheetData}
*/
public reportData(): ISalesByItemsSheetData {
const items = this.itemsSection();
const total = this.totalSection(items);
return { items, total };
}
}

View File

@@ -0,0 +1,56 @@
import { IFinancialSheetCommonMeta } from "../../types/Report.types";
import { INumberFormatQuery } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types";
export interface ISalesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[];
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
onlyActive: boolean;
}
export interface ISalesByItemsSheetMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}
export interface ISalesByItemsItem {
id: number;
name: string;
code: string;
quantitySold: number;
soldCost: number;
averageSellPrice: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
averageSellPriceFormatted: string;
currencyCode: string;
}
export interface ISalesByItemsTotal {
quantitySold: number;
soldCost: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
currencyCode: string;
}
export type ISalesByItemsSheetData = {
items: ISalesByItemsItem[];
total: ISalesByItemsTotal;
};
export interface ISalesByItemsSheet {
data: ISalesByItemsSheetData;
query: ISalesByItemsReportQuery;
meta: ISalesByItemsSheetMeta;
}
export interface ISalesByItemsTable extends IFinancialTable {
query: ISalesByItemsReportQuery;
meta: ISalesByItemsSheetMeta;
}

View File

@@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import {
ISalesByItemsReportQuery,
ISalesByItemsSheet,
ISalesByItemsTable,
} from './SalesByItems.types';
import { SalesByItemsReportService } from './SalesByItemsService';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
import { SalesByItemsExport } from './SalesByItemsExport';
import { SalesByItemsPdfInjectable } from './SalesByItemsPdfInjectable';
@Injectable()
export class SalesByItemsApplication {
constructor(
private readonly salesByItemsSheet: SalesByItemsReportService,
private readonly salesByItemsTable: SalesByItemsTableInjectable,
private readonly salesByItemsExport: SalesByItemsExport,
private readonly salesByItemsPdf: SalesByItemsPdfInjectable,
) {}
/**
* Retrieves the sales by items report in json format.
* @param {ISalesByItemsReportQuery} filter - Sales by items report query.
* @returns {Promise<ISalesByItemsSheetData>}
*/
public sheet(
filter: ISalesByItemsReportQuery,
): Promise<ISalesByItemsSheet> {
return this.salesByItemsSheet.salesByItems(filter);
}
/**
* Retrieves the sales by items report in table format.
* @param {ISalesByItemsReportQuery} filter - Sales by items report query.
* @returns {Promise<ISalesByItemsTable>}
*/
public table(
filter: ISalesByItemsReportQuery,
): Promise<ISalesByItemsTable> {
return this.salesByItemsTable.table(filter);
}
/**
* Retrieves the sales by items report in csv format.
* @param {ISalesByItemsReportQuery} filter - Sales by items report query.
* @returns {Promise<string>}
*/
public csv(
filter: ISalesByItemsReportQuery,
): Promise<string> {
return this.salesByItemsExport.csv(filter);
}
/**
* Retrieves the sales by items report in xlsx format.
* @param {ISalesByItemsReportQuery} filter - Sales by items report query.
* @returns {Promise<Buffer>}
*/
public xlsx(
filter: ISalesByItemsReportQuery,
): Promise<Buffer> {
return this.salesByItemsExport.xlsx(filter);
}
/**
* Retrieves the sales by items in pdf format.
* @param {ISalesByItemsReportQuery} filter - Sales by items report query.
* @returns {Promise<Buffer>}
*/
public pdf(
query: ISalesByItemsReportQuery,
): Promise<Buffer> {
return this.salesByItemsPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { TableSheet } from '../../common/TableSheet';
import { ISalesByItemsReportQuery } from './SalesByItems.types';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
@Injectable()
export class SalesByItemsExport {
constructor(
private readonly salesByItemsTable: SalesByItemsTableInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {ISalesByItemsReportQuery} query - Sales by items report query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ISalesByItemsReportQuery) {
const table = await this.salesByItemsTable.table(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 {ISalesByItemsReportQuery} query - Sales by items report query.
* @returns {Promise<Buffer>}
*/
public async csv(query: ISalesByItemsReportQuery): Promise<string> {
const table = await this.salesByItemsTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,36 @@
import moment from 'moment';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import { ISalesByItemsReportQuery, ISalesByItemsSheetMeta } from './SalesByItems.types';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class SalesByItemsMeta {
constructor(
private financialSheetMeta: FinancialSheetMeta,
private i18n: I18nService,
) {}
/**
* Retrieve the sales by items meta.
* @returns {IBalanceSheetMeta}
*/
public async meta(
query: ISalesByItemsReportQuery
): Promise<ISalesByItemsSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
const sheetName = 'Sales By Items';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,31 @@
import { ISalesByItemsReportQuery } from './SalesByItems.types';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SalesByItemsPdfInjectable {
constructor(
private readonly salesByItemsTable: SalesByItemsTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Retrieves the sales by items sheet in pdf format.
* @param {ISalesByItemsReportQuery} query - The query to apply to the report.
* @returns {Promise<Buffer>}
*/
public async pdf(
query: ISalesByItemsReportQuery,
): Promise<Buffer> {
const table = await this.salesByItemsTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,88 @@
import { SalesByItemsMeta } from './SalesByItemsMeta';
import { getSalesByItemsDefaultQuery } from './utils';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
ISalesByItemsReportQuery,
ISalesByItemsSheet,
} from './SalesByItems.types';
import { Item } from '@/modules/Items/models/Item';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
import { events } from '@/common/events/events';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { SalesByItemsReport } from './SalesByItems';
@Injectable()
export class SalesByItemsReportService {
constructor(
private readonly salesByItemsMeta: SalesByItemsMeta,
private readonly eventPublisher: EventEmitter2,
private readonly tenancyContext: TenancyContext,
@Inject(Item.name)
private readonly itemModel: typeof Item,
@Inject(InventoryTransaction.name)
private readonly inventoryTransactionModel: typeof InventoryTransaction,
) {}
/**
* Retrieve balance sheet statement.
* @param {ISalesByItemsReportQuery} query - The sales by items report query.
* @return {Promise<ISalesByItemsSheet>}
*/
public async salesByItems(
query: ISalesByItemsReportQuery,
): Promise<ISalesByItemsSheet> {
const filter = {
...getSalesByItemsDefaultQuery(),
...query,
};
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
// Inventory items for sales report.
const inventoryItems = await this.itemModel.query().onBuild((q) => {
q.where('type', 'inventory');
if (filter.itemsIds.length > 0) {
q.whereIn('id', filter.itemsIds);
}
});
const inventoryItemsIds = inventoryItems.map((item) => item.id);
// Calculates the total inventory total quantity and rate `IN` transactions.
const inventoryTransactions = await this.inventoryTransactionModel.query().onBuild(
(builder: any) => {
builder.modify('itemsTotals');
builder.modify('OUTDirection');
// Filter the inventory items only.
builder.whereIn('itemId', inventoryItemsIds);
// Filter the date range of the sheet.
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
},
);
const sheet = new SalesByItemsReport(
filter,
inventoryItems,
inventoryTransactions,
tenantMetadata.baseCurrency,
);
const salesByItemsData = sheet.reportData();
// Retrieve the sales by items meta.
const meta = await this.salesByItemsMeta.meta(query);
// Triggers `onSalesByItemViewed` event.
await this.eventPublisher.emitAsync(events.reports.onSalesByItemViewed, {
query,
});
return {
data: salesByItemsData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,102 @@
import * as R from 'ramda';
import {
ISalesByItemsItem,
ISalesByItemsTotal,
} from './SalesByItems.types';
import { ROW_TYPE } from './constants';
import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ITableColumn, ITableRow } from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class SalesByItemsTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private readonly data: ISalesByItemsSheetStatement;
/**
* Constructor method.
* @param {ISalesByItemsSheetStatement} data
*/
constructor(data: ISalesByItemsSheetStatement) {
super();
this.data = data;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumn[]}
*/
private commonTableAccessors() {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'sold_quantity', accessor: 'quantitySoldFormatted' },
{ key: 'sold_amount', accessor: 'soldCostFormatted' },
{ key: 'average_price', accessor: 'averageSellPriceFormatted' },
];
}
/**
* Maps the given item node to table row.
* @param {ISalesByItemsItem} item
* @returns {ITableRow}
*/
private itemMap = (item: ISalesByItemsItem): 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 {ISalesByItemsItem[]} items
* @returns {ITableRow[]}
*/
private itemsMap = (items: ISalesByItemsItem[]): ITableRow[] => {
return R.map(this.itemMap, items);
};
/**
* Maps the given total node to table row.
* @param {ISalesByItemsTotal} total
* @returns {ITableRow[]}
*/
private totalMap = (total: ISalesByItemsTotal) => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, columns, meta);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
const itemsRows = this.itemsMap(this.data.items);
const totalRow = this.totalMap(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)([...itemsRows]) as ITableRow[];
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = [
{ key: 'item_name', label: 'Item name' },
{ key: 'sold_quantity', label: 'Sold quantity' },
{ key: 'sold_amount', label: 'Sold amount' },
{ key: 'average_price', label: 'Average price' },
];
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,29 @@
import { ISalesByItemsReportQuery } from './SalesByItems.types';
import { SalesByItemsReportService } from './SalesByItemsService';
import { SalesByItemsTable } from './SalesByItemsTable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SalesByItemsTableInjectable {
constructor(private readonly salesByItemSheet: SalesByItemsReportService) {}
/**
* Retrieves the sales by items report in table format.
* @param {ISalesByItemsReportQuery} filter - The filter to apply to the report.
* @returns {Promise<ISalesByItemsTable>}
*/
public async table(filter: ISalesByItemsReportQuery) {
const { data, query, meta } =
await this.salesByItemSheet.salesByItems(filter);
const table = new SalesByItemsTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
meta,
query,
};
}
}

View File

@@ -0,0 +1,23 @@
export enum ROW_TYPE {
ITEM = 'ITEM',
TOTAL = 'TOTAL',
}
export const HtmlTableCustomCss = `
table tr.row-type--total td {
border-top: 1px solid #bbb;
border-bottom: 3px double #000;
font-weight: 600;
}
table .column--item_name{
width: 300px;
}
table .column--average_price,
table .column--sold_quantity,
table .column--sold_amount,
table .cell--average_price,
table .cell--sold_quantity,
table .cell--sold_amount{
text-align: right;
}
`;

View File

@@ -0,0 +1,16 @@
export const getSalesByItemsDefaultQuery = () => {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'always',
negativeFormat: 'mines',
},
noneTransactions: true,
onlyActive: false,
};
};

View File

@@ -70,3 +70,44 @@ export interface IDateRange {
fromDate: Date; fromDate: Date;
toDate: Date; toDate: Date;
} }
interface FinancialDateMeta {
date: Date;
formattedDate: string;
}
interface IFinancialSheetTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}
interface IFinancialSheetPercentage {
amount: number;
formattedAmount: string;
}
export interface IFinancialNodeWithPreviousPeriod {
previousPeriodFromDate?: FinancialDateMeta;
previousPeriodToDate?: FinancialDateMeta;
previousPeriod?: IFinancialSheetTotal;
previousPeriodChange?: IFinancialSheetTotal;
previousPeriodPercentage?: IFinancialSheetPercentage;
}
export interface IFinancialNodeWithPreviousYear {
previousYearFromDate: FinancialDateMeta;
previousYearToDate: FinancialDateMeta;
previousYear?: IFinancialSheetTotal;
previousYearChange?: IFinancialSheetTotal;
previousYearPercentage?: IFinancialSheetPercentage;
}
export interface IFinancialCommonNode {
total: IFinancialSheetTotal;
}
export interface IFinancialCommonHorizDatePeriodNode {
fromDate: FinancialDateMeta;
toDate: FinancialDateMeta;
total: IFinancialSheetTotal;
}

View File

@@ -11,6 +11,8 @@ export interface ITableCell {
export type ITableRow = { export type ITableRow = {
cells: ITableCell[]; cells: ITableCell[];
rowTypes?: Array<any>
id?: string;
}; };
export interface ITableColumn { export interface ITableColumn {
@@ -38,3 +40,9 @@ export interface ITableData {
export interface IFinancialTable { export interface IFinancialTable {
table: ITableData; table: ITableData;
} }
export interface IFinancialTableTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}

View File

@@ -1,19 +1,19 @@
import { kebabCase } from 'lodash'; import { kebabCase } from 'lodash';
import { ITableRow } from '@/interfaces'; import { ITableRow } from './types/Table.types';
export const formatNumber = (balance, { noCents, divideOn1000 }): string => { export const formatNumber = (balance, { noCents, divideOn1000 }): string => {
let formattedBalance: number = parseFloat(balance); let formattedBalance: number = parseFloat(balance);
if (noCents) { if (noCents) {
formattedBalance = parseInt(formattedBalance, 10); formattedBalance = parseInt(formattedBalance.toString(), 10);
} }
if (divideOn1000) { if (divideOn1000) {
formattedBalance /= 1000; formattedBalance /= 1000;
} }
return formattedBalance; return formattedBalance.toString();
}; };
export const tableClassNames = (rows: ITableRow[]) => { export const tableClassNames = (rows: ITableRow[]): ITableRow[] => {
return rows.map((row) => { return rows.map((row) => {
const classNames = const classNames =
row?.rowTypes?.map((rowType) => `row-type--${kebabCase(rowType)}`) || []; row?.rowTypes?.map((rowType) => `row-type--${kebabCase(rowType)}`) || [];

View File

@@ -110,8 +110,8 @@ export class ImportFileDataTransformer {
valueDTOs: Record<string, any>[], valueDTOs: Record<string, any>[],
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<Record<string, any>[]> { ): Promise<Record<string, any>[]> {
const tenantModels = this.tenancy.models(tenantId); // const tenantModels = this.tenancy.models(tenantId);
const _valueParser = valueParser(fields, tenantModels, trx); const _valueParser = valueParser(fields, {}, trx);
const _keyParser = parseKey(fields); const _keyParser = parseKey(fields);
const parseAsync = async (valueDTO) => { const parseAsync = async (valueDTO) => {

View File

@@ -1,24 +1,24 @@
import Container, { Service } from 'typedi'; // import { AccountsImportable } from '../Accounts/AccountsImportable';
import { AccountsImportable } from '../Accounts/AccountsImportable'; import { Injectable } from '@nestjs/common';
import { ImportableRegistry } from './ImportableRegistry'; import { ImportableRegistry } from './ImportableRegistry';
import { UncategorizedTransactionsImportable } from '../BankingCategorize/commands/UncategorizedTransactionsImportable'; // import { UncategorizedTransactionsImportable } from '../BankingCategorize/commands/UncategorizedTransactionsImportable';
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable'; // import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable'; // import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable'; // import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable'; // import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport'; // import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport';
import { BillsImportable } from '../Purchases/Bills/BillsImportable'; // import { BillsImportable } from '../Purchases/Bills/BillsImportable';
import { ExpensesImportable } from '../Expenses/ExpensesImportable'; // import { ExpensesImportable } from '../Expenses/ExpensesImportable';
import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable'; // import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable'; // import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable'; // import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable'; // import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
import { PaymentsReceivedImportable } from '../Sales/PaymentReceived/PaymentsReceivedImportable'; // import { PaymentsReceivedImportable } from '../Sales/PaymentReceived/PaymentsReceivedImportable';
import { CreditNotesImportable } from '../CreditNotes/commands/CreditNotesImportable'; // import { CreditNotesImportable } from '../CreditNotes/commands/CreditNotesImportable';
import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable'; // import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable'; // import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable';
@Service() @Injectable()
export class ImportableResources { export class ImportableResources {
private static registry: ImportableRegistry; private static registry: ImportableRegistry;
@@ -30,26 +30,26 @@ export class ImportableResources {
* Importable instances. * Importable instances.
*/ */
private importables = [ private importables = [
{ resource: 'Account', importable: AccountsImportable }, // { resource: 'Account', importable: AccountsImportable },
{ // {
resource: 'UncategorizedCashflowTransaction', // resource: 'UncategorizedCashflowTransaction',
importable: UncategorizedTransactionsImportable, // importable: UncategorizedTransactionsImportable,
}, // },
{ resource: 'Customer', importable: CustomersImportable }, // { resource: 'Customer', importable: CustomersImportable },
{ resource: 'Vendor', importable: VendorsImportable }, // { resource: 'Vendor', importable: VendorsImportable },
{ resource: 'Item', importable: ItemsImportable }, // { resource: 'Item', importable: ItemsImportable },
{ resource: 'ItemCategory', importable: ItemCategoriesImportable }, // { resource: 'ItemCategory', importable: ItemCategoriesImportable },
{ resource: 'ManualJournal', importable: ManualJournalImportable }, // { resource: 'ManualJournal', importable: ManualJournalImportable },
{ resource: 'Bill', importable: BillsImportable }, // { resource: 'Bill', importable: BillsImportable },
{ resource: 'Expense', importable: ExpensesImportable }, // { resource: 'Expense', importable: ExpensesImportable },
{ resource: 'SaleInvoice', importable: SaleInvoicesImportable }, // { resource: 'SaleInvoice', importable: SaleInvoicesImportable },
{ resource: 'SaleEstimate', importable: SaleEstimatesImportable }, // { resource: 'SaleEstimate', importable: SaleEstimatesImportable },
{ resource: 'BillPayment', importable: BillPaymentsImportable }, // { resource: 'BillPayment', importable: BillPaymentsImportable },
{ resource: 'PaymentReceive', importable: PaymentsReceivedImportable }, // { resource: 'PaymentReceive', importable: PaymentsReceivedImportable },
{ resource: 'VendorCredit', importable: VendorCreditsImportable }, // { resource: 'VendorCredit', importable: VendorCreditsImportable },
{ resource: 'CreditNote', importable: CreditNotesImportable }, // { resource: 'CreditNote', importable: CreditNotesImportable },
{ resource: 'SaleReceipt', importable: SaleReceiptsImportable }, // { resource: 'SaleReceipt', importable: SaleReceiptsImportable },
{ resource: 'TaxRate', importable: TaxRatesImportable }, // { resource: 'TaxRate', importable: TaxRatesImportable },
]; ];
public get registry() { public get registry() {
@@ -64,8 +64,8 @@ export class ImportableResources {
const instance = ImportableRegistry.getInstance(); const instance = ImportableRegistry.getInstance();
this.importables.forEach((importable) => { this.importables.forEach((importable) => {
const importableInstance = Container.get(importable.importable); // const importableInstance = Container.get(importable.importable);
instance.registerImportable(importable.resource, importableInstance); // instance.registerImportable(importable.resource, importableInstance);
}); });
ImportableResources.registry = instance; ImportableResources.registry = instance;
} }

View File

@@ -336,7 +336,7 @@ export const valueParser =
* @param {string} key - Mapped key path. formats: `group.key` or `key`. * @param {string} key - Mapped key path. formats: `group.key` or `key`.
* @returns {string} * @returns {string}
*/ */
export const parseKey = R.curry( export const parseKey: R.Curry<string> = R.curry(
(fields: { [key: string]: IModelMetaField2 }, key: string) => { (fields: { [key: string]: IModelMetaField2 }, key: string) => {
const fieldKey = getFieldKey(key); const fieldKey = getFieldKey(key);
const field = fields[fieldKey]; const field = fields[fieldKey];

View File

@@ -1,28 +1,28 @@
import Container, { Service } from 'typedi'; // import Container, { Service } from 'typedi';
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles'; // import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
@Service() // @Service()
export class ImportDeleteExpiredFilesJobs { // export class ImportDeleteExpiredFilesJobs {
/** // /**
* Constructor method. // * Constructor method.
*/ // */
constructor(agenda) { // constructor(agenda) {
agenda.define('delete-expired-imported-files', this.handler); // agenda.define('delete-expired-imported-files', this.handler);
} // }
/** // /**
* Triggers sending invoice mail. // * Triggers sending invoice mail.
*/ // */
private handler = async (job, done: Function) => { // private handler = async (job, done: Function) => {
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles); // const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
try { // try {
console.log('Delete expired import files has started.'); // console.log('Delete expired import files has started.');
await importDeleteExpiredFiles.deleteExpiredFiles(); // await importDeleteExpiredFiles.deleteExpiredFiles();
done(); // done();
} catch (error) { // } catch (error) {
console.log(error); // console.log(error);
done(error); // done(error);
} // }
}; // };
} // }

View File

@@ -1,7 +1,6 @@
import { Model, raw } from 'objection'; import { Model, raw } from 'objection';
import { castArray } from 'lodash'; import { castArray } from 'lodash';
import moment, { unitOfTime } from 'moment'; import * as moment from 'moment';
import { BaseModel } from '@/models/Model';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils'; import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
import { TInventoryTransactionDirection } from '../types/InventoryCost.types'; import { TInventoryTransactionDirection } from '../types/InventoryCost.types';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
@@ -53,7 +52,7 @@ export class InventoryTransaction extends TenantBaseModel {
query, query,
startDate, startDate,
endDate, endDate,
type: unitOfTime.StartOf = 'day', type: moment.unitOfTime.StartOf = 'day',
) { ) {
const dateFormat = 'YYYY-MM-DD'; const dateFormat = 'YYYY-MM-DD';
const fromDate = moment(startDate).startOf(type).format(dateFormat); const fromDate = moment(startDate).startOf(type).format(dateFormat);

View File

@@ -4,6 +4,7 @@ import { ILedger } from './types/Ledger.types';
import { ILedgerEntry } from './types/Ledger.types'; import { ILedgerEntry } from './types/Ledger.types';
import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; import { AccountTransaction } from '../Accounts/models/AccountTransaction.model';
import { IAccountTransaction } from '@/interfaces/Account'; import { IAccountTransaction } from '@/interfaces/Account';
import { ModelObject } from 'objection';
export class Ledger implements ILedger { export class Ledger implements ILedger {
readonly entries: ILedgerEntry[]; readonly entries: ILedgerEntry[];
@@ -71,7 +72,8 @@ export class Ledger implements ILedger {
return this.filter( return this.filter(
(entry) => (entry) =>
fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date) fromDateParsed.isBefore(entry.date) ||
fromDateParsed.isSame(entry.date),
); );
} }
@@ -85,7 +87,7 @@ export class Ledger implements ILedger {
return this.filter( return this.filter(
(entry) => (entry) =>
toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date) toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date),
); );
} }
@@ -191,7 +193,7 @@ export class Ledger implements ILedger {
*/ */
public getAccountsIds = (): number[] => { public getAccountsIds = (): number[] => {
return uniqBy(this.entries, 'accountId').map( return uniqBy(this.entries, 'accountId').map(
(e: ILedgerEntry) => e.accountId (e: ILedgerEntry) => e.accountId,
); );
}; };
@@ -222,22 +224,21 @@ export class Ledger implements ILedger {
// --------------------------------- // ---------------------------------
// # STATIC METHODS. // # STATIC METHODS.
// ---------------------------------- // ----------------------------------
/** /**
* Mappes the account transactions to ledger entries. * Mappes the account transactions to ledger entries.
* @param {IAccountTransaction[]} entries * @param {IAccountTransaction[]} entries
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */
static mappingTransactions(entries: AccountTransaction[]): ILedgerEntry[] { static mappingTransactions(entries: ModelObject<AccountTransaction>[]): ILedgerEntry[] {
return entries.map(this.mapTransaction); return entries.map(this.mapTransaction);
} }
/** /**
* Mappes the account transaction to ledger entry. * Mappes the account transaction to ledger entry.
* @param {IAccountTransaction} entry * @param {IAccountTransaction} entry - Account transaction.
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
*/ */
static mapTransaction(entry: AccountTransaction): ILedgerEntry { static mapTransaction(entry: ModelObject<AccountTransaction>): ILedgerEntry {
return { return {
credit: defaultTo(entry.credit, 0), credit: defaultTo(entry.credit, 0),
debit: defaultTo(entry.debit, 0), debit: defaultTo(entry.debit, 0),
@@ -277,7 +278,9 @@ export class Ledger implements ILedger {
* @param {IAccountTransaction[]} transactions * @param {IAccountTransaction[]} transactions
* @returns {ILedger} * @returns {ILedger}
*/ */
static fromTransactions(transactions: AccountTransaction[]): Ledger { static fromTransactions(
transactions: Array<ModelObject<AccountTransaction>>,
): Ledger {
const entries = Ledger.mappingTransactions(transactions); const entries = Ledger.mappingTransactions(transactions);
return new Ledger(entries); return new Ledger(entries);
} }

View File

@@ -0,0 +1,7 @@
export const accumSum = (data: any[], callback: (data: any) => number): number => {
return data.reduce((acc, _data) => {
const amount = callback(_data);
return acc + amount;
}, 0);
};

View File

@@ -4,7 +4,7 @@ export const dateRangeCollection = (
fromDate, fromDate,
toDate, toDate,
addType: moment.unitOfTime.StartOf = 'day', addType: moment.unitOfTime.StartOf = 'day',
increment = 1, increment: number = 1,
) => { ) => {
const collection = []; const collection = [];
const momentFromDate = moment(fromDate); const momentFromDate = moment(fromDate);
@@ -26,7 +26,7 @@ export const dateRangeCollection = (
for ( for (
let i = momentFromDate; let i = momentFromDate;
i.isBefore(toDate, addType) || i.isSame(toDate, addType); i.isBefore(toDate, addType) || i.isSame(toDate, addType);
i.add(increment, `${addType}s`) i.add(increment, `${addType}s` as moment.unitOfTime.DurationConstructor)
) { ) {
collection.push(i.endOf(addType).format(dateFormat)); collection.push(i.endOf(addType).format(dateFormat));
} }
@@ -37,7 +37,7 @@ export const dateRangeFromToCollection = (
fromDate: moment.MomentInput, fromDate: moment.MomentInput,
toDate: moment.MomentInput, toDate: moment.MomentInput,
addType: moment.unitOfTime.StartOf = 'day', addType: moment.unitOfTime.StartOf = 'day',
increment = 1, increment: number = 1,
) => { ) => {
const collection = []; const collection = [];
const momentFromDate = moment(fromDate); const momentFromDate = moment(fromDate);
@@ -46,7 +46,7 @@ export const dateRangeFromToCollection = (
for ( for (
let i = momentFromDate; let i = momentFromDate;
i.isBefore(toDate, addType) || i.isSame(toDate, addType); i.isBefore(toDate, addType) || i.isSame(toDate, addType);
i.add(increment, `${addType}s`) i.add(increment, `${addType}s` as moment.unitOfTime.DurationConstructor)
) { ) {
collection.push({ collection.push({
fromDate: i.startOf(addType).format(dateFormat), fromDate: i.startOf(addType).format(dateFormat),

View File

@@ -0,0 +1,131 @@
// @ts-nocheck
import * as _ from 'lodash';
import * as addDeepdash from 'deepdash';
const {
condense,
condenseDeep,
eachDeep,
exists,
filterDeep,
findDeep,
findPathDeep,
findValueDeep,
forEachDeep,
index,
keysDeep,
mapDeep,
mapKeysDeep,
mapValuesDeep,
mapValues,
omitDeep,
pathMatches,
pathToString,
paths,
pickDeep,
reduceDeep,
someDeep,
iteratee,
} = addDeepdash(_);
const mapValuesDeepReverse = (nodes, callback, config?) => {
const clonedNodes = _.clone(nodes);
const nodesPaths = paths(nodes, config);
const reversedPaths = _.reverse(nodesPaths);
reversedPaths.forEach((pathStack: string[], i) => {
const node = _.get(clonedNodes, pathStack);
const pathString = pathToString(pathStack);
const children = _.get(
clonedNodes,
`${pathString}.${config.childrenPath}`,
[]
);
const mappedNode = callback(node, children);
if (!mappedNode.children && children) {
mappedNode.children = children;
}
_.set(clonedNodes, pathString, mappedNode);
});
return clonedNodes;
};
const filterNodesDeep = (predicate, nodes) => {
return condense(
reduceDeep(
nodes,
(accumulator, value, key, parent, context) => {
const newValue = { ...value };
if (newValue.children) {
_.set(newValue, 'children', condense(value.children));
}
const isTrue = predicate(newValue, key, parent, context);
if (isTrue === true) {
_.set(accumulator, context.path, newValue);
} else if (isTrue === false) {
_.unset(accumulator, context.path);
}
return accumulator;
},
[],
{
childrenPath: 'children',
pathFormat: 'array',
callbackAfterIterate: true,
}
)
);
};
const flatNestedTree = (obj, mapper, options) => {
return reduceDeep(
obj,
(accumulator, value, key, parentValue, context) => {
const computedValue = _.omit(value, ['children']);
const mappedValue = mapper
? mapper(computedValue, key, context)
: computedValue;
accumulator.push(mappedValue);
return accumulator;
},
[],
{
childrenPath: 'children',
pathFormat: 'array',
...options,
}
);
};
export {
iteratee,
condense,
condenseDeep,
eachDeep,
exists,
filterDeep,
findDeep,
findPathDeep,
findValueDeep,
forEachDeep,
index,
keysDeep,
mapDeep,
mapKeysDeep,
mapValuesDeep,
mapValues,
omitDeep,
pathMatches,
pathToString,
paths,
pickDeep,
reduceDeep,
someDeep,
mapValuesDeepReverse,
filterNodesDeep,
flatNestedTree,
};

View File

@@ -15,6 +15,19 @@ const getCurrencySign = (currencyCode) => {
return get(Currencies, `${currencyCode}.symbol`); return get(Currencies, `${currencyCode}.symbol`);
}; };
export interface IFormatNumberSettings {
precision?: number;
divideOn1000?: boolean;
excerptZero?: boolean;
negativeFormat?: string;
thousand?: string;
decimal?: string;
zeroSign?: string;
money?: boolean;
currencyCode?: string;
symbol?: string;
}
export const formatNumber = ( export const formatNumber = (
balance, balance,
{ {
@@ -28,7 +41,7 @@ export const formatNumber = (
money = true, money = true,
currencyCode, currencyCode,
symbol = '', symbol = '',
}, }: IFormatNumberSettings,
) => { ) => {
const formattedSymbol = getCurrencySign(currencyCode); const formattedSymbol = getCurrencySign(currencyCode);
const negForamt = getNegativeFormat(negativeFormat); const negForamt = getNegativeFormat(negativeFormat);

View File

@@ -5,11 +5,13 @@
"test", "test",
"dist", "dist",
"**/*spec.ts", "**/*spec.ts",
// "./src/modules/DynamicListing/**/*.ts", "./src/modules/DynamicListing/**/*.ts",
"./src/modules/Import/**/*.ts", "./src/modules/Import/**/*.ts",
"./src/modules/Export/**/*.ts", "./src/modules/Export/**/*.ts",
"./src/modules/DynamicListing", "./src/modules/DynamicListing",
"./src/modules/DynamicListing/**/*.ts", "./src/modules/DynamicListing/**/*.ts",
"./src/modules/FinancialStatements/**/*.ts",
// "./src/modules/FinancialStatements/modules/BalanceSheet/**.ts",
"./src/modules/Views", "./src/modules/Views",
"./src/modules/Expenses/subscribers" "./src/modules/Expenses/subscribers"
] ]

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": false,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,

View File

@@ -10,10 +10,7 @@ import {
import { FinancialPreviousYear } from '../FinancialPreviousYear'; import { FinancialPreviousYear } from '../FinancialPreviousYear';
export const BalanceSheetComparsionPreviousYear = (Base: any) => export const BalanceSheetComparsionPreviousYear = (Base: any) =>
class class extends R.compose(FinancialPreviousYear)(Base) {
extends R.compose(FinancialPreviousYear)(Base)
implements IBalanceSheetComparsions
{
// ------------------------------ // ------------------------------
// # Account // # Account
// ------------------------------ // ------------------------------

View File

@@ -1,28 +1,28 @@
import Container, { Service } from 'typedi'; // import Container, { Service } from 'typedi';
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles'; // import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
@Service() // @Service()
export class ImportDeleteExpiredFilesJobs { // export class ImportDeleteExpiredFilesJobs {
/** // /**
* Constructor method. // * Constructor method.
*/ // */
constructor(agenda) { // constructor(agenda) {
agenda.define('delete-expired-imported-files', this.handler); // agenda.define('delete-expired-imported-files', this.handler);
} // }
/** // /**
* Triggers sending invoice mail. // * Triggers sending invoice mail.
*/ // */
private handler = async (job, done: Function) => { // private handler = async (job, done: Function) => {
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles); // const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
try { // try {
console.log('Delete expired import files has started.'); // console.log('Delete expired import files has started.');
await importDeleteExpiredFiles.deleteExpiredFiles(); // await importDeleteExpiredFiles.deleteExpiredFiles();
done(); // done();
} catch (error) { // } catch (error) {
console.log(error); // console.log(error);
done(error); // done(error);
} // }
}; // };
} // }

View File

@@ -771,4 +771,5 @@ export default {
onSalesByItemViewed: 'onSalesByItemViewed', onSalesByItemViewed: 'onSalesByItemViewed',
onPurchasesByItemViewed: 'onPurchasesByItemViewed', onPurchasesByItemViewed: 'onPurchasesByItemViewed',
}, },
}; };

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'; import * as _ from 'lodash';
import deepdash from 'deepdash'; import * as deepdash from 'deepdash';
const { const {
condense, condense,
@@ -24,7 +24,7 @@ const {
reduceDeep, reduceDeep,
someDeep, someDeep,
iteratee, iteratee,
} = deepdash(_); } = deepdash.default(_);
const mapValuesDeepReverse = (nodes, callback, config?) => { const mapValuesDeepReverse = (nodes, callback, config?) => {
const clonedNodes = _.clone(nodes); const clonedNodes = _.clone(nodes);

Some files were not shown because too many files have changed in this diff Show More