refactor: financial statements to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-20 01:05:33 +02:00
parent 6550e88af3
commit 9eec60ea22
56 changed files with 3440 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
import { Model, raw } from 'objection';
import moment, { unitOfTime } from 'moment';
import * as moment from 'moment';
import { unitOfTime } from 'moment';
import { isEmpty, castArray } from 'lodash';
import { BaseModel } from '@/models/Model';
import { Account } from './Account.model';
@@ -10,6 +11,7 @@ export class AccountTransaction extends BaseModel {
public readonly referenceId: number;
public readonly accountId: number;
public readonly contactId: number;
public readonly contactType: string;
public readonly credit: number;
public readonly debit: number;
public readonly exchangeRate: number;

View File

@@ -3,6 +3,10 @@ import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByIt
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module';
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
import { TransactionsByVendorModule } from './modules/TransactionsByVendor/TransactionsByVendor.module';
//
@Module({
providers: [],
@@ -10,7 +14,14 @@ import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.modul
PurchasesByItemsModule,
CustomerBalanceSummaryModule,
SalesByItemsModule,
GeneralLedgerModule
GeneralLedgerModule,
TrialBalanceSheetModule,
TransactionsByCustomerModule,
TransactionsByVendorModule,
// TransactionsByReferenceModule,
// TransactionsByVendorModule,
// TransactionsByContactModule,
],
})
export class FinancialStatementsModule {}

View File

@@ -141,7 +141,7 @@ export class FinancialSheet {
* @param {string} format
* @returns
*/
protected getDateMeta(date: Date, format = 'YYYY-MM-DD') {
protected getDateMeta(date: moment.MomentInput, format = 'YYYY-MM-DD') {
return {
formattedDate: moment(date).format(format),
date: moment(date).toDate(),

View File

@@ -0,0 +1,194 @@
import { sumBy, defaultTo } from 'lodash';
import {
ITransactionsByContactsTransaction,
ITransactionsByContactsAmount,
ITransactionsByContactsFilter,
ITransactionsByContactsContact,
} from './TransactionsByContact.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { I18nService } from 'nestjs-i18n';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { TransactionsByContactRepository } from './TransactionsByContactRepository';
export class TransactionsByContact extends FinancialSheet {
public readonly filter: ITransactionsByContactsFilter;
public readonly i18n: I18nService
public readonly repository: TransactionsByContactRepository;
/**
* Customer transaction mapper.
* @param {ILedgerEntry} entry - Ledger entry.
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
*/
protected contactTransactionMapper(
entry: ILedgerEntry
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
const account = this.repository.accountsGraph.getNodeData(entry.accountId);
const currencyCode = this.baseCurrency;
return {
credit: this.getContactAmount(entry.credit, currencyCode),
debit: this.getContactAmount(entry.debit, currencyCode),
accountName: account.name,
currencyCode: this.baseCurrency,
transactionNumber: entry.transactionNumber,
transactionType: this.i18n.t(entry.referenceTypeFormatted),
date: entry.date,
createdAt: entry.createdAt,
};
}
/**
* Customer transactions mapper with running balance.
* @param {number} openingBalance
* @param {ITransactionsByContactsTransaction[]} transactions
* @returns {ITransactionsByContactsTransaction[]}
*/
protected contactTransactionRunningBalance(
openingBalance: number,
accountNormal: 'credit' | 'debit',
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[]
): any {
let _openingBalance = openingBalance;
return transactions.map(
(transaction: ITransactionsByContactsTransaction) => {
_openingBalance +=
accountNormal === 'debit'
? transaction.debit.amount
: -1 * transaction.debit.amount;
_openingBalance +=
accountNormal === 'credit'
? transaction.credit.amount
: -1 * transaction.credit.amount;
const runningBalance = this.getTotalAmountMeta(
_openingBalance,
transaction.currencyCode
);
return { ...transaction, runningBalance };
}
);
}
/**
* Retrieve the customer closing balance from the given transactions and opening balance.
* @param {number} customerTransactions
* @param {number} openingBalance
* @returns {number}
*/
protected getContactClosingBalance(
customerTransactions: ITransactionsByContactsTransaction[],
contactNormal: 'credit' | 'debit',
openingBalance: number
): number {
const closingBalance = openingBalance;
const totalCredit = sumBy(customerTransactions, 'credit.amount');
const totalDebit = sumBy(customerTransactions, 'debit.amount');
const total =
contactNormal === 'debit'
? totalDebit - totalCredit
: totalCredit - totalDebit;
return closingBalance + total;
}
/**
* Retrieve the given customer opening balance from the given customer id.
* @param {number} customerId
* @returns {number}
*/
protected getContactOpeningBalance(customerId: number): number {
const openingBalanceLedger = this.repository.ledger
.whereContactId(customerId)
.whereToDate(this.filter.fromDate);
// Retrieve the closing balance of the ledger.
const openingBalance = openingBalanceLedger.getClosingBalance();
return defaultTo(openingBalance, 0);
}
/**
* Retrieve the customer amount format meta.
* @param {number} amount
* @param {string} currencyCode
* @returns {ITransactionsByContactsAmount}
*/
protected getContactAmount(
amount: number,
currencyCode: string
): ITransactionsByContactsAmount {
return {
amount,
formattedAmount: this.formatNumber(amount, { currencyCode }),
currencyCode,
};
}
/**
* Retrieve the contact total amount format meta.
* @param {number} amount - Amount.
* @param {string} currencyCode - Currency code./
* @returns {ITransactionsByContactsAmount}
*/
protected getTotalAmountMeta(amount: number, currencyCode: string) {
return {
amount,
formattedAmount: this.formatTotalNumber(amount, { currencyCode }),
currencyCode,
};
}
/**
* Filter customer section that has no transactions.
* @param {ITransactionsByCustomersCustomer} transactionsByCustomer
* @returns {boolean}
*/
private filterContactByNoneTransaction = (
transactionsByContact: ITransactionsByContactsContact
): boolean => {
return transactionsByContact.transactions.length > 0;
};
/**
* Filters customer section has zero closing balnace.
* @param {ITransactionsByCustomersCustomer} transactionsByCustomer
* @returns {boolean}
*/
private filterContactNoneZero = (
transactionsByContact: ITransactionsByContactsContact
): boolean => {
return transactionsByContact.closingBalance.amount !== 0;
};
/**
* Filters the given customer node;
* @param {ITransactionsByContactsContact} node - Contact node.
*/
private contactNodeFilter = (node: ITransactionsByContactsContact) => {
const { noneTransactions, noneZero } = this.filter;
// Conditions pair filter detarminer.
const condsPairFilters = [
[noneTransactions, this.filterContactByNoneTransaction],
[noneZero, this.filterContactNoneZero],
];
return allPassedConditionsPass(condsPairFilters)(node);
};
/**
* Filters the given customers nodes.
* @param {ICustomerBalanceSummaryCustomer[]} nodes
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
protected contactsFilter = (
nodes: ITransactionsByContactsContact[]
): ITransactionsByContactsContact[] => {
return nodes.filter(this.contactNodeFilter);
};
}

View File

@@ -0,0 +1,34 @@
import { INumberFormatQuery } from "../../types/Report.types";
export interface ITransactionsByContactsAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ITransactionsByContactsTransaction {
date: string|Date,
credit: ITransactionsByContactsAmount;
debit: ITransactionsByContactsAmount;
accountName: string,
runningBalance: ITransactionsByContactsAmount;
currencyCode: string;
transactionType: string;
transactionNumber: string;
createdAt: string|Date,
};
export interface ITransactionsByContactsContact {
openingBalance: ITransactionsByContactsAmount,
closingBalance: ITransactionsByContactsAmount,
transactions: ITransactionsByContactsTransaction[],
}
export interface ITransactionsByContactsFilter {
fromDate: Date|string;
toDate: Date|string;
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
noneZero: boolean;
}

View File

@@ -0,0 +1,27 @@
import { Ledger } from '@/modules/Ledger/Ledger';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
export class TransactionsByContactRepository {
/**
* Base currency.
* @param {string} baseCurrency
*/
public baseCurrency: string;
/**
* Report data.
*/
public accountsGraph: any;
/**
* Report data.
* @param {Ledger} ledger
*/
public ledger: Ledger;
/**
* Opening balance entries.
* @param {ILedgerEntry[]} openingBalanceEntries
*/
public openingBalanceEntries: ILedgerEntry[];
}

View File

@@ -0,0 +1,91 @@
import moment from 'moment';
import * as R from 'ramda';
import { ITransactionsByContactsContact } from './TransactionsByContact.types';
import { ITableRow } from '../../types/Table.types';
import { tableMapper, tableRowMapper } from '../../utils/Table.utils';
import { I18nService } from 'nestjs-i18n';
enum ROW_TYPE {
OPENING_BALANCE = 'OPENING_BALANCE',
CLOSING_BALANCE = 'CLOSING_BALANCE',
TRANSACTION = 'TRANSACTION',
CUSTOMER = 'CUSTOMER',
}
export class TransactionsByContactsTableRows {
public i18n: I18nService;
public dateAccessor = (value): string => {
return moment(value.date).format('YYYY MMM DD');
};
/**
* Retrieve the table rows of contact transactions.
* @param {ITransactionsByCustomersCustomer} contact
* @returns {ITableRow[]}
*/
public contactTransactions = (
contact: ITransactionsByContactsContact,
): ITableRow[] => {
const columns = [
{ key: 'date', accessor: this.dateAccessor },
{ key: 'account', accessor: 'accountName' },
{ key: 'transactionType', accessor: 'transactionType' },
{ key: 'transactionNumber', accessor: 'transactionNumber' },
{ key: 'credit', accessor: 'credit.formattedAmount' },
{ key: 'debit', accessor: 'debit.formattedAmount' },
{ key: 'runningBalance', accessor: 'runningBalance.formattedAmount' },
];
return tableMapper(contact.transactions, columns, {
rowTypes: [ROW_TYPE.TRANSACTION],
});
};
/**
* Retrieve the table row of contact opening balance.
* @param {ITransactionsByCustomersCustomer} contact
* @returns {ITableRow}
*/
public contactOpeningBalance = (
contact: ITransactionsByContactsContact,
): ITableRow => {
const columns = [
{
key: 'openingBalanceLabel',
value: this.i18n.t('Opening balance') as string,
},
...R.repeat({ key: 'empty', value: '' }, 5),
{
key: 'openingBalanceValue',
accessor: 'openingBalance.formattedAmount',
},
];
return tableRowMapper(contact, columns, {
rowTypes: [ROW_TYPE.OPENING_BALANCE],
});
};
/**
* Retrieve the table row of contact closing balance.
* @param {ITransactionsByCustomersCustomer} contact -
* @returns {ITableRow}
*/
public contactClosingBalance = (
contact: ITransactionsByContactsContact,
): ITableRow => {
const columns = [
{
key: 'closingBalanceLabel',
value: this.i18n.t('Closing balance') as string,
},
...R.repeat({ key: 'empty', value: '' }, 5),
{
key: 'closingBalanceValue',
accessor: 'closingBalance.formattedAmount',
},
];
return tableRowMapper(contact, columns, {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
});
};
}

View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TransactionsByCustomersExportInjectable } from './TransactionsByCustomersExportInjectable';
import { TransactionsByCustomersPdf } from './TransactionsByCustomersPdf';
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
providers: [
TransactionsByCustomersRepository,
TransactionsByCustomersTableInjectable,
TransactionsByCustomersExportInjectable,
TransactionsByCustomersSheet,
TransactionsByCustomersPdf,
TransactionsByCustomersMeta,
TenancyContext
],
controllers: [],
})
export class TransactionsByCustomerModule {}

View File

@@ -0,0 +1,52 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
import {
ITransactionsByContactsAmount,
ITransactionsByContactsTransaction,
ITransactionsByContactsFilter,
} from '../TransactionsByContact/TransactionsByContact.types';
export interface ITransactionsByCustomersAmount
extends ITransactionsByContactsAmount {}
export interface ITransactionsByCustomersTransaction
extends ITransactionsByContactsTransaction {}
export interface ITransactionsByCustomersCustomer {
customerName: string;
openingBalance: ITransactionsByCustomersAmount;
closingBalance: ITransactionsByCustomersAmount;
transactions: ITransactionsByCustomersTransaction[];
}
export interface ITransactionsByCustomersFilter
extends ITransactionsByContactsFilter {
customersIds: number[];
}
export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[];
export interface ITransactionsByCustomersStatement {
data: ITransactionsByCustomersData;
query: ITransactionsByCustomersFilter;
meta: ITransactionsByCustomersMeta;
}
export interface ITransactionsByCustomersTable extends IFinancialTable {
query: ITransactionsByCustomersFilter;
meta: ITransactionsByCustomersMeta;
}
export interface ITransactionsByCustomersService {
transactionsByCustomers(
tenantId: number,
filter: ITransactionsByCustomersFilter
): Promise<ITransactionsByCustomersStatement>;
}
export interface ITransactionsByCustomersMeta
extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}

