mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
fix: vendor summary balance and transaction report.
This commit is contained in:
@@ -2,6 +2,7 @@ import { INumberFormatQuery } from './FinancialStatements';
|
||||
|
||||
export interface IVendorBalanceSummaryQuery {
|
||||
asDate: Date;
|
||||
vendorsIds: number[],
|
||||
numberFormat: INumberFormatQuery;
|
||||
comparison: {
|
||||
percentageOfColumn: boolean;
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet {
|
||||
protected getTotalFormat(amount: number): IContactBalanceSummaryAmount {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||
formattedAmount: this.formatTotalNumber(amount, { money: true }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
ITransactionsByCustomersTransaction,
|
||||
ITransactionsByCustomersFilter,
|
||||
ITransactionsByCustomersCustomer,
|
||||
ITransactionsByCustomersAmount,
|
||||
ITransactionsByCustomersData,
|
||||
INumberFormatQuery,
|
||||
IAccountTransaction,
|
||||
ICustomer,
|
||||
} from 'interfaces';
|
||||
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';
|
||||
|
||||
@@ -173,13 +173,10 @@ export default class TransactionsByCustomersService
|
||||
filter,
|
||||
baseCurrency
|
||||
);
|
||||
const reportData = reportInstance.reportData();
|
||||
|
||||
const reportColumns = reportInstance.reportColumns();
|
||||
|
||||
return {
|
||||
data: reportData,
|
||||
columns: reportColumns,
|
||||
data: reportInstance.reportData(),
|
||||
columns: reportInstance.reportColumns(),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import {
|
||||
ITransactionsByVendorsFilter,
|
||||
ITransactionsByVendorsTransaction,
|
||||
ITransactionsByVendorsVendor,
|
||||
ITransactionsByVendorsAmount,
|
||||
ITransactionsByVendorsData,
|
||||
IAccountTransaction,
|
||||
ILedger,
|
||||
INumberFormatQuery,
|
||||
IVendor
|
||||
} from 'interfaces';
|
||||
@@ -18,6 +17,8 @@ export default class TransactionsByVendors extends TransactionsByContact{
|
||||
readonly filter: ITransactionsByVendorsFilter;
|
||||
readonly baseCurrency: string;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
readonly accountsGraph: any;
|
||||
readonly ledger: ILedger;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -27,14 +28,16 @@ export default class TransactionsByVendors extends TransactionsByContact{
|
||||
*/
|
||||
constructor(
|
||||
vendors: IVendor[],
|
||||
transactionsByContact: Map<number, IAccountTransaction[]>,
|
||||
accountsGraph: any,
|
||||
ledger: ILedger,
|
||||
filter: ITransactionsByVendorsFilter,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.contacts = vendors;
|
||||
this.transactionsByContact = transactionsByContact;
|
||||
this.accountsGraph = accountsGraph;
|
||||
this.ledger = ledger;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.filter = filter;
|
||||
this.numberFormat = this.filter.numberFormat;
|
||||
@@ -50,12 +53,17 @@ export default class TransactionsByVendors extends TransactionsByContact{
|
||||
vendorId: number,
|
||||
openingBalance: number
|
||||
): ITransactionsByVendorsTransaction[] {
|
||||
const transactions = this.transactionsByContact.get(vendorId + '') || [];
|
||||
const openingBalanceLedger = this.ledger
|
||||
.whereContactId(vendorId)
|
||||
.whereFromDate(this.filter.fromDate)
|
||||
.whereToDate(this.filter.toDate);
|
||||
|
||||
const openingEntries = openingBalanceLedger.getEntries();
|
||||
|
||||
return R.compose(
|
||||
R.curry(this.contactTransactionRunningBalance)(openingBalance),
|
||||
R.map(this.contactTransactionMapper.bind(this))
|
||||
).bind(this)(transactions);
|
||||
).bind(this)(openingEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,17 +74,17 @@ export default class TransactionsByVendors extends TransactionsByContact{
|
||||
private vendorMapper(
|
||||
vendor: IVendor
|
||||
): ITransactionsByVendorsVendor {
|
||||
const openingBalance = this.getContactOpeningBalance(1);
|
||||
const openingBalance = this.getContactOpeningBalance(vendor.id);
|
||||
const transactions = this.vendorTransactions(vendor.id, openingBalance);
|
||||
const closingBalance = this.getContactClosingBalance(transactions, 0);
|
||||
const closingBalance = this.getContactClosingBalance(transactions, openingBalance);
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
openingBalance: this.getContactAmount(
|
||||
openingBalance: this.getTotalAmountMeta(
|
||||
openingBalance,
|
||||
vendor.currencyCode
|
||||
),
|
||||
closingBalance: this.getContactAmount(
|
||||
closingBalance: this.getTotalAmountMeta(
|
||||
closingBalance,
|
||||
vendor.currencyCode
|
||||
),
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { groupBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { map } from 'lodash';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
IVendor,
|
||||
ITransactionsByVendorsService,
|
||||
ITransactionsByVendorsFilter,
|
||||
ITransactionsByVendorsStatement,
|
||||
} from 'interfaces';
|
||||
import TransactionsByVendor from './TransactionsByVendor';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
|
||||
export default class TransactionsByVendorsService
|
||||
implements ITransactionsByVendorsService {
|
||||
@@ -40,6 +44,100 @@ export default class TransactionsByVendorsService
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report vendors.
|
||||
* @param tenantId
|
||||
* @returns
|
||||
*/
|
||||
private getReportVendors(tenantId: number): Promise<IVendor[]> {
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
return Vendor.query().orderBy('displayName');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the accounts receivable.
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
*/
|
||||
private async getPayableAccounts(tenantId: number) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const accounts = await Account.query().where(
|
||||
'accountType',
|
||||
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
|
||||
);
|
||||
return accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customers opening balance transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} openingDate
|
||||
* @param {number} customersIds
|
||||
* @returns {}
|
||||
*/
|
||||
private async getVendorsOpeningBalance(
|
||||
tenantId: number,
|
||||
openingDate: Date,
|
||||
customersIds?: number[]
|
||||
): Promise<ILedgerEntry[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const payableAccounts = await this.getPayableAccounts(tenantId);
|
||||
const payableAccountsIds = map(payableAccounts, 'id');
|
||||
|
||||
const openingTransactions = await AccountTransaction.query().modify(
|
||||
'contactsOpeningBalance',
|
||||
openingDate,
|
||||
payableAccountsIds,
|
||||
customersIds
|
||||
);
|
||||
return R.compose(
|
||||
R.map(R.assoc('date', openingDate)),
|
||||
R.map(R.assoc('accountNormal', 'credit'))
|
||||
)(openingTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {Date|string} openingDate
|
||||
* @param {number[]} customersIds
|
||||
*/
|
||||
async getVendorsPeriodTransactions(
|
||||
tenantId: number,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): Promise<ILedgerEntry[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const receivableAccounts = await this.getPayableAccounts(tenantId);
|
||||
const receivableAccountsIds = map(receivableAccounts, 'id');
|
||||
|
||||
const transactions = await AccountTransaction.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 R.compose(R.map(R.assoc('accountNormal', 'credit')))(transactions);
|
||||
}
|
||||
|
||||
async getReportTransactions(tenantId: number, fromDate: Date, toDate: Date) {
|
||||
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
|
||||
|
||||
return [
|
||||
...(await this.getVendorsOpeningBalance(tenantId, openingBalanceDate)),
|
||||
...(await this.getVendorsPeriodTransactions(tenantId, fromDate, toDate)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions by by the customers.
|
||||
* @param {number} tenantId
|
||||
@@ -50,9 +148,8 @@ export default class TransactionsByVendorsService
|
||||
tenantId: number,
|
||||
query: ITransactionsByVendorsFilter
|
||||
): Promise<ITransactionsByVendorsStatement> {
|
||||
const { transactionsRepository } = this.tenancy.repositories(tenantId);
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
@@ -60,37 +157,35 @@ export default class TransactionsByVendorsService
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const vendors = await Vendor.query().orderBy('displayName');
|
||||
const filter = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
});
|
||||
// Transactions map by contact id.
|
||||
const transactionsMap = new Map(
|
||||
Object.entries(groupBy(transactions, 'contactId'))
|
||||
// Retrieve the report vendors.
|
||||
const vendors = await this.getReportVendors(tenantId);
|
||||
|
||||
// Retrieve the accounts graph.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Journal transactions.
|
||||
const journalTransactions = await this.getReportTransactions(
|
||||
tenantId,
|
||||
filter.fromDate,
|
||||
filter.toDate
|
||||
);
|
||||
// Ledger collection.
|
||||
const journal = new Ledger(journalTransactions);
|
||||
|
||||
// Transactions by customers data mapper.
|
||||
const reportInstance = new TransactionsByVendor(
|
||||
vendors,
|
||||
transactionsMap,
|
||||
accountsGraph,
|
||||
journal,
|
||||
filter,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve the report data.
|
||||
const reportData = reportInstance.reportData();
|
||||
|
||||
// Retireve the report columns.
|
||||
const reportColumns = reportInstance.reportColumns();
|
||||
|
||||
return {
|
||||
data: reportData,
|
||||
columns: reportColumns,
|
||||
data: reportInstance.reportData(),
|
||||
columns: reportInstance.reportColumns(),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IJournalPoster,
|
||||
ILedger,
|
||||
IVendor,
|
||||
IVendorBalanceSummaryVendor,
|
||||
IVendorBalanceSummaryQuery,
|
||||
IVendorBalanceSummaryData,
|
||||
IVendorBalanceSummaryTotal,
|
||||
INumberFormatQuery,
|
||||
} from 'interfaces';
|
||||
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
|
||||
|
||||
export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||
readonly payableLedger: IJournalPoster;
|
||||
readonly ledger: ILedger;
|
||||
readonly baseCurrency: string;
|
||||
readonly vendors: IVendor[];
|
||||
readonly filter: IVendorBalanceSummaryQuery;
|
||||
@@ -25,14 +24,14 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
payableLedger: IJournalPoster,
|
||||
ledger: ILedger,
|
||||
vendors: IVendor[],
|
||||
filter: IVendorBalanceSummaryQuery,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.payableLedger = payableLedger;
|
||||
this.ledger = ledger;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.vendors = vendors;
|
||||
this.filter = filter;
|
||||
@@ -45,11 +44,13 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||
* @returns {IVendorBalanceSummaryVendor}
|
||||
*/
|
||||
private vendorMapper(vendor: IVendor): IVendorBalanceSummaryVendor {
|
||||
const balance = this.payableLedger.getContactBalance(null, vendor.id);
|
||||
const closingBalance = this.ledger
|
||||
.whereContactId(vendor.id)
|
||||
.getClosingBalance();
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
total: this.getContactTotalFormat(balance),
|
||||
total: this.getContactTotalFormat(closingBalance),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { map } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import {
|
||||
IVendor,
|
||||
IVendorBalanceSummaryService,
|
||||
IVendorBalanceSummaryQuery,
|
||||
IVendorBalanceSummaryStatement,
|
||||
} from 'interfaces';
|
||||
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
|
||||
export default class VendorBalanceSummaryService
|
||||
implements IVendorBalanceSummaryService {
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@@ -40,70 +44,110 @@ export default class VendorBalanceSummaryService
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report vendors.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} vendorsIds - Vendors ids.
|
||||
* @returns {IVendor[]}
|
||||
*/
|
||||
getReportVendors(
|
||||
tenantId: number,
|
||||
vendorsIds?: number[]
|
||||
): Promise<IVendor[]> {
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
return Vendor.query()
|
||||
.orderBy('displayName')
|
||||
.onBuild((query) => {
|
||||
if (!isEmpty(vendorsIds)) {
|
||||
query.whereIn('id', vendorsIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPayableAccounts(tenantId: number) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve
|
||||
* @param tenantId
|
||||
* @param asDate
|
||||
* @returns
|
||||
*/
|
||||
async getReportVendorsTransactions(tenantId: number, asDate: Date | string) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve payable accounts .
|
||||
const payableAccounts = await this.getPayableAccounts(tenantId);
|
||||
const payableAccountsIds = map(payableAccounts, 'id');
|
||||
|
||||
// Retrieve the customers transactions of A/R accounts.
|
||||
const customersTranasctions = await AccountTransaction.query().onBuild(
|
||||
(query) => {
|
||||
query.whereIn('accountId', payableAccountsIds);
|
||||
query.modify('filterDateRange', null, asDate);
|
||||
query.groupBy('contactId');
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.select('contactId');
|
||||
}
|
||||
);
|
||||
const commonProps = { accountNormal: 'credit', date: asDate };
|
||||
|
||||
return R.map(R.merge(commonProps))(customersTranasctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the statment of customer balance summary report.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IVendorBalanceSummaryQuery} query -
|
||||
* @param {IVendorBalanceSummaryQuery} query -
|
||||
* @return {Promise<IVendorBalanceSummaryStatement>}
|
||||
*/
|
||||
async vendorBalanceSummary(
|
||||
tenantId: number,
|
||||
query: IVendorBalanceSummaryQuery
|
||||
): Promise<IVendorBalanceSummaryStatement> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization', key: 'base_currency',
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[customer_balance_summary] trying to calculate the report.', {
|
||||
filter,
|
||||
const filter = { ...this.defaultQuery, ...query };
|
||||
this.logger.info(
|
||||
'[customer_balance_summary] trying to calculate the report.',
|
||||
{
|
||||
filter,
|
||||
tenantId,
|
||||
}
|
||||
);
|
||||
// Retrieve the vendors transactions.
|
||||
const vendorsTransactions = await this.getReportVendorsTransactions(
|
||||
tenantId,
|
||||
});
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await accountRepository.all();
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
toDate: query.asDate,
|
||||
});
|
||||
// Transform transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
transactions,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
query.asDate
|
||||
);
|
||||
// Retrieve the customers list ordered by the display name.
|
||||
const vendors = await Vendor.query().orderBy('displayName');
|
||||
const vendors = await this.getReportVendors(tenantId, query.vendorsIds);
|
||||
|
||||
// Ledger query.
|
||||
const ledger = new Ledger(vendorsTransactions);
|
||||
|
||||
// Report instance.
|
||||
const reportInstance = new VendorBalanceSummaryReport(
|
||||
transactionsJournal,
|
||||
ledger,
|
||||
vendors,
|
||||
filter,
|
||||
baseCurrency,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve the report statement.
|
||||
const reportData = reportInstance.reportData();
|
||||
|
||||
// Retrieve the report columns.
|
||||
const reportColumns = reportInstance.reportColumns();
|
||||
|
||||
return {
|
||||
data: reportData,
|
||||
columns: reportColumns,
|
||||
data: reportInstance.reportData(),
|
||||
columns: reportInstance.reportColumns(),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user