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