View File

@@ -0,0 +1,136 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import { I18nService } from 'nestjs-i18n';
import { ModelObject } from 'objection';
import {
ITransactionsByCustomersTransaction,
ITransactionsByCustomersFilter,
ITransactionsByCustomersCustomer,
ITransactionsByCustomersData,
} from './TransactionsByCustomer.types';
import { TransactionsByContact } from '../TransactionsByContact/TransactionsByContact';
import { Customer } from '@/modules/Customers/models/Customer';
import { INumberFormatQuery } from '../../types/Report.types';
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
const CUSTOMER_NORMAL = 'debit';
export class TransactionsByCustomers extends TransactionsByContact {
readonly filter: ITransactionsByCustomersFilter;
readonly numberFormat: INumberFormatQuery;
readonly repository: TransactionsByCustomersRepository;
readonly i18n: I18nService;
/**
* Constructor method.
* @param {ICustomer} customers
* @param {Map<number, IAccountTransaction[]>} transactionsLedger
* @param {string} baseCurrency
*/
constructor(
filter: ITransactionsByCustomersFilter,
transactionsByCustomersRepository: TransactionsByCustomersRepository,
i18n: I18nService
) {
super();
this.filter = filter;
this.repository = transactionsByCustomersRepository;
this.numberFormat = this.filter.numberFormat;
this.i18n = i18n;
}
/**
* Retrieve the customer transactions from the given customer id and opening balance.
* @param {number} customerId - Customer id.
* @param {number} openingBalance - Opening balance amount.
* @returns {ITransactionsByCustomersTransaction[]}
*/
private customerTransactions(
customerId: number,
openingBalance: number
): ITransactionsByCustomersTransaction[] {
const ledger = this.repository.ledger
.whereContactId(customerId)
.whereFromDate(this.filter.fromDate)
.whereToDate(this.filter.toDate);
const ledgerEntries = ledger.getEntries();
return R.compose(
R.curry(this.contactTransactionRunningBalance)(openingBalance, 'debit'),
R.map(this.contactTransactionMapper.bind(this))
).bind(this)(ledgerEntries);
}
/**
* Customer section mapper.
* @param {ModelObject<Customer>} customer
* @returns {ITransactionsByCustomersCustomer}
*/
private customerMapper(
customer: ModelObject<Customer>
): ITransactionsByCustomersCustomer {
const openingBalance = this.getContactOpeningBalance(customer.id);
const transactions = this.customerTransactions(customer.id, openingBalance);
const closingBalance = this.getCustomerClosingBalance(
transactions,
openingBalance
);
const currencyCode = this.baseCurrency;
return {
customerName: customer.displayName,
openingBalance: this.getTotalAmountMeta(openingBalance, currencyCode),
closingBalance: this.getTotalAmountMeta(closingBalance, currencyCode),
transactions,
};
}
/**
* Retrieve the vendor closing balance from the given customer transactions.
* @param {ITransactionsByContactsTransaction[]} customerTransactions
* @param {number} openingBalance
* @returns
*/
private getCustomerClosingBalance(
customerTransactions: ITransactionsByCustomersTransaction[],
openingBalance: number
): number {
return this.getContactClosingBalance(
customerTransactions,
CUSTOMER_NORMAL,
openingBalance
);
}
/**
* 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 {ICustomer[]} customers
* @returns {ITransactionsByCustomersCustomer[]}
*/
private customersMapper(
customers: ModelObject<Customer>[]
): ITransactionsByCustomersCustomer[] {
return R.compose(
R.when(this.isCustomersPostFilter, this.contactsFilter),
R.map(this.customerMapper.bind(this))
).bind(this)(customers);
}
/**
* Retrieve the report data.
* @returns {ITransactionsByCustomersData}
*/
public reportData(): ITransactionsByCustomersData {
return this.customersMapper(this.repository.customers);
}
}

View File

@@ -0,0 +1,66 @@
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement,
} from './TransactionsByCustomer.types';
import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
import { TransactionsByCustomersExportInjectable } from './TransactionsByCustomersExportInjectable';
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
import { TransactionsByCustomersPdf } from './TransactionsByCustomersPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByCustomerApplication {
constructor(
private readonly transactionsByCustomersTable: TransactionsByCustomersTableInjectable,
private readonly transactionsByCustomersExport: TransactionsByCustomersExportInjectable,
private readonly transactionsByCustomersSheet: TransactionsByCustomersSheet,
private readonly transactionsByCustomersPdf: TransactionsByCustomersPdf,
) {}
/**
* Retrieves the transactions by customers sheet in json format.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {Promise<ITransactionsByCustomersStatement>}
*/
public sheet(
query: ITransactionsByCustomersFilter,
): Promise<ITransactionsByCustomersStatement> {
return this.transactionsByCustomersSheet.transactionsByCustomers(query);
}
/**
* Retrieves the transactions by vendors sheet in table format.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {Promise<ITransactionsByCustomersTable>}
*/
public table(query: ITransactionsByCustomersFilter) {
return this.transactionsByCustomersTable.table(query);
}
/**
* Retrieves the transactions by vendors sheet in CSV format.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {Promise<string>}
*/
public csv(query: ITransactionsByCustomersFilter): Promise<string> {
return this.transactionsByCustomersExport.csv(query);
}
/**
* Retrieves the transactions by vendors sheet in XLSX format.
* @param {number} tenantId
* @param {ITransactionsByCustomersFilter} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: ITransactionsByCustomersFilter): Promise<Buffer> {
return this.transactionsByCustomersExport.xlsx(query);
}
/**
* Retrieves the transactions by vendors sheet in PDF format.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {Promise<Buffer>}
*/
public pdf(query: ITransactionsByCustomersFilter): Promise<Buffer> {
return this.transactionsByCustomersPdf.pdf(query);
}
}

View File

