mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-05-12 22:05:01 +00:00
refactor: financial reports to nestjs
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"express-validator": "^7.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fp-ts": "^2.16.9",
|
||||
@@ -75,6 +76,7 @@
|
||||
"ramda": "^0.30.1",
|
||||
"redis": "^4.7.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"remeda": "^2.19.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"serialize-interceptor": "^1.1.7",
|
||||
"strategy": "^1.1.1",
|
||||
@@ -82,7 +84,8 @@
|
||||
"uuid": "^10.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^0.28.1",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"mathjs": "^9.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
@@ -93,12 +96,14 @@
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/yup": "^0.29.13",
|
||||
"@types/mathjs": "^6.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"mustache": "^3.0.3",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
@@ -106,8 +111,7 @@
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3",
|
||||
"mustache": "^3.0.3"
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
@@ -180,6 +180,7 @@ export const events = {
|
||||
* Sales estimates service.
|
||||
*/
|
||||
saleEstimate: {
|
||||
onViewed: 'onSaleEstimateViewed',
|
||||
onPdfViewed: 'onSaleEstimatePdfViewed',
|
||||
|
||||
onCreating: 'onSaleEstimateCreating',
|
||||
@@ -212,9 +213,7 @@ export const events = {
|
||||
|
||||
onPreMailSend: 'onSaleEstimatePreMailSend',
|
||||
onMailSend: 'onSaleEstimateMailSend',
|
||||
onMailSent: 'onSaleEstimateMailSend',
|
||||
|
||||
onViewed: 'onSaleEstimateViewed',
|
||||
onMailSent: 'onSaleEstimateMailSent',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -753,4 +752,24 @@ export const events = {
|
||||
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
|
||||
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',
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
|
||||
export type Constructor = new (...args: any[]) => {};
|
||||
export type GConstructor<T> = new (...args: any[]) => T;
|
||||
@@ -14,6 +14,7 @@ import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
|
||||
import { Model } from 'objection';
|
||||
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||
// import AccountSettings from './Account.Settings';
|
||||
// import { DEFAULT_VIEWS } from '@/modules/Accounts/constants';
|
||||
// import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
|
||||
@@ -406,10 +407,10 @@ export class Account extends TenantBaseModel {
|
||||
* @param {Object} options
|
||||
*/
|
||||
static toNestedArray(accounts, options = { children: 'children' }) {
|
||||
// return flatToNestedArray(accounts, {
|
||||
// id: 'id',
|
||||
// parentId: 'parentAccountId',
|
||||
// });
|
||||
return flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Param, Post } from '@nestjs/common';
|
||||
import { BankAccountsApplication } from './BankAccountsApplication.service';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('banking/accounts')
|
||||
@ApiTags('banking-accounts')
|
||||
@@ -11,6 +11,14 @@ export class BankAccountsController {
|
||||
@ApiOperation({
|
||||
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) {
|
||||
return this.bankAccountsApplication.disconnectBankAccount(bankAccountId);
|
||||
}
|
||||
@@ -19,6 +27,14 @@ export class BankAccountsController {
|
||||
@ApiOperation({
|
||||
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) {
|
||||
return this.bankAccountsApplication.refreshBankAccount(bankAccountId);
|
||||
}
|
||||
@@ -27,6 +43,14 @@ export class BankAccountsController {
|
||||
@ApiOperation({
|
||||
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) {
|
||||
return this.bankAccountsApplication.pauseBankAccount(bankAccountId);
|
||||
}
|
||||
@@ -35,6 +59,14 @@ export class BankAccountsController {
|
||||
@ApiOperation({
|
||||
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) {
|
||||
return this.bankAccountsApplication.resumeBankAccount(bankAccountId);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TableSheetPdf } from './TableSheetPdf';
|
||||
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({
|
||||
providers: [TableSheetPdf],
|
||||
imports: [PurchasesByItemsModule],
|
||||
providers: [],
|
||||
imports: [
|
||||
PurchasesByItemsModule,
|
||||
CustomerBalanceSummaryModule,
|
||||
SalesByItemsModule,
|
||||
GeneralLedgerModule
|
||||
],
|
||||
})
|
||||
export class FinancialStatementsModule {}
|
||||
|
||||
@@ -3,14 +3,16 @@ import { memoize } from 'lodash';
|
||||
import {
|
||||
IAccountTransactionsGroupBy,
|
||||
IFinancialDatePeriodsUnit,
|
||||
IFinancialSheetTotalPeriod,
|
||||
IFormatNumberSettings,
|
||||
} from '../types/Report.types';
|
||||
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
|
||||
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) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getDateRanges = memoize(
|
||||
(fromDate: Date, toDate: Date, unit: string) => {
|
||||
(fromDate: Date, toDate: Date, unit: moment.unitOfTime.StartOf) => {
|
||||
return dateRangeFromToCollection(fromDate, toDate, unit);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -35,7 +37,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings?: IFormatNumberSettings
|
||||
overrideSettings?: IFormatNumberSettings,
|
||||
): IFinancialSheetTotalPeriod => {
|
||||
return {
|
||||
fromDate: this.getDateMeta(fromDate),
|
||||
@@ -55,7 +57,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
overrideSettings: IFormatNumberSettings = {},
|
||||
) => {
|
||||
return this.getDatePeriodMeta(total, fromDate, toDate, {
|
||||
money: true,
|
||||
@@ -79,8 +81,8 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
|
||||
node: any,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
) => any
|
||||
index: number,
|
||||
) => any,
|
||||
) => {
|
||||
const curriedCallback = R.curry(callback)(node);
|
||||
// Retrieves memorized date ranges.
|
||||
@@ -88,7 +90,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
|
||||
return dateRanges.map((dateRange, index) => {
|
||||
return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
/**
|
||||
* Retrieve the accounts transactions group type from display columns by.
|
||||
@@ -96,7 +98,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
|
||||
* @returns {IAccountTransactionsGroupBy}
|
||||
*/
|
||||
public getGroupByFromDisplayColumnsBy = (
|
||||
columnsBy: IFinancialDatePeriodsUnit
|
||||
columnsBy: IFinancialDatePeriodsUnit,
|
||||
): IAccountTransactionsGroupBy => {
|
||||
const paris = {
|
||||
week: IAccountTransactionsGroupBy.Day,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import moment from 'moment';
|
||||
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 {
|
||||
/**
|
||||
* Retrieve previous period (PP) date of the given date.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
* @param {IFinancialDatePeriodsUnit} unit -
|
||||
* @param {Date} date - Date.
|
||||
* @param {number} value - Value.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @returns {Date}
|
||||
*/
|
||||
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} toDate
|
||||
* @returns
|
||||
* @returns {number}
|
||||
*/
|
||||
public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => {
|
||||
return moment(toDate).diff(fromDate, 'days') + 1;
|
||||
@@ -31,8 +34,11 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
|
||||
|
||||
/**
|
||||
* Retrieves the periods period dates.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @param {number} amount - Amount of time.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPreviousPeriodDateRange = (
|
||||
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.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
* @param {IFinancialDatePeriodsUnit}
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPPDatePeriodDateRange = (
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import * as mathjs from 'mathjs';
|
||||
import * as R from 'ramda';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { omit, get, mapValues } from 'lodash';
|
||||
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) =>
|
||||
class extends compose(FinancialSheetStructure)(Base) {
|
||||
export const FinancialEvaluateEquation = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T
|
||||
) =>
|
||||
class FinancialEvaluateEquation extends R.compose(FinancialSheetStructure)(Base) {
|
||||
/**
|
||||
* Evauluate equaation string with the given scope table.
|
||||
* @param {string} equation -
|
||||
@@ -34,7 +38,6 @@ export const FinancialEvaluateEquation = <T extends Constructor>(Base: T) =>
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as R from 'ramda';
|
||||
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) =>
|
||||
class extends Base {
|
||||
export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class FinancialHorizTotals extends Base {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,14 @@ import {
|
||||
IFinancialNodeWithPreviousPeriod,
|
||||
} from '../types/Report.types';
|
||||
import * as R from 'ramda';
|
||||
import { Constructor, GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { FinancialDatePeriods } from './FinancialDatePeriods';
|
||||
|
||||
export const FinancialPreviousPeriod = (Base) =>
|
||||
class extends Base {
|
||||
export const FinancialPreviousPeriod = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.compose(FinancialDatePeriods)(Base) {
|
||||
// ---------------------------
|
||||
// # Common Node.
|
||||
// ---------------------------
|
||||
@@ -16,16 +21,16 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
public assocPreviousPeriodPercentageNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
accountNode: IProfitLossSheetAccountNode,
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
accountNode.previousPeriod.amount,
|
||||
accountNode.previousPeriodChange.amount
|
||||
accountNode.previousPeriodChange.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodPercentage',
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
accountNode
|
||||
accountNode,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,16 +40,16 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
public assocPreviousPeriodChangeNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
accountNode: IProfitLossSheetAccountNode,
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const change = this.getAmountChange(
|
||||
accountNode.total.amount,
|
||||
accountNode.previousPeriod.amount
|
||||
accountNode.previousPeriod.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodChange',
|
||||
this.getAmountMeta(change),
|
||||
accountNode
|
||||
accountNode,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,16 +62,16 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
public assocPreviousPeriodTotalPercentageNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
accountNode: IProfitLossSheetAccountNode,
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
accountNode.previousPeriod.amount,
|
||||
accountNode.previousPeriodChange.amount
|
||||
accountNode.previousPeriodChange.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodPercentage',
|
||||
this.getPercentageTotalAmountMeta(percentage),
|
||||
accountNode
|
||||
accountNode,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,16 +81,16 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
public assocPreviousPeriodTotalChangeNode = (
|
||||
accountNode: any
|
||||
accountNode: any,
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const change = this.getAmountChange(
|
||||
accountNode.total.amount,
|
||||
accountNode.previousPeriod.amount
|
||||
accountNode.previousPeriod.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodChange',
|
||||
this.getTotalAmountMeta(change),
|
||||
accountNode
|
||||
accountNode,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -97,19 +102,19 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
public assocPreviousPeriodHorizNodeFromToDates = R.curry(
|
||||
(
|
||||
periodUnit: IFinancialDatePeriodsUnit,
|
||||
horizNode: any
|
||||
horizNode: any,
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const { fromDate: PPFromDate, toDate: PPToDate } =
|
||||
this.getPreviousPeriodDateRange(
|
||||
horizNode.fromDate.date,
|
||||
horizNode.toDate.date,
|
||||
periodUnit
|
||||
periodUnit,
|
||||
);
|
||||
return R.compose(
|
||||
R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)),
|
||||
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate))
|
||||
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate)),
|
||||
)(horizNode);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -121,7 +126,7 @@ export const FinancialPreviousPeriod = (Base) =>
|
||||
public getPPHorizNodesTotalSumation = (index: number, node): number => {
|
||||
return sumBy(
|
||||
node.children,
|
||||
`horizontalTotals[${index}].previousPeriod.amount`
|
||||
`horizontalTotals[${index}].previousPeriod.amount`,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash'
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IFinancialCommonHorizDatePeriodNode,
|
||||
IFinancialCommonNode,
|
||||
IFinancialNodeWithPreviousYear,
|
||||
} from '../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { FinancialDatePeriods } from './FinancialDatePeriods';
|
||||
|
||||
export const FinancialPreviousYear = (Base) =>
|
||||
class extends Base {
|
||||
export const FinancialPreviousYear = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.compose(FinancialDatePeriods)(Base) {
|
||||
// ---------------------------
|
||||
// # Common Node
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year change attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
* @param {IFinancialCommonNode & IFinancialNodeWithPreviousYear} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousYear}
|
||||
*/
|
||||
public assocPreviousYearChangetNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const change = this.getAmountChange(
|
||||
node.total.amount,
|
||||
node.previousYear.amount
|
||||
node.previousYear.amount,
|
||||
);
|
||||
return R.assoc('previousYearChange', this.getAmountMeta(change), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year percentage attribute to account node.
|
||||
*
|
||||
* % increase = Increase ÷ Original Number × 100.
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
public assocPreviousYearPercentageNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
node.previousYear.amount,
|
||||
node.previousYearChange.amount
|
||||
node.previousYearChange.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearPercentage',
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
node
|
||||
node,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,16 +58,16 @@ export const FinancialPreviousYear = (Base) =>
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
public assocPreviousYearTotalChangeNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const change = this.getAmountChange(
|
||||
node.total.amount,
|
||||
node.previousYear.amount
|
||||
node.previousYear.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearChange',
|
||||
this.getTotalAmountMeta(change),
|
||||
node
|
||||
node,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,16 +77,16 @@ export const FinancialPreviousYear = (Base) =>
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
public assocPreviousYearTotalPercentageNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
node.previousYear.amount,
|
||||
node.previousYearChange.amount
|
||||
node.previousYearChange.amount,
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearPercentage',
|
||||
this.getPercentageTotalAmountMeta(percentage),
|
||||
node
|
||||
node,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -92,27 +96,27 @@ export const FinancialPreviousYear = (Base) =>
|
||||
* @returns
|
||||
*/
|
||||
public assocPreviousYearHorizNodeFromToDates = (
|
||||
horizNode: IFinancialCommonHorizDatePeriodNode
|
||||
horizNode: IFinancialCommonHorizDatePeriodNode,
|
||||
) => {
|
||||
const PYFromDate = this.getPreviousYearDate(horizNode.fromDate.date);
|
||||
const PYToDate = this.getPreviousYearDate(horizNode.toDate.date);
|
||||
|
||||
return R.compose(
|
||||
R.assoc('previousYearToDate', this.getDateMeta(PYToDate)),
|
||||
R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate))
|
||||
R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate)),
|
||||
)(horizNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves PP total sumation of the given horiz index node.
|
||||
* @param {number} index
|
||||
* @param {} node
|
||||
* Retrieves PP total sumation of the given horiz index node.
|
||||
* @param {number} index
|
||||
* @param {} node
|
||||
* @returns {number}
|
||||
*/
|
||||
public getPYHorizNodesTotalSumation = (index: number, node): number => {
|
||||
return sumBy(
|
||||
node.children,
|
||||
`horizontalTotals[${index}].previousYear.amount`
|
||||
)
|
||||
}
|
||||
`horizontalTotals[${index}].previousYear.amount`,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as R from 'ramda';
|
||||
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) =>
|
||||
class extends R.compose(FinancialSheetStructure)(Base) {
|
||||
export const FinancialSchema = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T
|
||||
) =>
|
||||
class FinancialSchema extends R.compose(FinancialSheetStructure)(Base) {
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
@@ -17,7 +20,7 @@ export const FinancialSchema = <T extends Constructor>(Base: T) =>
|
||||
* @param {string|number} id
|
||||
* @returns
|
||||
*/
|
||||
publicgetSchemaNodeById = (id: string | number) => {
|
||||
public getSchemaNodeById = (id: string | number) => {
|
||||
const schema = this.getSchema();
|
||||
|
||||
return this.findNodeDeep(schema, (node) => node.id === id);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ICashFlowStatementTotal,
|
||||
IFormatNumberSettings,
|
||||
INumberFormatQuery,
|
||||
|
||||
} from '../types/Report.types';
|
||||
import { formatNumber } from '@/utils/format-number';
|
||||
import { IFinancialTableTotal } from '../types/Table.types';
|
||||
|
||||
export default class FinancialSheet {
|
||||
readonly numberFormat: INumberFormatQuery = {
|
||||
export class FinancialSheet {
|
||||
public numberFormat: INumberFormatQuery = {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
};
|
||||
readonly baseCurrency: string;
|
||||
public baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Transformes the number format query to settings
|
||||
@@ -109,7 +110,7 @@ export default class FinancialSheet {
|
||||
protected getAmountMeta(
|
||||
amount: number,
|
||||
overrideSettings?: IFormatNumberSettings
|
||||
): ICashFlowStatementTotal {
|
||||
): IFinancialTableTotal {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount, overrideSettings),
|
||||
@@ -125,7 +126,7 @@ export default class FinancialSheet {
|
||||
protected getTotalAmountMeta(
|
||||
amount: number,
|
||||
title?: string
|
||||
): ICashFlowStatementTotal {
|
||||
): IFinancialTableTotal {
|
||||
return {
|
||||
...(title ? { title } : {}),
|
||||
amount,
|
||||
|
||||
@@ -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 {}
|
||||
@@ -4,10 +4,7 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class FinancialSheetMeta {
|
||||
constructor(
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
) {}
|
||||
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||
|
||||
/**
|
||||
* Retrieves the common meta data of the financial sheet.
|
||||
@@ -20,8 +17,10 @@ export class FinancialSheetMeta {
|
||||
const baseCurrency = tenantMetadata.baseCurrency;
|
||||
const dateFormat = tenantMetadata.dateFormat;
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
// const isCostComputeRunning =
|
||||
// this.inventoryService.isItemsCostComputeRunning();
|
||||
|
||||
const isCostComputeRunning = false;
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
|
||||
@@ -4,16 +4,20 @@ import {
|
||||
mapValuesDeepReverse,
|
||||
mapValuesDeep,
|
||||
mapValues,
|
||||
condense,
|
||||
filterDeep,
|
||||
reduceDeep,
|
||||
findValueDeep,
|
||||
filterNodesDeep,
|
||||
} from 'utils/deepdash';
|
||||
import { Constructor } from '@/common/types/Constructor';
|
||||
} from '@/utils/deepdash';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialSheetStructure = <T extends Constructor>(Base: T) =>
|
||||
class extends Base {
|
||||
export const FinancialSheetStructure = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T
|
||||
) =>
|
||||
class FinancialSheetStructure extends Base {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
|
||||
@@ -2,10 +2,21 @@ import * as R from 'ramda';
|
||||
import { isEmpty, clone, cloneDeep, omit } from 'lodash';
|
||||
import { increment } from '@/utils/increment';
|
||||
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.
|
||||
* @param {ITableColumn[]} columns
|
||||
@@ -23,13 +34,15 @@ export const FinancialTable = (Base) =>
|
||||
});
|
||||
};
|
||||
|
||||
addTotalRow = (node: ITableRow) => {
|
||||
public addTotalRow = (node: ITableRow) => {
|
||||
const clonedNode = clone(node);
|
||||
|
||||
if (clonedNode.children) {
|
||||
const cells = cloneDeep(node.cells);
|
||||
cells[0].value = this.i18n.__('financial_sheet.total_row', {
|
||||
value: cells[0].value,
|
||||
cells[0].value = this.i18n.t('financial_sheet.total_row', {
|
||||
args: {
|
||||
value: cells[0].value,
|
||||
},
|
||||
});
|
||||
|
||||
clonedNode.children.push({
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
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 {
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
getTotalPreviousPeriod = () => {
|
||||
return this.query.PPToDate;
|
||||
};
|
||||
@@ -24,8 +34,8 @@ export const FinancialTablePreviousPeriod = (Base) =>
|
||||
|
||||
return {
|
||||
key: 'previous_period',
|
||||
label: this.i18n.__(`financial_sheet.previoud_period_date`, {
|
||||
date: PPFormatted,
|
||||
label: this.i18n.t(`financial_sheet.previoud_period_date`, {
|
||||
args: { date: PPFormatted, }
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -37,7 +47,7 @@ export const FinancialTablePreviousPeriod = (Base) =>
|
||||
public getPreviousPeriodChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
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 => {
|
||||
return {
|
||||
key: 'previous_period_percentage',
|
||||
label: this.i18n.__('financial_sheet.previous_period_percentage'),
|
||||
label: this.i18n.t('financial_sheet.previous_period_percentage'),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import moment from 'moment';
|
||||
import { ITableColumn, ITableColumnAccessor } from '../types/Table.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 {
|
||||
getTotalPreviousYear = () => {
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
/**
|
||||
* Retrieves the total previous year date.
|
||||
* @returns {Date}
|
||||
*/
|
||||
public getTotalPreviousYear = () => {
|
||||
return this.query.PYToDate;
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// # Columns.
|
||||
// ------------------------------------
|
||||
/**
|
||||
* Retrive previous year total column.
|
||||
* @param {DateRange} previousYear -
|
||||
* @param {DateRange} previousYear -
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public getPreviousYearTotalColumn = (
|
||||
@@ -23,8 +37,8 @@ export const FinancialTablePreviousYear = (Base) =>
|
||||
|
||||
return {
|
||||
key: 'previous_year',
|
||||
label: this.i18n.__('financial_sheet.previous_year_date', {
|
||||
date: PYFormatted,
|
||||
label: this.i18n.t('financial_sheet.previous_year_date', {
|
||||
args: { date: PYFormatted },
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -36,7 +50,7 @@ export const FinancialTablePreviousYear = (Base) =>
|
||||
public getPreviousYearChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
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 => {
|
||||
return {
|
||||
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.
|
||||
* @param {number} index
|
||||
* @param {number} index - Index.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
public getPreviousYearTotalHorizAccessor = (
|
||||
@@ -103,7 +117,7 @@ export const FinancialTablePreviousYear = (Base) =>
|
||||
|
||||
/**
|
||||
* Retrieves previous previous year change horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
public getPreviousYearChangeHorizAccessor = (
|
||||
@@ -117,7 +131,7 @@ export const FinancialTablePreviousYear = (Base) =>
|
||||
|
||||
/**
|
||||
* Retrieves previous year percentage horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
public getPreviousYearPercentageHorizAccessor = (
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as R from 'ramda';
|
||||
import { ITableColumn, ITableData, ITableRow } from './types/Table.types';
|
||||
import { FinancialTableStructure } from './common/FinancialTableStructure';
|
||||
import { tableClassNames } from './utils';
|
||||
import { ITableColumn, ITableData, ITableRow } from '../types/Table.types';
|
||||
import { FinancialTableStructure } from './FinancialTableStructure';
|
||||
import { tableClassNames } from '../utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service';
|
||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { TemplateInjectable } from '../../TemplateInjectable/TemplateInjectable.service';
|
||||
import { ChromiumlyTenancy } from '../../ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
|
||||
@Injectable()
|
||||
export class TableSheetPdf {
|
||||
/**
|
||||
* @param {TemplateInjectable} templateInjectable - The template injectable service.
|
||||
* @param {ChromiumlyTenancy} chromiumlyTenancy - The chromiumly tenancy service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly templateInjectable: TemplateInjectable,
|
||||
private readonly chromiumlyTenancy: ChromiumlyTenancy,
|
||||
@@ -60,8 +64,8 @@ export class TableSheetPdf {
|
||||
|
||||
/**
|
||||
* Converts the table rows to pdf rows.
|
||||
* @param {ITableRow[]} rows -
|
||||
* @returns {ITableRow[]}
|
||||
* @param {ITableRow[]} rows - The table rows to be converted.
|
||||
* @returns {ITableRow[]} - The converted table rows.
|
||||
*/
|
||||
private tablePdfRows = (rows: ITableRow[]): ITableRow[] => {
|
||||
const curriedFlatNestedTree = R.curry(
|
||||
@@ -70,6 +74,8 @@ export class TableSheetPdf {
|
||||
const flatNestedTree = curriedFlatNestedTree(R.__, {
|
||||
nestedPrefix: '<span style="padding-left: 15px;"></span>',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return R.compose(tableClassNames, flatNestedTree)(rows);
|
||||
};
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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: [],
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum ROW_TYPE {
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
OPENING_BALANCE = 'OPENING_BALANCE',
|
||||
TRANSACTION = 'TRANSACTION',
|
||||
CLOSING_BALANCE = 'CLOSING_BALANCE',
|
||||
}
|
||||
@@ -3,14 +3,19 @@ import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
|
||||
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
|
||||
import { ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('/reports/purchases-by-items')
|
||||
@PublicRoute()
|
||||
@ApiTags('reports')
|
||||
export class PurchasesByItemReportController {
|
||||
constructor(
|
||||
private readonly purchasesByItemsApp: PurchasesByItemsApplication,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiResponse({ status: 200, description: 'Purchases by items report' })
|
||||
async purchasesByItems(
|
||||
@Query() filter: IPurchasesByItemsReportQuery,
|
||||
@Res() res: Response,
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
|
||||
import { PurchasesByItemsService } from './PurchasesByItemsService';
|
||||
import { PurchasesByItemsService } from './PurchasesByItems.service';
|
||||
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
|
||||
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
|
||||
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
|
||||
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({
|
||||
imports: [InventoryCostModule, FinancialSheetCommonModule],
|
||||
providers: [
|
||||
PurchasesByItemsTableInjectable,
|
||||
PurchasesByItemsService,
|
||||
PurchasesByItemsExport,
|
||||
PurchasesByItemsPdf,
|
||||
PurchasesByItemsMeta,
|
||||
PurchasesByItemsApplication,
|
||||
TenancyContext,
|
||||
],
|
||||
exports: [PurchasesByItemsApplication],
|
||||
controllers: [PurchasesByItemReportController],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import moment from 'moment';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PurchasesByItems } from './PurchasesByItems';
|
||||
import {
|
||||
@@ -11,9 +10,17 @@ import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTr
|
||||
import { Item } from '@/modules/Items/models/Item';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { getPurchasesByItemsDefaultQuery } from './utils';
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
private readonly purchasesByItemsMeta: PurchasesByItemsMeta,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
@@ -27,39 +34,16 @@ export class PurchasesByItemsService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Defaults purchases by items filter query.
|
||||
* @return {IPurchasesByItemsReportQuery}
|
||||
*/
|
||||
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>}
|
||||
* Retrieve purchases by items statement.
|
||||
* @param {IPurchasesByItemsReportQuery} query - Purchases by items report query.
|
||||
* @return {Promise<IPurchasesByItemsSheet>} - Purchases by items sheet.
|
||||
*/
|
||||
public async purchasesByItems(
|
||||
query: IPurchasesByItemsReportQuery,
|
||||
): Promise<IPurchasesByItemsSheet> {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...getPurchasesByItemsDefaultQuery(),
|
||||
...query,
|
||||
};
|
||||
const inventoryItems = await this.itemModel.query().onBuild((q) => {
|
||||
@@ -88,7 +72,7 @@ export class PurchasesByItemsService {
|
||||
filter,
|
||||
inventoryItems,
|
||||
inventoryTransactions,
|
||||
tenant.metadata.baseCurrency,
|
||||
tenantMetadata.baseCurrency,
|
||||
);
|
||||
const purchasesByItemsData = purchasesByItemsInstance.reportData();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IPurchasesByItemsSheetData,
|
||||
IPurchasesByItemsTotal,
|
||||
} from './types/PurchasesByItems.types';
|
||||
import FinancialSheet from '../../common/FinancialSheet';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||
import { Item } from '@/modules/Items/models/Item';
|
||||
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
|
||||
@@ -48,7 +48,7 @@ export class PurchasesByItems extends FinancialSheet{
|
||||
cost: number;
|
||||
average: number;
|
||||
} {
|
||||
const transaction = this.itemsTransactions.get(itemId);
|
||||
const transaction = this.itemsTransactions.get(itemId.toString());
|
||||
|
||||
const quantity = get(transaction, 'quantity', 0);
|
||||
const cost = get(transaction, 'cost', 0);
|
||||
@@ -105,14 +105,17 @@ export class PurchasesByItems extends FinancialSheet{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
|
||||
quantityPurchased: meta.quantity,
|
||||
purchaseCost: meta.cost,
|
||||
averageCostPrice: meta.average,
|
||||
|
||||
quantityPurchasedFormatted: this.formatNumber(meta.quantity, {
|
||||
money: false,
|
||||
}),
|
||||
purchaseCostFormatted: this.formatNumber(meta.cost),
|
||||
averageCostPriceFormatted: this.formatNumber(meta.average),
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,11 +6,17 @@ import {
|
||||
IPurchasesByItemsTable,
|
||||
} from './types/PurchasesByItems.types';
|
||||
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
|
||||
import { PurchasesByItemsService } from './PurchasesByItemsService';
|
||||
import { PurchasesByItemsService } from './PurchasesByItems.service';
|
||||
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
private readonly purchasesByItemsSheetService: PurchasesByItemsService,
|
||||
private readonly purchasesByItemsTableService: PurchasesByItemsTableInjectable,
|
||||
@@ -21,7 +27,7 @@ export class PurchasesByItemsApplication {
|
||||
/**
|
||||
* Retrieves the purchases by items in json format.
|
||||
* @param {IPurchasesByItemsReportQuery} query
|
||||
* @returns
|
||||
* @returns {Promise<IPurchasesByItemsSheet>}
|
||||
*/
|
||||
public sheet(
|
||||
query: IPurchasesByItemsReportQuery,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TableSheetPdf } from '../../TableSheetPdf';
|
||||
import { TableSheetPdf } from '../../common/TableSheetPdf';
|
||||
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
|
||||
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
|
||||
import { HtmlTableCustomCss } from './_types';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { ITableColumn, ITableColumnAccessor, ITableRow } from '../../types/Table.types';
|
||||
import { FinancialTable } from '../../common/FinancialTable';
|
||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||
import FinancialSheet from '../../common/FinancialSheet';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { tableRowMapper } from '../../utils/Table.utils';
|
||||
|
||||
export class PurchasesByItemsTable extends R.compose(
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
IPurchasesByItemsReportQuery,
|
||||
IPurchasesByItemsTable,
|
||||
} from './types/PurchasesByItems.types';
|
||||
import { PurchasesByItemsService } from './PurchasesByItemsService';
|
||||
import { PurchasesByItemsService } from './PurchasesByItems.service';
|
||||
import { PurchasesByItemsTable } from './PurchasesByItemsTable';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@@ -14,9 +14,8 @@ export class PurchasesByItemsTableInjectable {
|
||||
|
||||
/**
|
||||
* Retrieves the purchases by items table format.
|
||||
* @param {number} tenantId
|
||||
* @param {IPurchasesByItemsReportQuery} filter
|
||||
* @returns {Promise<IPurchasesByItemsTable>}
|
||||
* @param {IPurchasesByItemsReportQuery} filter - The filter to be used.
|
||||
* @returns {Promise<IPurchasesByItemsTable>} - The purchases by items table.
|
||||
*/
|
||||
public async table(
|
||||
filter: IPurchasesByItemsReportQuery,
|
||||
|
||||
@@ -23,15 +23,16 @@ export interface IPurchasesByItemsItem {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
soldCost: number;
|
||||
|
||||
averageSellPrice: number;
|
||||
averageSellPriceFormatted: string;
|
||||
purchaseCost: number;
|
||||
purchaseCostFormatted: string;
|
||||
|
||||
averageCostPrice: number;
|
||||
averageCostPriceFormatted: string;
|
||||
|
||||
quantityPurchased: number;
|
||||
quantityPurchasedFormatted: string;
|
||||
|
||||
soldCostFormatted: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -70,3 +70,44 @@ export interface IDateRange {
|
||||
fromDate: 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;
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export interface ITableCell {
|
||||
|
||||
export type ITableRow = {
|
||||
cells: ITableCell[];
|
||||
rowTypes?: Array<any>
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export interface ITableColumn {
|
||||
@@ -38,3 +40,9 @@ export interface ITableData {
|
||||
export interface IFinancialTable {
|
||||
table: ITableData;
|
||||
}
|
||||
|
||||
export interface IFinancialTableTotal {
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { kebabCase } from 'lodash';
|
||||
import { ITableRow } from '@/interfaces';
|
||||
import { ITableRow } from './types/Table.types';
|
||||
|
||||
export const formatNumber = (balance, { noCents, divideOn1000 }): string => {
|
||||
let formattedBalance: number = parseFloat(balance);
|
||||
|
||||
if (noCents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
formattedBalance = parseInt(formattedBalance.toString(), 10);
|
||||
}
|
||||
if (divideOn1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
return formattedBalance.toString();
|
||||
};
|
||||
|
||||
export const tableClassNames = (rows: ITableRow[]) => {
|
||||
export const tableClassNames = (rows: ITableRow[]): ITableRow[] => {
|
||||
return rows.map((row) => {
|
||||
const classNames =
|
||||
row?.rowTypes?.map((rowType) => `row-type--${kebabCase(rowType)}`) || [];
|
||||
|
||||
@@ -110,8 +110,8 @@ export class ImportFileDataTransformer {
|
||||
valueDTOs: Record<string, any>[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<Record<string, any>[]> {
|
||||
const tenantModels = this.tenancy.models(tenantId);
|
||||
const _valueParser = valueParser(fields, tenantModels, trx);
|
||||
// const tenantModels = this.tenancy.models(tenantId);
|
||||
const _valueParser = valueParser(fields, {}, trx);
|
||||
const _keyParser = parseKey(fields);
|
||||
|
||||
const parseAsync = async (valueDTO) => {
|
||||
|
||||
@@ -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 { UncategorizedTransactionsImportable } from '../BankingCategorize/commands/UncategorizedTransactionsImportable';
|
||||
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
||||
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
||||
import { ItemsImportable } from '../Items/ItemsImportable';
|
||||
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
|
||||
import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport';
|
||||
import { BillsImportable } from '../Purchases/Bills/BillsImportable';
|
||||
import { ExpensesImportable } from '../Expenses/ExpensesImportable';
|
||||
import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
|
||||
import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
|
||||
import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
|
||||
import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
|
||||
import { PaymentsReceivedImportable } from '../Sales/PaymentReceived/PaymentsReceivedImportable';
|
||||
import { CreditNotesImportable } from '../CreditNotes/commands/CreditNotesImportable';
|
||||
import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
|
||||
import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable';
|
||||
// import { UncategorizedTransactionsImportable } from '../BankingCategorize/commands/UncategorizedTransactionsImportable';
|
||||
// import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
||||
// import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
||||
// import { ItemsImportable } from '../Items/ItemsImportable';
|
||||
// import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
|
||||
// import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport';
|
||||
// import { BillsImportable } from '../Purchases/Bills/BillsImportable';
|
||||
// import { ExpensesImportable } from '../Expenses/ExpensesImportable';
|
||||
// import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
|
||||
// import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
|
||||
// import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
|
||||
// import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
|
||||
// import { PaymentsReceivedImportable } from '../Sales/PaymentReceived/PaymentsReceivedImportable';
|
||||
// import { CreditNotesImportable } from '../CreditNotes/commands/CreditNotesImportable';
|
||||
// import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
|
||||
// import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable';
|
||||
|
||||
@Service()
|
||||
@Injectable()
|
||||
export class ImportableResources {
|
||||
private static registry: ImportableRegistry;
|
||||
|
||||
@@ -30,26 +30,26 @@ export class ImportableResources {
|
||||
* Importable instances.
|
||||
*/
|
||||
private importables = [
|
||||
{ resource: 'Account', importable: AccountsImportable },
|
||||
{
|
||||
resource: 'UncategorizedCashflowTransaction',
|
||||
importable: UncategorizedTransactionsImportable,
|
||||
},
|
||||
{ resource: 'Customer', importable: CustomersImportable },
|
||||
{ resource: 'Vendor', importable: VendorsImportable },
|
||||
{ resource: 'Item', importable: ItemsImportable },
|
||||
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
|
||||
{ resource: 'ManualJournal', importable: ManualJournalImportable },
|
||||
{ resource: 'Bill', importable: BillsImportable },
|
||||
{ resource: 'Expense', importable: ExpensesImportable },
|
||||
{ resource: 'SaleInvoice', importable: SaleInvoicesImportable },
|
||||
{ resource: 'SaleEstimate', importable: SaleEstimatesImportable },
|
||||
{ resource: 'BillPayment', importable: BillPaymentsImportable },
|
||||
{ resource: 'PaymentReceive', importable: PaymentsReceivedImportable },
|
||||
{ resource: 'VendorCredit', importable: VendorCreditsImportable },
|
||||
{ resource: 'CreditNote', importable: CreditNotesImportable },
|
||||
{ resource: 'SaleReceipt', importable: SaleReceiptsImportable },
|
||||
{ resource: 'TaxRate', importable: TaxRatesImportable },
|
||||
// { resource: 'Account', importable: AccountsImportable },
|
||||
// {
|
||||
// resource: 'UncategorizedCashflowTransaction',
|
||||
// importable: UncategorizedTransactionsImportable,
|
||||
// },
|
||||
// { resource: 'Customer', importable: CustomersImportable },
|
||||
// { resource: 'Vendor', importable: VendorsImportable },
|
||||
// { resource: 'Item', importable: ItemsImportable },
|
||||
// { resource: 'ItemCategory', importable: ItemCategoriesImportable },
|
||||
// { resource: 'ManualJournal', importable: ManualJournalImportable },
|
||||
// { resource: 'Bill', importable: BillsImportable },
|
||||
// { resource: 'Expense', importable: ExpensesImportable },
|
||||
// { resource: 'SaleInvoice', importable: SaleInvoicesImportable },
|
||||
// { resource: 'SaleEstimate', importable: SaleEstimatesImportable },
|
||||
// { resource: 'BillPayment', importable: BillPaymentsImportable },
|
||||
// { resource: 'PaymentReceive', importable: PaymentsReceivedImportable },
|
||||
// { resource: 'VendorCredit', importable: VendorCreditsImportable },
|
||||
// { resource: 'CreditNote', importable: CreditNotesImportable },
|
||||
// { resource: 'SaleReceipt', importable: SaleReceiptsImportable },
|
||||
// { resource: 'TaxRate', importable: TaxRatesImportable },
|
||||
];
|
||||
|
||||
public get registry() {
|
||||
@@ -64,8 +64,8 @@ export class ImportableResources {
|
||||
const instance = ImportableRegistry.getInstance();
|
||||
|
||||
this.importables.forEach((importable) => {
|
||||
const importableInstance = Container.get(importable.importable);
|
||||
instance.registerImportable(importable.resource, importableInstance);
|
||||
// const importableInstance = Container.get(importable.importable);
|
||||
// instance.registerImportable(importable.resource, importableInstance);
|
||||
});
|
||||
ImportableResources.registry = instance;
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ export const valueParser =
|
||||
* @param {string} key - Mapped key path. formats: `group.key` or `key`.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const parseKey = R.curry(
|
||||
export const parseKey: R.Curry<string> = R.curry(
|
||||
(fields: { [key: string]: IModelMetaField2 }, key: string) => {
|
||||
const fieldKey = getFieldKey(key);
|
||||
const field = fields[fieldKey];
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
|
||||
|
||||
@Service()
|
||||
export class ImportDeleteExpiredFilesJobs {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define('delete-expired-imported-files', this.handler);
|
||||
}
|
||||
// @Service()
|
||||
// export class ImportDeleteExpiredFilesJobs {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define('delete-expired-imported-files', this.handler);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
|
||||
|
||||
try {
|
||||
console.log('Delete expired import files has started.');
|
||||
await importDeleteExpiredFiles.deleteExpiredFiles();
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
// try {
|
||||
// console.log('Delete expired import files has started.');
|
||||
// await importDeleteExpiredFiles.deleteExpiredFiles();
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Model, raw } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import * as moment from 'moment';
|
||||
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
|
||||
import { TInventoryTransactionDirection } from '../types/InventoryCost.types';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
@@ -53,7 +52,7 @@ export class InventoryTransaction extends TenantBaseModel {
|
||||
query,
|
||||
startDate,
|
||||
endDate,
|
||||
type: unitOfTime.StartOf = 'day',
|
||||
type: moment.unitOfTime.StartOf = 'day',
|
||||
) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ILedger } from './types/Ledger.types';
|
||||
import { ILedgerEntry } from './types/Ledger.types';
|
||||
import { AccountTransaction } from '../Accounts/models/AccountTransaction.model';
|
||||
import { IAccountTransaction } from '@/interfaces/Account';
|
||||
import { ModelObject } from 'objection';
|
||||
|
||||
export class Ledger implements ILedger {
|
||||
readonly entries: ILedgerEntry[];
|
||||
@@ -71,7 +72,8 @@ export class Ledger implements ILedger {
|
||||
|
||||
return this.filter(
|
||||
(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(
|
||||
(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[] => {
|
||||
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.
|
||||
// ----------------------------------
|
||||
|
||||
/**
|
||||
* Mappes the account transactions to ledger entries.
|
||||
* @param {IAccountTransaction[]} entries
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
static mappingTransactions(entries: AccountTransaction[]): ILedgerEntry[] {
|
||||
static mappingTransactions(entries: ModelObject<AccountTransaction>[]): ILedgerEntry[] {
|
||||
return entries.map(this.mapTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the account transaction to ledger entry.
|
||||
* @param {IAccountTransaction} entry
|
||||
* @param {IAccountTransaction} entry - Account transaction.
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
static mapTransaction(entry: AccountTransaction): ILedgerEntry {
|
||||
static mapTransaction(entry: ModelObject<AccountTransaction>): ILedgerEntry {
|
||||
return {
|
||||
credit: defaultTo(entry.credit, 0),
|
||||
debit: defaultTo(entry.debit, 0),
|
||||
@@ -277,7 +278,9 @@ export class Ledger implements ILedger {
|
||||
* @param {IAccountTransaction[]} transactions
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
static fromTransactions(transactions: AccountTransaction[]): Ledger {
|
||||
static fromTransactions(
|
||||
transactions: Array<ModelObject<AccountTransaction>>,
|
||||
): Ledger {
|
||||
const entries = Ledger.mappingTransactions(transactions);
|
||||
return new Ledger(entries);
|
||||
}
|
||||
|
||||
7
packages/server-nest/src/utils/accum-sum.ts
Normal file
7
packages/server-nest/src/utils/accum-sum.ts
Normal 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);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ export const dateRangeCollection = (
|
||||
fromDate,
|
||||
toDate,
|
||||
addType: moment.unitOfTime.StartOf = 'day',
|
||||
increment = 1,
|
||||
increment: number = 1,
|
||||
) => {
|
||||
const collection = [];
|
||||
const momentFromDate = moment(fromDate);
|
||||
@@ -26,7 +26,7 @@ export const dateRangeCollection = (
|
||||
for (
|
||||
let i = momentFromDate;
|
||||
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));
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const dateRangeFromToCollection = (
|
||||
fromDate: moment.MomentInput,
|
||||
toDate: moment.MomentInput,
|
||||
addType: moment.unitOfTime.StartOf = 'day',
|
||||
increment = 1,
|
||||
increment: number = 1,
|
||||
) => {
|
||||
const collection = [];
|
||||
const momentFromDate = moment(fromDate);
|
||||
@@ -46,7 +46,7 @@ export const dateRangeFromToCollection = (
|
||||
for (
|
||||
let i = momentFromDate;
|
||||
i.isBefore(toDate, addType) || i.isSame(toDate, addType);
|
||||
i.add(increment, `${addType}s`)
|
||||
i.add(increment, `${addType}s` as moment.unitOfTime.DurationConstructor)
|
||||
) {
|
||||
collection.push({
|
||||
fromDate: i.startOf(addType).format(dateFormat),
|
||||
|
||||
131
packages/server-nest/src/utils/deepdash.ts
Normal file
131
packages/server-nest/src/utils/deepdash.ts
Normal 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,
|
||||
};
|
||||
@@ -15,6 +15,19 @@ const getCurrencySign = (currencyCode) => {
|
||||
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 = (
|
||||
balance,
|
||||
{
|
||||
@@ -28,7 +41,7 @@ export const formatNumber = (
|
||||
money = true,
|
||||
currencyCode,
|
||||
symbol = '',
|
||||
},
|
||||
}: IFormatNumberSettings,
|
||||
) => {
|
||||
const formattedSymbol = getCurrencySign(currencyCode);
|
||||
const negForamt = getNegativeFormat(negativeFormat);
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
"test",
|
||||
"dist",
|
||||
"**/*spec.ts",
|
||||
// "./src/modules/DynamicListing/**/*.ts",
|
||||
"./src/modules/DynamicListing/**/*.ts",
|
||||
"./src/modules/Import/**/*.ts",
|
||||
"./src/modules/Export/**/*.ts",
|
||||
"./src/modules/DynamicListing",
|
||||
"./src/modules/DynamicListing/**/*.ts",
|
||||
"./src/modules/FinancialStatements/**/*.ts",
|
||||
// "./src/modules/FinancialStatements/modules/BalanceSheet/**.ts",
|
||||
"./src/modules/Views",
|
||||
"./src/modules/Expenses/subscribers"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"declaration": false,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
import { FinancialPreviousYear } from '../FinancialPreviousYear';
|
||||
|
||||
export const BalanceSheetComparsionPreviousYear = (Base: any) =>
|
||||
class
|
||||
extends R.compose(FinancialPreviousYear)(Base)
|
||||
implements IBalanceSheetComparsions
|
||||
{
|
||||
class extends R.compose(FinancialPreviousYear)(Base) {
|
||||
// ------------------------------
|
||||
// # Account
|
||||
// ------------------------------
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
|
||||
|
||||
@Service()
|
||||
export class ImportDeleteExpiredFilesJobs {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define('delete-expired-imported-files', this.handler);
|
||||
}
|
||||
// @Service()
|
||||
// export class ImportDeleteExpiredFilesJobs {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define('delete-expired-imported-files', this.handler);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
|
||||
|
||||
try {
|
||||
console.log('Delete expired import files has started.');
|
||||
await importDeleteExpiredFiles.deleteExpiredFiles();
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
// try {
|
||||
// console.log('Delete expired import files has started.');
|
||||
// await importDeleteExpiredFiles.deleteExpiredFiles();
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -771,4 +771,5 @@ export default {
|
||||
onSalesByItemViewed: 'onSalesByItemViewed',
|
||||
onPurchasesByItemViewed: 'onPurchasesByItemViewed',
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import deepdash from 'deepdash';
|
||||
import * as _ from 'lodash';
|
||||
import * as deepdash from 'deepdash';
|
||||
|
||||
const {
|
||||
condense,
|
||||
@@ -24,7 +24,7 @@ const {
|
||||
reduceDeep,
|
||||
someDeep,
|
||||
iteratee,
|
||||
} = deepdash(_);
|
||||
} = deepdash.default(_);
|
||||
|
||||
const mapValuesDeepReverse = (nodes, callback, config?) => {
|
||||
const clonedNodes = _.clone(nodes);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user