@@ -0,0 +1,39 @@
import { TableSheet } from '../../common/TableSheet';
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByCustomersExportInjectable {
constructor(
private readonly transactionsByCustomerTable: TransactionsByCustomersTableInjectable,
) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ITransactionsByCustomersFilter} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ITransactionsByCustomersFilter): Promise<Buffer> {
const table = await this.transactionsByCustomerTable.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 {ITransactionsByCustomersFilter} query
* @returns {Promise<string>}
*/
public async csv(query: ITransactionsByCustomersFilter): Promise<string> {
const table = await this.transactionsByCustomerTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,35 @@
import moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersMeta,
} from './TransactionsByCustomer.types';
@Injectable()
export class TransactionsByCustomersMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the transactions by customers meta.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {ITransactionsByCustomersMeta}
*/
public async meta(
query: ITransactionsByCustomersFilter,
): Promise<ITransactionsByCustomersMeta> {
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: 'Transactions By Customers',
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,25 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
export class TransactionsByCustomersPdf {
constructor(
private readonly transactionsByCustomersTable: TransactionsByCustomersTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Retrieves the transactions by customers in PDF format.
* @param {ITransactionsByCustomersFilter} query - Transactions by customers filter.
* @returns {Promise<Buffer>}
*/
public async pdf(query: ITransactionsByCustomersFilter): Promise<Buffer> {
const table = await this.transactionsByCustomersTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
);
}
}

View File

@@ -0,0 +1,251 @@
import * as R from 'ramda';
import { ACCOUNT_TYPE } from '@/constants/accounts';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Customer } from '@/modules/Customers/models/Customer';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { isEmpty, map } from 'lodash';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types';
import { ModelObject } from 'objection';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository';
@Injectable({ scope: Scope.TRANSIENT })
export class TransactionsByCustomersRepository extends TransactionsByContactRepository {
@Inject(Customer.name)
private readonly customerModel: typeof Customer;
@Inject(Account.name)
private readonly accountModel: typeof Account;
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction;
@Inject(AccountRepository)
private readonly accountRepository: AccountRepository;
@Inject(TenancyContext)
private readonly tenancyContext: TenancyContext;
/**
* Customers models.
* @param {ModelObject<Customer>[]} customers
*/
public customers: ModelObject<Customer>[];
/**
* Report filter.
* @param {ITransactionsByCustomersFilter} filter
*/
public filter: ITransactionsByCustomersFilter;
/**
* Customers periods entries.
* @param {ILedgerEntry[]} customersPeriodsEntries
*/
public customersPeriodsEntries: ILedgerEntry[];
/**
* Initialize the report data.
* @param {ITransactionsByCustomersFilter} filter
*/
public async asyncInit(filter: ITransactionsByCustomersFilter) {
this.filter = filter;
await this.initAccountsGraph();
await this.initCustomers();
await this.initOpeningBalanceEntries();
await this.initCustomersPeriodsEntries();
await this.initLedger();
await this.initBaseCurrency();
}
/**
* Initialize the accounts graph.
*/
async initAccountsGraph() {
const accountsGraph = await this.accountRepository.getDependencyGraph();
this.accountsGraph = accountsGraph;
}
/**
* Initialize the customers.
*/
async initCustomers() {
// Retrieve the report customers.
const customers = await this.getCustomers(this.filter.customersIds);
this.customers = customers;
}
/**
* Initialize the opening balance entries.
*/
async initOpeningBalanceEntries() {
const openingBalanceDate = moment(this.filter.fromDate)
.subtract(1, 'days')
.toDate();
// Retrieve all ledger transactions of the opening balance of.
const openingBalanceEntries =
await this.getCustomersOpeningBalanceEntries(openingBalanceDate);
this.openingBalanceEntries = openingBalanceEntries;
}
/**
* Initialize the customers periods entries.
*/
async initCustomersPeriodsEntries() {
// Retrieve all ledger transactions between opeing and closing period.
const customersTransactions = await this.getCustomersPeriodsEntries(
this.filter.fromDate,
this.filter.toDate,
);
this.customersPeriodsEntries = customersTransactions;
}
/**
* Initialize the ledger.
*/
async initLedger() {
// Concats the opening balance and period customer ledger transactions.
const journalTransactions = [
...this.openingBalanceEntries,
...this.customersPeriodsEntries,
];
this.ledger = new Ledger(journalTransactions);
}
async initBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
}
/**
* Retrieve the customers opening balance ledger entries.
* @param {number} tenantId
* @param {Date} openingDate
* @param {number[]} customersIds
* @returns {Promise<ILedgerEntry[]>}
*/
private async getCustomersOpeningBalanceEntries(
openingDate: Date,
customersIds?: number[],
): Promise<ILedgerEntry[]> {
const openingTransactions =
await this.getCustomersOpeningBalanceTransactions(
openingDate,
customersIds,
);
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'debit')),
)(openingTransactions);
}
/**
* Retrieve the customers periods ledger entries.
* @param {number} tenantId
* @param {Date} fromDate
* @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/
private async getCustomersPeriodsEntries(
fromDate: Date | string,
toDate: Date | string,
): Promise<ILedgerEntry[]> {
const transactions =
await this.getCustomersPeriodTransactions(
fromDate,
toDate,
);
return R.compose(
R.map(R.assoc('accountNormal', 'debit')),
R.map((trans) => ({
...trans,
referenceTypeFormatted: trans.referenceTypeFormatted,
})),
)(transactions);
}
/**
* Retrieve the report customers.
* @param {number[]} customersIds - Customers ids.
* @returns {Promise<ICustomer[]>}
*/
public async getCustomers(customersIds?: number[]) {
return this.customerModel.query().onBuild((q) => {
q.orderBy('displayName');
if (!isEmpty(customersIds)) {
q.whereIn('id', customersIds);
}
});
}
/**
* Retrieve the accounts receivable.
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(): Promise<Account[]> {
const accounts = await this.accountModel
.query()
.where('accountType', ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} openingDate - Opening date.
* @param {number} customersIds - Customers ids.
* @returns {Promise<IAccountTransaction[]>}
*/
public async getCustomersOpeningBalanceTransactions(
openingDate: Date,
customersIds?: number[],
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getReceivableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');
const openingTransactions = await this.accountTransactionModel
.query()
.modify(
'contactsOpeningBalance',
openingDate,
receivableAccountsIds,
customersIds,
);
return openingTransactions;
}
/**
* Retrieve the customers periods transactions.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {Promise<IAccountTransaction[]>}
*/
public async getCustomersPeriodTransactions(
fromDate: Date,
toDate: Date,
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getReceivableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await this.accountTransactionModel
.query()
.onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -0,0 +1,63 @@
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement,
} from './TransactionsByCustomer.types';
import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository';
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
import { getTransactionsByCustomerDefaultQuery } from './utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TransactionsByCustomers } from './TransactionsByCustomers';
import { events } from '@/common/events/events';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByCustomersSheet {
constructor(
private readonly transactionsByCustomersMeta: TransactionsByCustomersMeta,
private readonly transactionsByCustomersRepository: TransactionsByCustomersRepository,
private readonly eventPublisher: EventEmitter2,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Retrieve transactions by by the customers.
* @param {number} tenantId
* @param {ITransactionsByCustomersFilter} query
* @return {Promise<ITransactionsByCustomersStatement>}
*/
public async transactionsByCustomers(
query: ITransactionsByCustomersFilter,
): Promise<ITransactionsByCustomersStatement> {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
const filter = {
...getTransactionsByCustomerDefaultQuery(),
...query,
};
await this.transactionsByCustomersRepository.asyncInit(filter);
// Transactions by customers data mapper.
const reportInstance = new TransactionsByCustomers(
filter,
this.transactionsByCustomersRepository,
this.i18n,
);
const meta = await this.transactionsByCustomersMeta.meta(filter);
// Triggers `onCustomerTransactionsViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerTransactionsViewed,
{
query,
},
);
return {
data: reportInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,92 @@
import * as R from 'ramda';
import { ITransactionsByCustomersCustomer } from './TransactionsByCustomer.types';
import { ITableRow, ITableColumn } from '../../types/Table.types';
import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows';
import { tableRowMapper } from '../../utils/Table.utils';
enum ROW_TYPE {
OPENING_BALANCE = 'OPENING_BALANCE',
CLOSING_BALANCE = 'CLOSING_BALANCE',
TRANSACTION = 'TRANSACTION',
CUSTOMER = 'CUSTOMER',
}
export class TransactionsByCustomersTable extends TransactionsByContactsTableRows {
private customersTransactions: ITransactionsByCustomersCustomer[];
/**
* Constructor method.
* @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions.
*/
constructor(customersTransactions: ITransactionsByCustomersCustomer[], i18n) {
super();
this.customersTransactions = customersTransactions;
this.i18n = i18n;
}
/**
* Retrieve the table row of customer details.
* @param {ITransactionsByCustomersCustomer} customer -
* @returns {ITableRow[]}
*/
private customerDetails = (customer: ITransactionsByCustomersCustomer) => {
const columns = [
{ key: 'customerName', accessor: 'customerName' },
...R.repeat({ key: 'empty', value: '' }, 5),
{
key: 'closingBalanceValue',
accessor: 'closingBalance.formattedAmount',
},
];
return {
...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }),
children: R.pipe(
R.when(
R.always(customer.transactions.length > 0),
R.pipe(
R.concat(this.contactTransactions(customer)),
R.prepend(this.contactOpeningBalance(customer)),
),
),
R.append(this.contactClosingBalance(customer)),
)([]),
};
};
/**
* Retrieve the table rows of the customer section.
* @param {ITransactionsByCustomersCustomer} customer
* @returns {ITableRow[]}
*/
private customerRowsMapper = (customer: ITransactionsByCustomersCustomer) => {
return R.pipe(this.customerDetails)(customer);
};
/**
* Retrieve the table rows of transactions by customers report.
* @param {ITransactionsByCustomersCustomer[]} customers
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.map(this.customerRowsMapper.bind(this))(
this.customersTransactions,
);
};
/**
* Retrieve the table columns of transactions by customers report.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return [
{ key: 'customer_name', label: 'Customer name' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'ref_type', label: 'Reference Type' },
{ key: 'transaction_type', label: 'Transaction Type' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
{ key: 'running_balance', label: 'Running Balance' },
];
};
}

View File

@@ -0,0 +1,42 @@
import {
ITransactionsByCustomersFilter,
ITransactionsByCustomersTable,
} from './TransactionsByCustomer.types';
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
import { TransactionsByCustomersTable } from './TransactionsByCustomersTable';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class TransactionsByCustomersTableInjectable {
constructor(
private readonly transactionsByCustomerService: TransactionsByCustomersSheet,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the transactions by customers sheet in table format.
* @param {number} tenantId
* @param {ITransactionsByCustomersFilter} filter
* @returns {Promise<ITransactionsByCustomersFilter>}
*/
public async table(
filter: ITransactionsByCustomersFilter,
): Promise<ITransactionsByCustomersTable> {
const customersTransactions =
await this.transactionsByCustomerService.transactionsByCustomers(filter);
const table = new TransactionsByCustomersTable(
customersTransactions.data,
this.i18n,
);
return {
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query: customersTransactions.query,
meta: customersTransactions.meta,
};
}
}

View File

@@ -0,0 +1,22 @@
export const getTransactionsByCustomerDefaultQuery = () => {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
comparison: {
percentageOfColumn: true,
},
noneZero: false,
noneTransactions: true,
customersIds: [],
};
};

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository';
import { TransactionsByReferenceService } from './TransactionsByReference.service';
import { TransactionsByReferenceController } from './TransactionsByReference.controller';
@Module({
providers: [
TransactionsByReferenceRepository,
TransactionsByReferenceApplication,
TransactionsByReferenceService,
],
controllers: [TransactionsByReferenceController],
})
export class TransactionsByReferenceModule {}

View File

@@ -0,0 +1,19 @@
import { Controller, Get, Query } from '@nestjs/common';
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
@Controller('reports/transactions-by-reference')
export class TransactionsByReferenceController {
constructor(
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,
) {}
@Get()
async getTransactionsByReference(
@Query() query: ITransactionsByReferenceQuery,
) {
const data = await this.transactionsByReferenceApp.getTransactions(query);
return data;
}
}

View File

@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import {
ITransactionsByReferencePojo,
ITransactionsByReferenceQuery,
} from './TransactionsByReference.types';
import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository';
import { TransactionsByReference } from './TransactionsByReferenceReport';
import { getTransactionsByReferenceQuery } from './_utils';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class TransactionsByReferenceService {
constructor(
private readonly repository: TransactionsByReferenceRepository,
private readonly tenancyContext: TenancyContext
) {}
/**
* Retrieve accounts transactions by given reference id and type.
* @param {ITransactionsByReferenceQuery} filter - Transactions by reference query.
* @returns {Promise<ITransactionsByReferencePojo>}
*/
public async getTransactionsByReference(
query: ITransactionsByReferenceQuery
): Promise<ITransactionsByReferencePojo> {
const filter = {
...getTransactionsByReferenceQuery(),
...query,
};
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
// Retrieve the accounts transactions of the given reference.
const transactions = await this.repository.getTransactions(
Number(filter.referenceId),
filter.referenceType
);
// Transactions by reference report.
const report = new TransactionsByReference(
transactions,
filter,
tenantMetadata.baseCurrency
);
return {
transactions: report.reportData(),
};
}
}

View File

@@ -0,0 +1,34 @@
export interface ITransactionsByReferenceQuery {
referenceType: string;
referenceId: string;
}
export interface ITransactionsByReferenceAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ITransactionsByReferenceTransaction {
credit: ITransactionsByReferenceAmount;
debit: ITransactionsByReferenceAmount;
contactType: string;
formattedContactType: string;
contactId: number;
referenceType: string;
formattedReferenceType: string;
referenceId: number;
accountName: string;
accountCode: string;
accountId: number;
}
export interface ITransactionsByReferencePojo {
transactions: ITransactionsByReferenceTransaction[];
}

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { TransactionsByReferenceService } from './TransactionsByReference.service';
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
@Injectable()
export class TransactionsByReferenceApplication {
constructor(
private readonly transactionsByReferenceService: TransactionsByReferenceService,
) {}
/**
* Retrieve accounts transactions by given reference id and type.
* @param {ITransactionsByReferenceQuery} query - Transactions by reference query.
* @returns {Promise<ITransactionsByReferencePojo>}
*/
public async getTransactions(query: ITransactionsByReferenceQuery) {
return this.transactionsByReferenceService.getTransactionsByReference(
query,
);
}
}

View File

@@ -0,0 +1,81 @@
import {
ITransactionsByReferenceQuery,
ITransactionsByReferenceTransaction,
} from './TransactionsByReference.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ModelObject } from 'objection';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { INumberFormatQuery } from '../../types/Report.types';
export class TransactionsByReference extends FinancialSheet {
readonly transactions: ModelObject<AccountTransaction>[];
readonly query: ITransactionsByReferenceQuery;
readonly baseCurrency: string;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
* @param {ModelObject<AccountTransaction>[]} transactions
* @param {ITransactionsByReferenceQuery} query
* @param {string} baseCurrency
*/
constructor(
transactions: ModelObject<AccountTransaction>[],
query: ITransactionsByReferenceQuery,
baseCurrency: string
) {
super();
this.transactions = transactions;
this.query = query;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
}
/**
* Mappes the given account transaction to report transaction.
* @param {IAccountTransaction} transaction
* @returns {ITransactionsByReferenceTransaction}
*/
private transactionMapper = (
transaction: ModelObject<AccountTransaction>
): ITransactionsByReferenceTransaction => {
return {
date: this.getDateMeta(transaction.date),
credit: this.getAmountMeta(transaction.credit, { money: false }),
debit: this.getAmountMeta(transaction.debit, { money: false }),
referenceTypeFormatted: transaction.referenceTypeFormatted,
referenceType: transaction.referenceType,
referenceId: transaction.referenceId,
contactId: transaction.contactId,
contactType: transaction.contactType,
contactTypeFormatted: transaction.contactType,
accountName: transaction.account.name,
accountCode: transaction.account.code,
accountId: transaction.accountId,
};
};
/**
* Mappes the given accounts transactions to report transactions.
* @param {IAccountTransaction} transaction
* @returns {ITransactionsByReferenceTransaction}
*/
private transactionsMapper = (
transactions: ModelObject<AccountTransaction>[]
): ITransactionsByReferenceTransaction[] => {
return transactions.map(this.transactionMapper);
};
/**
* Retrieve the report data.
* @returns {ITransactionsByReferenceTransaction}
*/
public reportData(): ITransactionsByReferenceTransaction[] {
return this.transactionsMapper(this.transactions);
}
}

View File

@@ -0,0 +1,28 @@
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Inject, Injectable } from '@nestjs/common';
import { ModelObject } from 'objection';
@Injectable()
export class TransactionsByReferenceRepository {
constructor(
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction,
) {}
/**
* Retrieve the accounts transactions of the givne reference id and type.
* @param {number} tenantId -
* @param {number} referenceId - Reference id.
* @param {string} referenceType - Reference type.
* @return {Promise<IAccountTransaction[]>}
*/
public async getTransactions(
referenceId: number,
referenceType: string,
): Promise<Array<ModelObject<AccountTransaction>>> {
return this.accountTransactionModel.query()
.where('reference_id', referenceId)
.where('reference_type', referenceType)
.withGraphFetched('account');
}
}

View File

@@ -0,0 +1,12 @@
export const getTransactionsByReferenceQuery = () => ({
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
})

View File

@@ -0,0 +1,59 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { TransactionsByVendorApplication } from './TransactionsByVendorApplication';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('/reports/transactions-by-vendors')
@PublicRoute()
export class TransactionsByVendorController {
constructor(
private readonly transactionsByVendorsApp: TransactionsByVendorApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get transactions by vendor' })
@ApiResponse({ status: 200, description: 'Transactions by vendor' })
async transactionsByVendor(
@Query() filter: ITransactionsByVendorsFilter,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the xlsx format.
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.transactionsByVendorsApp.xlsx(filter);
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
res.setHeader('Content-Disposition', 'attachment; filename=report.xlsx');
return res.send(buffer);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.transactionsByVendorsApp.csv(filter);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=report.csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.transactionsByVendorsApp.table(filter);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.transactionsByVendorsApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.transactionsByVendorsApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { TransactionsByVendorController } from './TransactionsByVendor.controller';
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
import { TransactionsByVendorMeta } from './TransactionsByVendorMeta';
import { TransactionsByVendorRepository } from './TransactionsByVendorRepository';
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExportInjectable';
import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
import { TransactionsByVendorApplication } from './TransactionsByVendorApplication';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
controllers: [TransactionsByVendorController,],
providers: [
TransactionsByVendorsInjectable,
TransactionsByVendorRepository,
TransactionsByVendorMeta,
TransactionsByVendorTableInjectable,
TransactionsByVendorExportInjectable,
TransactionsByVendorsPdf,
TransactionsByVendorApplication,
TenancyContext
],
exports: [TransactionsByVendorApplication],
})
export class TransactionsByVendorModule {}

View File

@@ -0,0 +1,136 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection';
import {
ITransactionsByVendorsFilter,
ITransactionsByVendorsTransaction,
ITransactionsByVendorsVendor,
ITransactionsByVendorsData,
} from './TransactionsByVendor.types';
import { TransactionsByContact } from '../TransactionsByContact/TransactionsByContact';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { INumberFormatQuery } from '../../types/Report.types';
import { I18nService } from 'nestjs-i18n';
import { TransactionsByVendorRepository } from './TransactionsByVendorRepository';
const VENDOR_NORMAL = 'credit';
export class TransactionsByVendor extends TransactionsByContact {
public readonly repository: TransactionsByVendorRepository;
public readonly filter: ITransactionsByVendorsFilter;
public readonly numberFormat: INumberFormatQuery;
public readonly i18n: I18nService;
/**
* Constructor method.
* @param {TransactionsByVendorRepository} transactionsByVendorRepository - Transactions by vendor repository.
* @param {ITransactionsByVendorsFilter} filter - Transactions by vendors filter.
* @param {I18nService} i18n - Internationalization service.
*/
constructor(
transactionsByVendorRepository: TransactionsByVendorRepository,
filter: ITransactionsByVendorsFilter,
i18n: I18nService,
) {
super();
this.repository = transactionsByVendorRepository;
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
this.i18n = i18n;
}
/**
* Retrieve the vendor transactions from the given vendor id and opening balance.
* @param {number} vendorId - Vendor id.
* @param {number} openingBalance - Opening balance amount.
* @returns {ITransactionsByVendorsTransaction[]}
*/
private vendorTransactions(
vendorId: number,
openingBalance: number,
): ITransactionsByVendorsTransaction[] {
const openingBalanceLedger = this.repository.journal
.whereContactId(vendorId)
.whereFromDate(this.filter.fromDate)
.whereToDate(this.filter.toDate);
const openingEntries = openingBalanceLedger.getEntries();
return R.compose(
R.curry(this.contactTransactionRunningBalance)(openingBalance, 'credit'),
R.map(this.contactTransactionMapper.bind(this)),
).bind(this)(openingEntries);
}
/**
* Vendor section mapper.
* @param {IVendor} vendor
* @returns {ITransactionsByVendorsVendor}
*/
private vendorMapper(
vendor: ModelObject<Vendor>,
): ITransactionsByVendorsVendor {
const openingBalance = this.getContactOpeningBalance(vendor.id);
const transactions = this.vendorTransactions(vendor.id, openingBalance);
const closingBalance = this.getVendorClosingBalance(
transactions,
openingBalance,
);
const currencyCode = this.baseCurrency;
return {
vendorName: vendor.displayName,
openingBalance: this.getTotalAmountMeta(openingBalance, currencyCode),
closingBalance: this.getTotalAmountMeta(closingBalance, currencyCode),
transactions,
};
}
/**
* Retrieve the vendor closing balance from the given customer transactions.
* @param {ITransactionsByContactsTransaction[]} customerTransactions
* @param {number} openingBalance
* @returns
*/
private getVendorClosingBalance(
vendorTransactions: ITransactionsByVendorsTransaction[],
openingBalance: number,
) {
return this.getContactClosingBalance(
vendorTransactions,
VENDOR_NORMAL,
openingBalance,
);
}
/**
* Detarmines whether the vendors post filter is active.
* @returns {boolean}
*/
private isVendorsPostFilter = (): boolean => {
return isEmpty(this.filter.vendorsIds);
};
/**
* Retrieve the vendors sections of the report.
* @param {IVendor[]} vendors
* @returns {ITransactionsByVendorsVendor[]}
*/
private vendorsMapper(
vendors: ModelObject<Vendor>[],
): ITransactionsByVendorsVendor[] {
return R.compose(
R.when(this.isVendorsPostFilter, this.contactsFilter),
R.map(this.vendorMapper.bind(this)),
).bind(this)(vendors);
}
/**
* Retrieve the report data.
* @returns {ITransactionsByVendorsData}
*/
public reportData(): ITransactionsByVendorsData {
return this.vendorsMapper(this.repository.vendors);
}
}

View File

@@ -0,0 +1,52 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
import {
ITransactionsByContactsAmount,
ITransactionsByContactsTransaction,
ITransactionsByContactsFilter,
} from '../TransactionsByContact/TransactionsByContact.types';
export interface ITransactionsByVendorsAmount
extends ITransactionsByContactsAmount {}
export interface ITransactionsByVendorsTransaction
extends ITransactionsByContactsTransaction {}
export interface ITransactionsByVendorsVendor {
vendorName: string;
openingBalance: ITransactionsByVendorsAmount;
closingBalance: ITransactionsByVendorsAmount;
transactions: ITransactionsByVendorsTransaction[];
}
export interface ITransactionsByVendorsFilter
extends ITransactionsByContactsFilter {
vendorsIds: number[];
}
export type ITransactionsByVendorsData = ITransactionsByVendorsVendor[];
export interface ITransactionsByVendorsStatement {
data: ITransactionsByVendorsData;
query: ITransactionsByVendorsFilter;
meta: ITransactionsByVendorMeta;
}
export interface ITransactionsByVendorsService {
transactionsByVendors(
tenantId: number,
filter: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement>;
}
export interface ITransactionsByVendorTable extends IFinancialTable {
query: ITransactionsByVendorsFilter;
meta: ITransactionsByVendorMeta;
}
export interface ITransactionsByVendorMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}

View File

@@ -0,0 +1,77 @@
import {
ITransactionsByVendorTable,
ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement,
} from './TransactionsByVendor.types';
import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExportInjectable';
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransactionsByVendorApplication {
constructor(
private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable,
private readonly transactionsByVendorExport: TransactionsByVendorExportInjectable,
private readonly transactionsByVendorSheet: TransactionsByVendorsInjectable,
private readonly transactionsByVendorPdf: TransactionsByVendorsPdf,
) {}
/**
* Retrieves the transactions by vendor in sheet format.
* @param {ITransactionsByVendorsFilter} query - The filter query.
* @returns {Promise<ITransactionsByVendorsStatement>}
*/
public sheet(
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement> {
return this.transactionsByVendorSheet.transactionsByVendors(
query
);
}
/**
* Retrieves the transactions by vendor in table format.
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<ITransactionsByVendorTable>}
*/
public table(
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorTable> {
return this.transactionsByVendorTable.table(query);
}
/**
* Retrieves the transactions by vendor in CSV format.
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<string>}
*/
public csv(
query: ITransactionsByVendorsFilter
): Promise<string> {
return this.transactionsByVendorExport.csv(query);
}
/**
* Retrieves the transactions by vendor in XLSX format.
* @param {number} tenantId
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<Buffer>}
*/
public xlsx(
query: ITransactionsByVendorsFilter
): Promise<Buffer> {
return this.transactionsByVendorExport.xlsx(query);
}
/**
* Retrieves the transactions by vendor in PDF format.
* @param {number} tenantId
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<Buffer>}
*/
public pdf(query: ITransactionsByVendorsFilter) {
return this.transactionsByVendorPdf.pdf(query);
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class TransactionsByVendorExportInjectable {
constructor(
private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable,
) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ITransactionsByVendorsFilter} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
query: ITransactionsByVendorsFilter
): Promise<Buffer> {
const table = await this.transactionsByVendorTable.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 {ICashFlowStatementQuery} query
* @returns {Promise<string>}
*/
public async csv(
query: ITransactionsByVendorsFilter
): Promise<string> {
const table = await this.transactionsByVendorTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement,
} from './TransactionsByVendor.types';
import { TransactionsByVendor } from './TransactionsByVendor';
import { TransactionsByVendorRepository } from './TransactionsByVendorRepository';
import { TransactionsByVendorMeta } from './TransactionsByVendorMeta';
import { getTransactionsByVendorDefaultQuery } from './utils';
import { events } from '@/common/events/events';
@Injectable()
export class TransactionsByVendorsInjectable {
constructor(
private readonly transactionsByVendorRepository: TransactionsByVendorRepository,
private readonly transactionsByVendorMeta: TransactionsByVendorMeta,
private readonly eventPublisher: EventEmitter2,
private readonly i18n: I18nService,
) {}
/**
* Retrieve transactions by by the customers.
* @param {ITransactionsByVendorsFilter} query - Transactions by vendors filter.
* @return {Promise<ITransactionsByVendorsStatement>}
*/
public async transactionsByVendors(
query: ITransactionsByVendorsFilter,
): Promise<ITransactionsByVendorsStatement> {
const filter = { ...getTransactionsByVendorDefaultQuery(), ...query };
// Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor(
this.transactionsByVendorRepository,
filter,
this.i18n,
);
const meta = await this.transactionsByVendorMeta.meta(filter);
// Triggers `onVendorTransactionsViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onVendorTransactionsViewed,
{ query },
);
return {
data: reportInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,38 @@
import * as moment from 'moment';
import {
ITransactionsByVendorMeta,
ITransactionsByVendorsFilter,
} from './TransactionsByVendor.types';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class TransactionsByVendorMeta {
constructor(
private readonly financialSheetMeta: FinancialSheetMeta,
) {}
/**
* Retrieves the transactions by vendor meta.
* @returns {Promise<ITransactionsByVendorMeta>}
*/
public async meta(
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorMeta> {
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 = 'Transactions By Vendor';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

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

View File

@@ -0,0 +1,263 @@
import * as R from 'ramda';
import { isEmpty, map } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { ACCOUNT_TYPE } from '@/constants/accounts';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ModelObject } from 'objection';
import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types';
@Injectable()
export class TransactionsByVendorRepository extends TransactionsByContactRepository {
@Inject(TenancyContext)
public readonly tenancyContext: TenancyContext;
@Inject(AccountRepository)
public readonly accountRepository: AccountRepository;
@Inject(Vendor.name)
public readonly vendorModel: typeof Vendor;
@Inject(Account.name)
public readonly accountModel: typeof Account;
@Inject(AccountTransaction.name)
public readonly accountTransactionModel: typeof AccountTransaction;
/**
* Ledger.
* @param {Ledger} ledger
*/
public ledger: Ledger;
/**
* Vendors.
* @param {ModelObject<Vendor>[]} vendors
*/
public vendors: ModelObject<Vendor>[];
/**
* Accounts graph.
* @param {any} accountsGraph
*/
public accountsGraph: any;
/**
* Base currency.
* @param {string} baseCurrency
*/
public baseCurrency: string;
/**
* Report filter.
* @param {ITransactionsByVendorsFilter} filter
*/
public filter: ITransactionsByVendorsFilter;
/**
* Report entries.
* @param {ILedgerEntry[]} reportEntries
*/
public reportEntries: ILedgerEntry[];
/**
* Journal.
* @param {Ledger} journal
*/
public journal: Ledger;
async asyncInit() {
await this.initBaseCurrency();
await this.initVendors();
await this.initAccountsGraph();
await this.initPeriodEntries();
await this.initLedger();
}
/**
* Retrieve the base currency.
*/
async initBaseCurrency() {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = tenantMetadata.baseCurrency;
}
/**
* Retrieve the vendors.
*/
async initVendors() {
const vendors = await this.getVendors(this.filter.vendorsIds);
this.vendors = vendors;
}
/**
* Retrieve the accounts graph.
*/
async initAccountsGraph() {
this.accountsGraph = await this.accountRepository.getDependencyGraph();
}
/**
* Retrieve the report entries.
*/
async initPeriodEntries() {
this.reportEntries = await this.getReportEntries(
this.filter.fromDate,
this.filter.toDate,
);
}
async initLedger() {
this.journal = new Ledger(this.reportEntries);
}
/**
* Retrieve the vendors opening balance transactions.
* @param {Date} openingDate - The opening date.
* @param {number[]} customersIds - The customers ids.
* @returns {Promise<ILedgerEntry[]>}
*/
public async getVendorsOpeningBalanceEntries(
openingDate: Date,
customersIds?: number[],
): Promise<ILedgerEntry[]> {
const openingTransactions = await this.getVendorsOpeningBalance(
openingDate,
customersIds,
);
// @ts-ignore
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'credit')),
)(openingTransactions);
}
/**
* Retrieve the vendors period transactions.
* @param {Date|string} openingDate
* @param {number[]} customersIds
*/
public async getVendorsPeriodEntries(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
): Promise<ILedgerEntry[]> {
const transactions = await this.getVendorsPeriodTransactions(
fromDate,
toDate,
);
// @ts-ignore
return R.compose(
R.map(R.assoc('accountNormal', 'credit')),
R.map((trans) => ({
// @ts-ignore
...trans,
// @ts-ignore
referenceTypeFormatted: trans.referenceTypeFormatted,
})),
)(transactions);
}
/**
* Retrieve the report ledger entries from repository.
* @param {number} tenantId
* @param {Date} fromDate
* @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/
public async getReportEntries(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
): Promise<ILedgerEntry[]> {
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
return [
...(await this.getVendorsOpeningBalanceEntries(openingBalanceDate)),
...(await this.getVendorsPeriodEntries(fromDate, toDate)),
];
}
/**
* Retrieve the report vendors.
* @param {number[]} vendorsIds - The vendors IDs.
* @returns {Promise<IVendor[]>}
*/
public async getVendors(vendorsIds?: number[]): Promise<Vendor[]> {
return await this.vendorModel.query().onBuild((q) => {
q.orderBy('displayName');
if (!isEmpty(vendorsIds)) {
q.whereIn('id', vendorsIds);
}
});
}
/**
* Retrieve the accounts receivable.
* @returns {Promise<IAccount[]>}
*/
public async getPayableAccounts(): Promise<Account[]> {
const accounts = await this.accountModel
.query()
.where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {Date} openingDate - The opening date.
* @param {number[]} customersIds - The customers IDs.
* @returns {Promise<AccountTransaction[]>}
*/
public async getVendorsOpeningBalance(
openingDate: Date,
customersIds?: number[],
): Promise<AccountTransaction[]> {
const payableAccounts = await this.getPayableAccounts();
const payableAccountsIds = map(payableAccounts, 'id');
const openingTransactions = await this.accountTransactionModel
.query()
.modify(
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds,
);
return openingTransactions;
}
/**
* Retrieve vendors periods transactions.
* @param {Date} fromDate - The from date.
* @param {Date} toDate - The to date.
* @returns {Promise<AccountTransaction[]>}
*/
public async getVendorsPeriodTransactions(
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
): Promise<AccountTransaction[]> {
const receivableAccounts = await this.getPayableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await this.accountTransactionModel
.query()
.onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -0,0 +1,91 @@
import * as R from 'ramda';
import { ITransactionsByVendorsVendor } from './TransactionsByVendor.types';
import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows';
import { tableRowMapper } from '../../utils/Table.utils';
import { ITableRow, ITableColumn } from '../../types/Table.types';
enum ROW_TYPE {
OPENING_BALANCE = 'OPENING_BALANCE',
CLOSING_BALANCE = 'CLOSING_BALANCE',
TRANSACTION = 'TRANSACTION',
VENDOR = 'VENDOR',
}
export class TransactionsByVendorsTable extends TransactionsByContactsTableRows {
private vendorsTransactions: ITransactionsByVendorsVendor[];
/**
* Constructor method.
* @param {ITransactionsByVendorsVendor[]} vendorsTransactions -
* @param {any} i18n
*/
constructor(vendorsTransactions: ITransactionsByVendorsVendor[], i18n) {
super();
this.vendorsTransactions = vendorsTransactions;
this.i18n = i18n;
}
/**
* Retrieve the table row of vendor details.
* @param {ITransactionsByVendorsVendor} vendor -
* @returns {ITableRow[]}
*/
private vendorDetails = (vendor: ITransactionsByVendorsVendor) => {
const columns = [
{ key: 'vendorName', accessor: 'vendorName' },
...R.repeat({ key: 'empty', value: '' }, 5),
{
key: 'closingBalanceValue',
accessor: 'closingBalance.formattedAmount',
},
];
return {
...tableRowMapper(vendor, columns, { rowTypes: [ROW_TYPE.VENDOR] }),
children: R.pipe(
R.when(
R.always(vendor.transactions.length > 0),
R.pipe(
R.concat(this.contactTransactions(vendor)),
R.prepend(this.contactOpeningBalance(vendor)),
),
),
R.append(this.contactClosingBalance(vendor)),
)([]),
};
};
/**
* Retrieve the table rows of the vendor section.
* @param {ITransactionsByVendorsVendor} vendor
* @returns {ITableRow[]}
*/
private vendorRowsMapper = (vendor: ITransactionsByVendorsVendor) => {
return R.pipe(this.vendorDetails)(vendor);
};
/**
* Retrieve the table rows of transactions by vendors report.
* @param {ITransactionsByVendorsVendor[]} vendors
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.map(this.vendorRowsMapper)(this.vendorsTransactions);
};
/**
* Retrieve the table columns of transactions by vendors report.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return [
{ key: 'vendor_name', label: 'Vendor name' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'ref_type', label: 'Reference Type' },
{ key: 'transaction_type', label: 'Transaction Type' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
{ key: 'running_balance', label: 'Running Balance' },
];
};
}

View File

@@ -0,0 +1,39 @@
import { TransactionsByVendorsTable } from './TransactionsByVendorTable';
import {
ITransactionsByVendorTable,
ITransactionsByVendorsFilter,
} from './TransactionsByVendor.types';
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class TransactionsByVendorTableInjectable {
constructor(
private readonly transactionsByVendor: TransactionsByVendorsInjectable,
private readonly i18n: I18nService
) {}
/**
* Retrieves the transactions by vendor in table format.
* @param {ITransactionsByReferenceQuery} query - The filter query.
* @returns {Promise<ITransactionsByVendorTable>}
*/
public async table(
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorTable> {
const sheet = await this.transactionsByVendor.transactionsByVendors(
query
);
const table = new TransactionsByVendorsTable(sheet.data, this.i18n);
return {
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query,
meta: sheet.meta,
};
}
}

View File

@@ -0,0 +1,23 @@
export const HtmlTableCustomCss = `
table tr td:not(:first-child) {
border-left: 1px solid #ececec;
}
table tr:last-child td {
border-bottom: 1px solid #ececec;
}
table .cell--credit,
table .cell--debit,
table .column--credit,
table .column--debit,
table .column--running_balance,
table .cell--running_balance{
text-align: right;
}
table tr.row-type--closing-balance td,
table tr.row-type--opening-balance td {
font-weight: 600;
}
table tr.row-type--vendor:not(:first-child) td {
border-top: 1px solid #ddd;
}
`;

View File

@@ -0,0 +1,22 @@
import * as moment from 'moment';
export const getTransactionsByVendorDefaultQuery = () => {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
comparison: {
percentageOfColumn: true,
},
noneZero: false,
noneTransactions: true,
vendorsIds: [],
};
}

View File

@@ -0,0 +1,53 @@
import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
import { TrialBalanceSheetPdfInjectable } from './TrialBalanceSheetPdfInjectsable';
import { Injectable } from '@nestjs/common';
import { TableSheet } from '../../common/TableSheet';
import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types';
@Injectable()
export class TrialBalanceExportInjectable {
constructor(
private readonly trialBalanceSheetTable: TrialBalanceSheetTableInjectable,
private readonly trialBalanceSheetPdf: TrialBalanceSheetPdfInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ITrialBalanceSheetQuery) {
const table = await this.trialBalanceSheetTable.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 {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(query: ITrialBalanceSheetQuery): Promise<string> {
const table = await this.trialBalanceSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
/**
* Retrieves the trial balance sheet in PDF format.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async pdf(query: ITrialBalanceSheetQuery): Promise<Buffer> {
return this.trialBalanceSheetPdf.pdf(query);
}
}

View File

@@ -0,0 +1,75 @@
import {
Controller,
Get,
Headers,
Query,
Res,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { castArray } from 'lodash';
import { Response } from 'express';
import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types';
import { AcceptType } from '@/constants/accept-type';
import { TrialBalanceSheetApplication } from './TrialBalanceSheetApplication';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('reports/trial-balance-sheet')
@ApiTags('reports')
@PublicRoute()
export class TrialBalanceSheetController {
constructor(
private readonly trialBalanceSheetApp: TrialBalanceSheetApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get trial balance sheet' })
@ApiResponse({ status: 200, description: 'Trial balance sheet' })
async getTrialBalanceSheet(
@Query() query: ITrialBalanceSheetQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
const filter = {
...query,
accountIds: castArray(query.accountIds),
};
// Retrieves in json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const { table, meta, query } = await this.trialBalanceSheetApp.table(
filter,
);
return res.status(200).send({ table, meta, query });
// Retrieves in xlsx format
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.trialBalanceSheetApp.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 in csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.trialBalanceSheetApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves in pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.trialBalanceSheetApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves in json format.
} else {
const { data, query, meta } = await this.trialBalanceSheetApp.sheet(
filter,
);
return res.status(200).send({ data, query, meta });
}
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { TrialBalanceExportInjectable } from './TrialBalanceExportInjectable';
import { TrialBalanceSheetController } from './TrialBalanceSheet.controller';
import { TrialBalanceSheetApplication } from './TrialBalanceSheetApplication';
import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable';
import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TrialBalanceSheetPdfInjectable } from './TrialBalanceSheetPdfInjectsable';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
providers: [
TrialBalanceSheetApplication,
TrialBalanceSheetService,
TrialBalanceSheetTableInjectable,
TrialBalanceExportInjectable,
TrialBalanceSheetMeta,
TrialBalanceSheetRepository,
TenancyContext,
TrialBalanceSheetPdfInjectable
],
controllers: [TrialBalanceSheetController],
})
export class TrialBalanceSheetModule {}

View File

@@ -0,0 +1,262 @@
import { sumBy } from 'lodash';
import * as R from 'ramda';
import {
ITrialBalanceSheetQuery,
ITrialBalanceAccount,
ITrialBalanceTotal,
ITrialBalanceSheetData,
} from './TrialBalanceSheet.types';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Account } from '@/modules/Accounts/models/Account.model';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { ModelObject } from 'objection';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
export class TrialBalanceSheet extends FinancialSheet {
/**
* Trial balance sheet query.
* @param {ITrialBalanceSheetQuery} query
*/
public query: ITrialBalanceSheetQuery;
/**
* Trial balance sheet repository.
* @param {TrialBalanceSheetRepository}
*/
public repository: TrialBalanceSheetRepository;
/**
* Organization base currency.
* @param {string}
*/
public baseCurrency: string;
/**
* Constructor method.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @param {IAccount[]} accounts
* @param journalFinancial
*/
constructor(
query: ITrialBalanceSheetQuery,
repository: TrialBalanceSheetRepository,
baseCurrency: string
) {
super();
this.query = query;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
this.baseCurrency = baseCurrency;
}
/**
* Retrieves the closing credit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountCredit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingCredit();
}
/**
* Retrieves the closing debit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountDebit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingDebit();
}
/**
* Retrieves the closing total of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountTotal(accountId: number) {
const credit = this.getClosingAccountCredit(accountId);
const debit = this.getClosingAccountDebit(accountId);
return debit - credit;
}
/**
* Account mapper.
* @param {IAccount} account
* @return {ITrialBalanceAccount}
*/
private accountTransformer = (
account: Account
): ITrialBalanceAccount => {
const debit = this.getClosingAccountDebit(account.id);
const credit = this.getClosingAccountCredit(account.id);
const balance = this.getClosingAccountTotal(account.id);
return {
id: account.id,
parentAccountId: account.parentAccountId,
name: account.name,
formattedName: account.code
? `${account.name} - ${account.code}`
: `${account.name}`,
code: account.code,
accountNormal: account.accountNormal,
credit,
debit,
balance,
currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(credit),
formattedDebit: this.formatNumber(debit),
formattedBalance: this.formatNumber(balance),
};
};
/**
* Filters trial balance sheet accounts nodes based on the given report query.
* @param {ITrialBalanceAccount} accountNode
* @returns {boolean}
*/
private accountFilter = (accountNode: ITrialBalanceAccount): boolean => {
const { noneTransactions, noneZero, onlyActive } = this.query;
// Conditions pair filter detarminer.
const condsPairFilters = [
[noneTransactions, this.filterNoneTransactions],
[noneZero, this.filterNoneZero],
[onlyActive, this.filterActiveOnly],
];
return allPassedConditionsPass(condsPairFilters)(accountNode);
};
/**
* Fitlers the accounts nodes.
* @param {ITrialBalanceAccount[]} accountsNodes
* @returns {ITrialBalanceAccount[]}
*/
private accountsFilter = (
accountsNodes: ITrialBalanceAccount[]
): ITrialBalanceAccount[] => {
return accountsNodes.filter(this.accountFilter);
};
/**
* Mappes the given account object to trial balance account node.
* @param {IAccount[]} accountsNodes
* @returns {ITrialBalanceAccount[]}
*/
private accountsMapper = (
accountsNodes: ModelObject<Account>[]
): ITrialBalanceAccount[] => {
return accountsNodes.map(this.accountTransformer);
};
/**
* Detarmines whether the given account node is not none transactions.
* @param {ITrialBalanceAccount} accountNode
* @returns {boolean}
*/
private filterNoneTransactions = (
accountNode: ITrialBalanceAccount
): boolean => {
return false === this.repository.totalAccountsLedger.isEmpty();
};
/**
* Detarmines whether the given account none zero.
* @param {ITrialBalanceAccount} accountNode
* @returns {boolean}
*/
private filterNoneZero = (accountNode: ITrialBalanceAccount): boolean => {
return accountNode.balance !== 0;
};
/**
* Detarmines whether the given account is active.
* @param {ITrialBalanceAccount} accountNode
* @returns {boolean}
*/
private filterActiveOnly = (accountNode: ITrialBalanceAccount): boolean => {
return accountNode.credit !== 0 || accountNode.debit !== 0;
};
/**
* Transformes the flatten nodes to nested nodes.
* @param {ITrialBalanceAccount[]} flattenAccounts
* @returns {ITrialBalanceAccount[]}
*/
private nestedAccountsNode = (
flattenAccounts: ITrialBalanceAccount[]
): ITrialBalanceAccount[] => {
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
};
/**
* Retrieve trial balance total section.
* @param {ITrialBalanceAccount[]} accountsBalances
* @return {ITrialBalanceTotal}
*/
private tatalSection(
accountsBalances: ITrialBalanceAccount[]
): ITrialBalanceTotal {
const credit = sumBy(accountsBalances, 'credit');
const debit = sumBy(accountsBalances, 'debit');
const balance = sumBy(accountsBalances, 'balance');
const currencyCode = this.baseCurrency;
return {
credit,
debit,
balance,
currencyCode,
formattedCredit: this.formatTotalNumber(credit),
formattedDebit: this.formatTotalNumber(debit),
formattedBalance: this.formatTotalNumber(balance),
};
}
/**
* Retrieve accounts section of trial balance report.
* @param {IAccount[]} accounts
* @returns {ITrialBalanceAccount[]}
*/
private accountsSection(accounts: ModelObject<Account>[]) {
return R.compose(
this.nestedAccountsNode,
this.accountsFilter,
this.accountsMapper
)(accounts);
}
/**
* Retrieve trial balance sheet statement data.
* Note: Retruns null in case there is no transactions between the given date periods.
*
* @return {ITrialBalanceSheetData}
*/
public reportData(): ITrialBalanceSheetData {
// Retrieve accounts nodes.
const accounts = this.accountsSection(this.repository.accounts);
// Retrieve account node.
const total = this.tatalSection(accounts);
return { accounts, total };
}
}

View File

@@ -0,0 +1,56 @@
import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types";
export interface ITrialBalanceSheetQuery {
fromDate: Date | string;
toDate: Date | string;
numberFormat: INumberFormatQuery;
basis: 'cash' | 'accrual';
noneZero: boolean;
noneTransactions: boolean;
onlyActive: boolean;
accountIds: number[];
branchesIds?: number[];
}
export interface ITrialBalanceTotal {
credit: number;
debit: number;
balance: number;
currencyCode: string;
formattedCredit: string;
formattedDebit: string;
formattedBalance: string;
}
export interface ITrialBalanceSheetMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}
export interface ITrialBalanceAccount extends ITrialBalanceTotal {
id: number;
parentAccountId: number;
name: string;
formattedName: string;
code: string;
accountNormal: string;
}
export type ITrialBalanceSheetData = {
accounts: ITrialBalanceAccount[];
total: ITrialBalanceTotal;
};
export interface ITrialBalanceStatement {
data: ITrialBalanceSheetData;
query: ITrialBalanceSheetQuery;
meta: ITrialBalanceSheetMeta;
}
export interface ITrialBalanceSheetTable extends IFinancialTable {
meta: ITrialBalanceSheetMeta;
query: ITrialBalanceSheetQuery;
}

View File

@@ -0,0 +1,61 @@
import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
import { TrialBalanceExportInjectable } from './TrialBalanceExportInjectable';
import { ITrialBalanceSheetQuery, ITrialBalanceStatement } from './TrialBalanceSheet.types';
import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TrialBalanceSheetApplication {
constructor(
private readonly sheetService: TrialBalanceSheetService,
private readonly tablable: TrialBalanceSheetTableInjectable,
private readonly exportable: TrialBalanceExportInjectable,
) {}
/**
* Retrieves the trial balance sheet.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<ITrialBalanceStatement>}
*/
public sheet(
query: ITrialBalanceSheetQuery,
): Promise<ITrialBalanceStatement> {
return this.sheetService.trialBalanceSheet(query);
}
/**
* Retrieves the trial balance sheet in table format.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<ITrialBalanceSheetTable>}
*/
public table(query: ITrialBalanceSheetQuery) {
return this.tablable.table(query);
}
/**
* Retrieve the trial balance sheet in CSV format.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public csv(query: ITrialBalanceSheetQuery) {
return this.exportable.csv(query);
}
/**
* Retrieve the trial balance sheet in XLSX format.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ITrialBalanceSheetQuery) {
return this.exportable.xlsx(query);
}
/**
* Retrieve the trial balance sheet in pdf format.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async pdf(query: ITrialBalanceSheetQuery) {
return this.exportable.pdf(query);
}
}

View File

@@ -0,0 +1,68 @@
import {
ITrialBalanceSheetQuery,
ITrialBalanceStatement,
} from './TrialBalanceSheet.types';
import { TrialBalanceSheet } from './TrialBalanceSheet';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta';
import { getTrialBalanceSheetDefaultQuery } from './_utils';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class TrialBalanceSheetService {
constructor(
private readonly trialBalanceSheetMetaService: TrialBalanceSheetMeta,
private readonly eventPublisher: EventEmitter2,
private readonly trialBalanceSheetRepository: TrialBalanceSheetRepository,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Retrieve trial balance sheet statement.
* @param {ITrialBalanceSheetQuery} query - Trial balance sheet query.
* @return {ITrialBalanceStatement}
*/
public async trialBalanceSheet(
query: ITrialBalanceSheetQuery,
): Promise<ITrialBalanceStatement> {
const filter = {
...getTrialBalanceSheetDefaultQuery(),
...query,
};
this.trialBalanceSheetRepository.setQuery(filter);
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
// Loads the resources.
await this.trialBalanceSheetRepository.asyncInitialize();
// Trial balance report instance.
const trialBalanceInstance = new TrialBalanceSheet(
filter,
this.trialBalanceSheetRepository,
tenantMetadata.baseCurrency,
);
// Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData();
// Trial balance sheet meta.
const meta = await this.trialBalanceSheetMetaService.meta(filter);
// Triggers `onTrialBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onTrialBalanceSheetView,
{
query,
},
);
return {
data: trialBalanceSheetData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,33 @@
import * as moment from 'moment';
import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery } from './TrialBalanceSheet.types';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class TrialBalanceSheetMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the trial balance sheet meta.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<ITrialBalanceSheetMeta>}
*/
public async meta(
query: ITrialBalanceSheetQuery
): Promise<ITrialBalanceSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} to ${formattedToDate}`;
const sheetName = 'Trial Balance Sheet';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types';
import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
import { HtmlTableCustomCss } from './_constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TrialBalanceSheetPdfInjectable {
constructor(
private readonly trialBalanceSheetTable: TrialBalanceSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given trial balance sheet table to pdf.
* @param {ITrialBalanceSheetQuery} query - Trial balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: ITrialBalanceSheetQuery): Promise<Buffer> {
const table = await this.trialBalanceSheetTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,112 @@
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
@Injectable({ scope: Scope.TRANSIENT })
export class TrialBalanceSheetRepository {
private query: ITrialBalanceSheetQuery;
@Inject(Account.name)
private accountModel: typeof Account;
@Inject(AccountTransaction.name)
private accountTransactionModel: typeof AccountTransaction;
@Inject(AccountRepository)
private accountRepository: AccountRepository;
public accountsDepGraph: any;
public accounts: Array<ModelObject<Account>>;
/**
* Total closing accounts ledger.
* @param {Ledger}
*/
public totalAccountsLedger: Ledger;
/**
* Set query.
* @param {ITrialBalanceSheetQuery} query
*/
public setQuery(query: ITrialBalanceSheetQuery) {
this.query = query;
}
/**
* Async initialize.
* @returns {Promise<void>}
*/
public asyncInitialize = async () => {
await this.initAccounts();
await this.initAccountsClosingTotalLedger();
};
// ----------------------------
// # Accounts
// ----------------------------
/**
* Initialize accounts.
* @returns {Promise<void>}
*/
public initAccounts = async () => {
const accounts = await this.getAccounts();
const accountsDepGraph = await this.accountRepository.getDependencyGraph();
this.accountsDepGraph = accountsDepGraph;
this.accounts = accounts;
};
/**
* Initialize all accounts closing total ledger.
* @return {Promise<void>}
*/
public initAccountsClosingTotalLedger = async (): Promise<void> => {
const totalByAccounts = await this.closingAccountsTotal(this.query.toDate);
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccounts);
};
/**
* Retrieve accounts of the report.
* @return {Promise<IAccount[]>}
*/
private getAccounts = () => {
return this.accountModel.query();
};
/**
* Retrieve the opening balance transactions of the report.
* @param {Date|string} openingDate -
*/
public closingAccountsTotal = async (openingDate: Date | string) => {
return this.accountTransactionModel.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', null, openingDate);
query.withGraphFetched('account');
// @ts-ignore
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.branchesIds)) {
// @ts-ignore
query.modify('filterByBranches', this.query.branchesIds);
}
};
}

View File

@@ -0,0 +1,151 @@
import * as R from 'ramda';
import { FinancialSheet } from '../../common/FinancialSheet';
import { FinancialTable } from '../../common/FinancialTable';
import {
ITrialBalanceAccount,
ITrialBalanceSheetData,
ITrialBalanceSheetQuery,
ITrialBalanceTotal,
} from './TrialBalanceSheet.types';
import {
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { I18nService } from 'nestjs-i18n';
import { tableRowMapper } from '../../utils/Table.utils';
import { IROW_TYPE } from './_constants';
export class TrialBalanceSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure,
)(FinancialSheet) {
/**
* Trial balance sheet data.
* @param {ITrialBalanceSheetData}
*/
public data: ITrialBalanceSheetData;
/**
* Trial balance sheet query.
* @param {ITrialBalanceSheetQuery}
*/
public query: ITrialBalanceSheetQuery;
public i18n: I18nService;
/**
* Constructor method.
* @param {IBalanceSheetStatementData} reportData -
* @param {ITrialBalanceSheetQuery} query -
*/
constructor(
data: ITrialBalanceSheetData,
query: ITrialBalanceSheetQuery,
i18n: I18nService,
) {
super();
this.data = data;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieve the common columns for all report nodes.
* @param {ITableColumnAccessor[]}
*/
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'account', accessor: 'formattedName' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'total', accessor: 'formattedBalance' },
];
};
/**
* Maps the account node to table row.
* @param {ITrialBalanceAccount} node -
* @returns {ITableRow}
*/
private accountNodeTableRowsMapper = (
node: ITrialBalanceAccount,
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.ACCOUNT],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Maps the total node to table row.
* @param {ITrialBalanceTotal} node -
* @returns {ITableRow}
*/
private totalNodeTableRowsMapper = (node: ITrialBalanceTotal): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.TOTAL],
id: 'total',
};
return tableRowMapper(node, columns, meta);
};
/**
* Mappes the given report sections to table rows.
* @param {IBalanceSheetDataNode[]} nodes -
* @return {ITableRow}
*/
private accountsToTableRowsMap = (
nodes: ITrialBalanceAccount[],
): ITableRow[] => {
return this.mapNodesDeep(nodes, this.accountNodeTableRowsMapper);
};
/**
* Retrieves the accounts table rows of the given report data.
* @returns {ITableRow[]}
*/
private accountsTableRows = (): ITableRow[] => {
return this.accountsToTableRowsMap(this.data.accounts);
};
/**
* Maps the given total node to table row.
* @returns {ITableRow}
*/
private totalTableRow = (): ITableRow => {
return this.totalNodeTableRowsMapper(this.data.total);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.unless(R.isEmpty, R.append(this.totalTableRow())),
R.concat(this.accountsTableRows()),
)([]);
};
/**
* Retrrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
{ key: 'account', label: 'Account' },
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
{ key: 'total', label: 'Total' },
]),
)([]);
};
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import {
ITrialBalanceSheetQuery,
ITrialBalanceSheetTable,
} from './TrialBalanceSheet.types';
import { TrialBalanceSheetTable } from './TrialBalanceSheetTable';
import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class TrialBalanceSheetTableInjectable {
constructor(
private readonly sheet: TrialBalanceSheetService,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the trial balance sheet table.
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<ITrialBalanceSheetTable>}
*/
public async table(
query: ITrialBalanceSheetQuery,
): Promise<ITrialBalanceSheetTable> {
const trialBalance = await this.sheet.trialBalanceSheet(query);
const table = new TrialBalanceSheetTable(
trialBalance.data,
query,
this.i18n,
);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
meta: trialBalance.meta,
query: trialBalance.query,
};
}
}

View File

@@ -0,0 +1,25 @@
export enum IROW_TYPE {
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
export const HtmlTableCustomCss = `
table tr.row-type--total td{
border-top: 1px solid #bbb;
font-weight: 600;
border-bottom: 3px double #000;
}
table .column--account {
width: 400px;
}
table .column--debit,
table .column--credit,
table .column--total,
table .cell--debit,
table .cell--credit,
table .cell--total{
text-align: right;
}
`;

View File

@@ -0,0 +1,18 @@
import * as moment from 'moment';
export const getTrialBalanceSheetDefaultQuery = () => ({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
divideOn1000: false,
negativeFormat: 'mines',
showZero: false,
formatMoney: 'total',
precision: 2,
},
basis: 'accrual',
noneZero: false,
noneTransactions: true,
onlyActive: false,
accountIds: [],
});

View File

@@ -1,6 +1,6 @@
export interface IColumnMapperMeta {
key: string;
accessor?: string;
accessor?: string | ((value: any) => string);
value?: string;
}
@@ -11,7 +11,7 @@ export interface ITableCell {
export type ITableRow = {
cells: ITableCell[];
rowTypes?: Array<any>
rowTypes?: Array<any>;
id?: string;
};
@@ -42,7 +42,7 @@ export interface IFinancialTable {
}
export interface IFinancialTableTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}
amount: number;
formattedAmount: string;
currencyCode: string;
}