mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
WIP: transactions by customers.
This commit is contained in:
@@ -6,6 +6,7 @@ import BaseFinancialReportController from '../BaseFinancialReportController';
|
|||||||
import TransactionsByVendorsTableRows from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
|
import TransactionsByVendorsTableRows from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
|
||||||
import TransactionsByVendorsService from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
import TransactionsByVendorsService from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
||||||
import { ITransactionsByVendorsStatement } from 'interfaces';
|
import { ITransactionsByVendorsStatement } from 'interfaces';
|
||||||
|
|
||||||
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
||||||
@Inject()
|
@Inject()
|
||||||
transactionsByVendorsService: TransactionsByVendorsService;
|
transactionsByVendorsService: TransactionsByVendorsService;
|
||||||
@@ -44,7 +45,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
|||||||
* Transformes the report statement to table rows.
|
* Transformes the report statement to table rows.
|
||||||
* @param {ITransactionsByVendorsStatement} statement -
|
* @param {ITransactionsByVendorsStatement} statement -
|
||||||
*/
|
*/
|
||||||
transformToTableRows({ data }: ITransactionsByVendorsStatement) {
|
private transformToTableRows({ data }: ITransactionsByVendorsStatement) {
|
||||||
return {
|
return {
|
||||||
table: {
|
table: {
|
||||||
data: this.transactionsByVendorsTableRows.tableRows(data),
|
data: this.transactionsByVendorsTableRows.tableRows(data),
|
||||||
@@ -56,7 +57,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
|||||||
* Transformes the report statement to json response.
|
* Transformes the report statement to json response.
|
||||||
* @param {ITransactionsByVendorsStatement} statement -
|
* @param {ITransactionsByVendorsStatement} statement -
|
||||||
*/
|
*/
|
||||||
transformToJsonResponse({
|
private transformToJsonResponse({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
query,
|
query,
|
||||||
|
|||||||
17
server/src/interfaces/Ledger.ts
Normal file
17
server/src/interfaces/Ledger.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export interface ILedger {
|
||||||
|
entries: ILedgerEntry[];
|
||||||
|
|
||||||
|
getEntries(): ILedgerEntry[];
|
||||||
|
whereContactId(contactId: number): ILedger;
|
||||||
|
whereFromDate(fromDate: Date | string): ILedger;
|
||||||
|
whereToDate(toDate: Date | string): ILedger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILedgerEntry {
|
||||||
|
credit: number;
|
||||||
|
debit: number;
|
||||||
|
accountId?: number;
|
||||||
|
accountNormal: string;
|
||||||
|
contactId?: number;
|
||||||
|
date: Date | string;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface ITransactionsByContactsTransaction {
|
|||||||
date: string|Date,
|
date: string|Date,
|
||||||
credit: ITransactionsByContactsAmount;
|
credit: ITransactionsByContactsAmount;
|
||||||
debit: ITransactionsByContactsAmount;
|
debit: ITransactionsByContactsAmount;
|
||||||
|
accountName: string,
|
||||||
runningBalance: ITransactionsByContactsAmount;
|
runningBalance: ITransactionsByContactsAmount;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
referenceNumber: string;
|
referenceNumber: string;
|
||||||
|
|||||||
@@ -50,3 +50,4 @@ export * from './TransactionsByCustomers';
|
|||||||
export * from './TransactionsByContacts';
|
export * from './TransactionsByContacts';
|
||||||
export * from './TransactionsByVendors';
|
export * from './TransactionsByVendors';
|
||||||
export * from './Table';
|
export * from './Table';
|
||||||
|
export * from './Ledger';
|
||||||
@@ -120,6 +120,25 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
query.modify('filterDateRange', null, toDate)
|
query.modify('filterDateRange', null, toDate)
|
||||||
query.modify('sumationCreditDebit')
|
query.modify('sumationCreditDebit')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contactsOpeningBalance(query, openingDate, receivableAccounts, customersIds) {
|
||||||
|
// Filter by date.
|
||||||
|
query.modify('filterDateRange', null, openingDate);
|
||||||
|
|
||||||
|
// Filter by customers.
|
||||||
|
query.whereNot('contactId', null);
|
||||||
|
query.where('accountId', receivableAccounts);
|
||||||
|
|
||||||
|
if (customersIds) {
|
||||||
|
query.whereIn('contactId', customersIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by the contact transactions.
|
||||||
|
query.groupBy('contactId');
|
||||||
|
query.sum('credit as credit');
|
||||||
|
query.sum('debit as debit');
|
||||||
|
query.select('contactId');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -694,4 +694,51 @@ export default class JournalPoster implements IJournalPoster {
|
|||||||
getAccountEntries(accountId: number) {
|
getAccountEntries(accountId: number) {
|
||||||
return this.entries.filter((entry) => entry.account === accountId);
|
return this.entries.filter((entry) => entry.account === accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve total balnace of the given customer/vendor contact.
|
||||||
|
* @param {Number} accountId
|
||||||
|
* @param {Number} contactId
|
||||||
|
* @param {String} contactType
|
||||||
|
* @param {Date} closingDate
|
||||||
|
*/
|
||||||
|
getEntriesBalance(
|
||||||
|
entries
|
||||||
|
) {
|
||||||
|
let balance = 0;
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.credit) {
|
||||||
|
balance -= entry.credit;
|
||||||
|
}
|
||||||
|
if (entry.debit) {
|
||||||
|
balance += entry.debit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContactEntries(
|
||||||
|
contactId: number,
|
||||||
|
openingDate: Date,
|
||||||
|
closingDate?: Date,
|
||||||
|
) {
|
||||||
|
const momentClosingDate = moment(closingDate);
|
||||||
|
const momentOpeningDate = moment(openingDate);
|
||||||
|
|
||||||
|
return this.entries.filter((entry) => {
|
||||||
|
if (
|
||||||
|
(closingDate &&
|
||||||
|
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||||
|
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||||
|
(openingDate &&
|
||||||
|
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||||
|
!momentOpeningDate.isSame(entry.date)) ||
|
||||||
|
(entry.contactId === contactId)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
server/src/services/Accounting/Ledger.ts
Normal file
92
server/src/services/Accounting/Ledger.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
ILedger,
|
||||||
|
ILedgerEntry
|
||||||
|
} from 'interfaces';
|
||||||
|
|
||||||
|
export default class Ledger implements ILedger {
|
||||||
|
readonly entries: ILedgerEntry[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {ILedgerEntry[]} entries
|
||||||
|
*/
|
||||||
|
constructor(entries: ILedgerEntry[]) {
|
||||||
|
this.entries = Ledger.mappingEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the ledegr entries.
|
||||||
|
* @param callback
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
filter(callback) {
|
||||||
|
const entries = this.entries.filter(callback);
|
||||||
|
return new Ledger(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntries(): ILedgerEntry[] {
|
||||||
|
return this.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
whereContactId(contactId: number): ILedger {
|
||||||
|
return this.filter((entry) => entry.contactId === contactId);
|
||||||
|
}
|
||||||
|
|
||||||
|
whereAccountId(accountId: number): ILedger {
|
||||||
|
return this.filter((entry) => entry.accountId === accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
whereFromDate(fromDate: Date | string): ILedger {
|
||||||
|
const fromDateParsed = moment(fromDate);
|
||||||
|
|
||||||
|
return this.filter(
|
||||||
|
(entry) =>
|
||||||
|
fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
whereToDate(toDate: Date | string): ILedger {
|
||||||
|
const toDateParsed = moment(toDate);
|
||||||
|
|
||||||
|
return this.filter(
|
||||||
|
(entry) =>
|
||||||
|
toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the closing balance of the entries.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getClosingBalance() {
|
||||||
|
let closingBalance = 0;
|
||||||
|
|
||||||
|
this.entries.forEach((entry) => {
|
||||||
|
if (entry.accountNormal === 'credit') {
|
||||||
|
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
|
||||||
|
|
||||||
|
} else if (entry.accountNormal === 'debit') {
|
||||||
|
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return closingBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mappingEntries(entries): ILedgerEntry[] {
|
||||||
|
return entries.map(this.mapEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
static mapEntry(entry): ILedgerEntry {
|
||||||
|
return {
|
||||||
|
credit: entry.credit,
|
||||||
|
debit: entry.debit,
|
||||||
|
accountNormal: entry.accountNormal,
|
||||||
|
accountId: entry.accountId,
|
||||||
|
contactId: entry.contactId,
|
||||||
|
date: entry.date,
|
||||||
|
transactionNumber: entry.transactionNumber,
|
||||||
|
referenceNumber: entry.referenceNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,7 +100,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet {
|
|||||||
): IContactBalanceSummaryAmount {
|
): IContactBalanceSummaryAmount {
|
||||||
return {
|
return {
|
||||||
amount,
|
amount,
|
||||||
formattedAmount: this.formatNumber(amount),
|
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||||
currencyCode: this.baseCurrency,
|
currencyCode: this.baseCurrency,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet {
|
|||||||
protected getTotalFormat(amount: number): IContactBalanceSummaryAmount {
|
protected getTotalFormat(amount: number): IContactBalanceSummaryAmount {
|
||||||
return {
|
return {
|
||||||
amount,
|
amount,
|
||||||
formattedAmount: this.formatNumber(amount),
|
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||||
currencyCode: this.baseCurrency,
|
currencyCode: this.baseCurrency,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sumBy } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import {
|
import {
|
||||||
IJournalPoster,
|
IJournalPoster,
|
||||||
@@ -45,11 +45,12 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
|||||||
* @returns {ICustomerBalanceSummaryCustomer}
|
* @returns {ICustomerBalanceSummaryCustomer}
|
||||||
*/
|
*/
|
||||||
private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer {
|
private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer {
|
||||||
const balance = this.receivableLedger.getContactBalance(null, customer.id);
|
const customerBalance = this.receivableLedger.get(customer.id);
|
||||||
|
const balanceAmount = get(customerBalance, 'balance', 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customerName: customer.displayName,
|
customerName: customer.displayName,
|
||||||
total: this.getContactTotalFormat(balance),
|
total: this.getContactTotalFormat(balanceAmount),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Inject } from 'typedi';
|
import { Inject } from 'typedi';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { map } from 'lodash';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
import Journal from 'services/Accounting/JournalPoster';
|
import * as R from 'ramda';
|
||||||
|
import { transformToMap } from 'utils';
|
||||||
import {
|
import {
|
||||||
ICustomerBalanceSummaryService,
|
ICustomerBalanceSummaryService,
|
||||||
ICustomerBalanceSummaryQuery,
|
ICustomerBalanceSummaryQuery,
|
||||||
ICustomerBalanceSummaryStatement,
|
ICustomerBalanceSummaryStatement,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
|
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
|
||||||
|
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||||
|
|
||||||
export default class CustomerBalanceSummaryService
|
export default class CustomerBalanceSummaryService
|
||||||
implements ICustomerBalanceSummaryService {
|
implements ICustomerBalanceSummaryService {
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
|
||||||
@@ -40,6 +42,41 @@ export default class CustomerBalanceSummaryService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customersBalancesQuery(query) {
|
||||||
|
query.groupBy('contactId');
|
||||||
|
query.sum('credit as credit');
|
||||||
|
query.sum('debit as debit');
|
||||||
|
query.select('contactId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the customers credit/debit totals
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getCustomersCreditDebitTotals(tenantId: number) {
|
||||||
|
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const receivableAccounts = await Account.query().where(
|
||||||
|
'accountType',
|
||||||
|
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
|
||||||
|
);
|
||||||
|
const receivableAccountsIds = map(receivableAccounts, 'id');
|
||||||
|
|
||||||
|
const customersTotals = await AccountTransaction.query().onBuild((query) => {
|
||||||
|
query.whereIn('accountId', receivableAccountsIds);
|
||||||
|
this.customersBalancesQuery(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
return R.compose(
|
||||||
|
(customers) => transformToMap(customers, 'contactId'),
|
||||||
|
(customers) => customers.map((customer) => ({
|
||||||
|
...customer,
|
||||||
|
balance: customer.debit - customer.credit
|
||||||
|
})),
|
||||||
|
)(customersTotals);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the statment of customer balance summary report.
|
* Retrieve the statment of customer balance summary report.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -50,51 +87,38 @@ export default class CustomerBalanceSummaryService
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: ICustomerBalanceSummaryQuery
|
query: ICustomerBalanceSummaryQuery
|
||||||
): Promise<ICustomerBalanceSummaryStatement> {
|
): Promise<ICustomerBalanceSummaryStatement> {
|
||||||
const {
|
|
||||||
accountRepository,
|
|
||||||
transactionsRepository,
|
|
||||||
} = this.tenancy.repositories(tenantId);
|
|
||||||
|
|
||||||
const { Customer } = this.tenancy.models(tenantId);
|
const { Customer } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Settings tenant service.
|
// Settings tenant service.
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
const baseCurrency = settings.get({
|
const baseCurrency = settings.get({
|
||||||
group: 'organization', key: 'base_currency',
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
});
|
});
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
...this.defaultQuery,
|
...this.defaultQuery,
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
this.logger.info('[customer_balance_summary] trying to calculate the report.', {
|
this.logger.info(
|
||||||
filter,
|
'[customer_balance_summary] trying to calculate the report.',
|
||||||
tenantId,
|
{
|
||||||
});
|
filter,
|
||||||
// Retrieve all accounts on the storage.
|
tenantId,
|
||||||
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
|
|
||||||
);
|
);
|
||||||
// Retrieve the customers list ordered by the display name.
|
// Retrieve the customers list ordered by the display name.
|
||||||
const customers = await Customer.query().orderBy('displayName');
|
const customers = await Customer.query().orderBy('displayName');
|
||||||
|
|
||||||
|
// Retrieve the customers debit/credit totals.
|
||||||
|
const customersBalances = await this.getCustomersCreditDebitTotals(tenantId);
|
||||||
|
|
||||||
// Report instance.
|
// Report instance.
|
||||||
const reportInstance = new CustomerBalanceSummaryReport(
|
const reportInstance = new CustomerBalanceSummaryReport(
|
||||||
transactionsJournal,
|
customersBalances,
|
||||||
customers,
|
customers,
|
||||||
filter,
|
filter,
|
||||||
baseCurrency,
|
baseCurrency
|
||||||
);
|
);
|
||||||
// Retrieve the report statement.
|
// Retrieve the report statement.
|
||||||
const reportData = reportInstance.reportData();
|
const reportData = reportInstance.reportData();
|
||||||
|
|||||||
@@ -56,10 +56,20 @@ export default class FinancialSheet {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formates the amount to the percentage string.
|
||||||
|
* @param {number} amount
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
protected formatPercentage(
|
protected formatPercentage(
|
||||||
amount
|
amount
|
||||||
): string {
|
): string {
|
||||||
return `%${amount * 100}`;
|
const percentage = amount * 100;
|
||||||
|
|
||||||
|
return formatNumber(percentage, {
|
||||||
|
symbol: '%',
|
||||||
|
excerptZero: true,
|
||||||
|
money: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { sumBy } from 'lodash';
|
import { sumBy, defaultTo } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ITransactionsByContactsTransaction,
|
ITransactionsByContactsTransaction,
|
||||||
ITransactionsByContactsAmount,
|
ITransactionsByContactsAmount,
|
||||||
ITransactionsByContacts,
|
ITransactionsByContactsFilter,
|
||||||
IContact,
|
IContact,
|
||||||
|
ILedger,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
|
||||||
export default class TransactionsByContact extends FinancialSheet {
|
export default class TransactionsByContact extends FinancialSheet {
|
||||||
readonly contacts: IContact[];
|
readonly contacts: IContact[];
|
||||||
|
readonly ledger: ILedger;
|
||||||
|
readonly filter: ITransactionsByContactsFilter;
|
||||||
|
readonly accountsGraph: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customer transaction mapper.
|
* Customer transaction mapper.
|
||||||
@@ -18,11 +22,13 @@ export default class TransactionsByContact extends FinancialSheet {
|
|||||||
protected contactTransactionMapper(
|
protected contactTransactionMapper(
|
||||||
transaction
|
transaction
|
||||||
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
||||||
|
const account = this.accountsGraph.getNodeData(transaction.accountId);
|
||||||
const currencyCode = 'USD';
|
const currencyCode = 'USD';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credit: this.getContactAmount(transaction.credit, currencyCode),
|
credit: this.getContactAmount(transaction.credit, currencyCode),
|
||||||
debit: this.getContactAmount(transaction.debit, currencyCode),
|
debit: this.getContactAmount(transaction.debit, currencyCode),
|
||||||
|
accountName: account.name,
|
||||||
currencyCode: 'USD',
|
currencyCode: 'USD',
|
||||||
transactionNumber: transaction.transactionNumber,
|
transactionNumber: transaction.transactionNumber,
|
||||||
referenceNumber: transaction.referenceNumber,
|
referenceNumber: transaction.referenceNumber,
|
||||||
@@ -69,8 +75,8 @@ export default class TransactionsByContact extends FinancialSheet {
|
|||||||
): number {
|
): number {
|
||||||
const closingBalance = openingBalance;
|
const closingBalance = openingBalance;
|
||||||
|
|
||||||
const totalCredit = sumBy(customerTransactions, 'credit');
|
const totalCredit = sumBy(customerTransactions, 'credit.amount');
|
||||||
const totalDebit = sumBy(customerTransactions, 'debit');
|
const totalDebit = sumBy(customerTransactions, 'debit.amount');
|
||||||
|
|
||||||
return closingBalance + (totalDebit - totalCredit);
|
return closingBalance + (totalDebit - totalCredit);
|
||||||
}
|
}
|
||||||
@@ -81,7 +87,14 @@ export default class TransactionsByContact extends FinancialSheet {
|
|||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected getContactOpeningBalance(customerId: number): number {
|
protected getContactOpeningBalance(customerId: number): number {
|
||||||
return 0;
|
const openingBalanceLedger = this.ledger
|
||||||
|
.whereContactId(customerId)
|
||||||
|
.whereToDate(this.filter.fromDate);
|
||||||
|
|
||||||
|
// Retrieve the closing balance of the ledger.
|
||||||
|
const openingBalance = openingBalanceLedger.getClosingBalance();
|
||||||
|
|
||||||
|
return defaultTo(openingBalance, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,4 +113,21 @@ export default class TransactionsByContact extends FinancialSheet {
|
|||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { tableMapper, tableRowMapper } from 'utils';
|
import { tableMapper, tableRowMapper } from 'utils';
|
||||||
import {
|
import {
|
||||||
ITransactionsByContactsContact,
|
ITransactionsByContactsContact,
|
||||||
@@ -28,11 +29,12 @@ export default class TransactionsByContactsTableRows {
|
|||||||
): ITableRow[] {
|
): ITableRow[] {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'date', accessor: this.dateAccessor },
|
{ key: 'date', accessor: this.dateAccessor },
|
||||||
{ key: 'account', accessor: 'account.name' },
|
{ key: 'account', accessor: 'accountName' },
|
||||||
{ key: 'referenceType', accessor: 'referenceType' },
|
{ key: 'referenceNumber', accessor: 'referenceNumber' },
|
||||||
{ key: 'transactionType', accessor: 'transactionType' },
|
{ key: 'transactionNumber', accessor: 'transactionNumber' },
|
||||||
{ key: 'credit', accessor: 'credit.formattedAmount' },
|
{ key: 'credit', accessor: 'credit.formattedAmount' },
|
||||||
{ key: 'debit', accessor: 'debit.formattedAmount' },
|
{ key: 'debit', accessor: 'debit.formattedAmount' },
|
||||||
|
{ key: 'runningBalance', accessor: 'runningBalance.formattedAmount' },
|
||||||
];
|
];
|
||||||
return tableMapper(contact.transactions, columns, {
|
return tableMapper(contact.transactions, columns, {
|
||||||
rowTypes: [ROW_TYPE.TRANSACTION],
|
rowTypes: [ROW_TYPE.TRANSACTION],
|
||||||
@@ -49,6 +51,7 @@ export default class TransactionsByContactsTableRows {
|
|||||||
): ITableRow {
|
): ITableRow {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'openingBalanceLabel', value: 'Opening balance' },
|
{ key: 'openingBalanceLabel', value: 'Opening balance' },
|
||||||
|
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||||
{
|
{
|
||||||
key: 'openingBalanceValue',
|
key: 'openingBalanceValue',
|
||||||
accessor: 'openingBalance.formattedAmount',
|
accessor: 'openingBalance.formattedAmount',
|
||||||
@@ -69,6 +72,7 @@ export default class TransactionsByContactsTableRows {
|
|||||||
): ITableRow {
|
): ITableRow {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'closingBalanceLabel', value: 'Closing balance' },
|
{ key: 'closingBalanceLabel', value: 'Closing balance' },
|
||||||
|
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||||
{
|
{
|
||||||
key: 'closingBalanceValue',
|
key: 'closingBalanceValue',
|
||||||
accessor: 'closingBalance.formattedAmount',
|
accessor: 'closingBalance.formattedAmount',
|
||||||
|
|||||||
@@ -11,30 +11,34 @@ import {
|
|||||||
ICustomer,
|
ICustomer,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';
|
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';
|
||||||
|
import Ledger from 'services/Accounting/Ledger';
|
||||||
|
|
||||||
export default class TransactionsByCustomers extends TransactionsByContact {
|
export default class TransactionsByCustomers extends TransactionsByContact {
|
||||||
readonly customers: ICustomer[];
|
readonly customers: ICustomer[];
|
||||||
readonly transactionsByContact: any;
|
readonly ledger: Ledger;
|
||||||
readonly filter: ITransactionsByCustomersFilter;
|
readonly filter: ITransactionsByCustomersFilter;
|
||||||
readonly baseCurrency: string;
|
readonly baseCurrency: string;
|
||||||
readonly numberFormat: INumberFormatQuery;
|
readonly numberFormat: INumberFormatQuery;
|
||||||
|
readonly accountsGraph: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {ICustomer} customers
|
* @param {ICustomer} customers
|
||||||
* @param {Map<number, IAccountTransaction[]>} transactionsByContact
|
* @param {Map<number, IAccountTransaction[]>} transactionsLedger
|
||||||
* @param {string} baseCurrency
|
* @param {string} baseCurrency
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
customers: ICustomer[],
|
customers: ICustomer[],
|
||||||
transactionsByContact: Map<number, IAccountTransaction[]>,
|
accountsGraph: any,
|
||||||
|
ledger: Ledger,
|
||||||
filter: ITransactionsByCustomersFilter,
|
filter: ITransactionsByCustomersFilter,
|
||||||
baseCurrency: string
|
baseCurrency: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.customers = customers;
|
this.customers = customers;
|
||||||
this.transactionsByContact = transactionsByContact;
|
this.accountsGraph = accountsGraph;
|
||||||
|
this.ledger = ledger;
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = baseCurrency;
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
this.numberFormat = this.filter.numberFormat;
|
this.numberFormat = this.filter.numberFormat;
|
||||||
@@ -50,12 +54,17 @@ export default class TransactionsByCustomers extends TransactionsByContact {
|
|||||||
customerId: number,
|
customerId: number,
|
||||||
openingBalance: number
|
openingBalance: number
|
||||||
): ITransactionsByCustomersTransaction[] {
|
): ITransactionsByCustomersTransaction[] {
|
||||||
const transactions = this.transactionsByContact.get(customerId + '') || [];
|
const ledger = this.ledger
|
||||||
|
.whereContactId(customerId)
|
||||||
|
.whereFromDate(this.filter.fromDate)
|
||||||
|
.whereToDate(this.filter.toDate);
|
||||||
|
|
||||||
|
const ledgerEntries = ledger.getEntries();
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
R.curry(this.contactTransactionRunningBalance)(openingBalance),
|
R.curry(this.contactTransactionRunningBalance)(openingBalance),
|
||||||
R.map(this.contactTransactionMapper.bind(this))
|
R.map(this.contactTransactionMapper.bind(this))
|
||||||
).bind(this)(transactions);
|
).bind(this)(ledgerEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,17 +75,17 @@ export default class TransactionsByCustomers extends TransactionsByContact {
|
|||||||
private customerMapper(
|
private customerMapper(
|
||||||
customer: ICustomer
|
customer: ICustomer
|
||||||
): ITransactionsByCustomersCustomer {
|
): ITransactionsByCustomersCustomer {
|
||||||
const openingBalance = this.getContactOpeningBalance(1);
|
const openingBalance = this.getContactOpeningBalance(customer.id);
|
||||||
const transactions = this.customerTransactions(customer.id, openingBalance);
|
const transactions = this.customerTransactions(customer.id, openingBalance);
|
||||||
const closingBalance = this.getContactClosingBalance(transactions, 0);
|
const closingBalance = this.getContactClosingBalance(transactions, openingBalance);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customerName: customer.displayName,
|
customerName: customer.displayName,
|
||||||
openingBalance: this.getContactAmount(
|
openingBalance: this.getTotalAmountMeta(
|
||||||
openingBalance,
|
openingBalance,
|
||||||
customer.currencyCode
|
customer.currencyCode
|
||||||
),
|
),
|
||||||
closingBalance: this.getContactAmount(
|
closingBalance: this.getTotalAmountMeta(
|
||||||
closingBalance,
|
closingBalance,
|
||||||
customer.currencyCode
|
customer.currencyCode
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject } from 'typedi';
|
import { Inject } from 'typedi';
|
||||||
|
import * as R from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { groupBy } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
import {
|
import {
|
||||||
ITransactionsByCustomersService,
|
ITransactionsByCustomersService,
|
||||||
@@ -8,6 +9,9 @@ import {
|
|||||||
ITransactionsByCustomersStatement,
|
ITransactionsByCustomersStatement,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import TransactionsByCustomers from './TransactionsByCustomers';
|
import TransactionsByCustomers from './TransactionsByCustomers';
|
||||||
|
import Ledger from 'services/Accounting/Ledger';
|
||||||
|
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||||
|
import AccountRepository from 'repositories/AccountRepository';
|
||||||
|
|
||||||
export default class TransactionsByCustomersService
|
export default class TransactionsByCustomersService
|
||||||
implements ITransactionsByCustomersService {
|
implements ITransactionsByCustomersService {
|
||||||
@@ -23,8 +27,8 @@ export default class TransactionsByCustomersService
|
|||||||
*/
|
*/
|
||||||
get defaultQuery(): ITransactionsByCustomersFilter {
|
get defaultQuery(): ITransactionsByCustomersFilter {
|
||||||
return {
|
return {
|
||||||
fromDate: moment().format('YYYY-MM-DD'),
|
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
toDate: moment().format('YYYY-MM-DD'),
|
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
numberFormat: {
|
numberFormat: {
|
||||||
precision: 2,
|
precision: 2,
|
||||||
divideOn1000: false,
|
divideOn1000: false,
|
||||||
@@ -40,6 +44,80 @@ export default class TransactionsByCustomersService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the accounts receivable.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getReceivableAccounts(tenantId: number) {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const accounts = await Account.query().where(
|
||||||
|
'accountType',
|
||||||
|
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
|
||||||
|
);
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the customers opening balance transactions.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} openingDate
|
||||||
|
* @param {number} customersIds
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
async getCustomersOpeningBalance(
|
||||||
|
tenantId: number,
|
||||||
|
openingDate: Date,
|
||||||
|
customersIds?: number[]
|
||||||
|
): Promise<ILedgerEntry[]> {
|
||||||
|
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const receivableAccounts = await this.getReceivableAccounts(tenantId);
|
||||||
|
const receivableAccountsIds = map(receivableAccounts, 'id');
|
||||||
|
|
||||||
|
const openingTransactions = await AccountTransaction.query().modify(
|
||||||
|
'contactsOpeningBalance',
|
||||||
|
openingDate,
|
||||||
|
receivableAccountsIds,
|
||||||
|
customersIds
|
||||||
|
);
|
||||||
|
return R.compose(
|
||||||
|
R.map(R.assoc('date', openingDate)),
|
||||||
|
R.map(R.assoc('accountNormal', 'debit'))
|
||||||
|
)(openingTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {Date|string} openingDate
|
||||||
|
* @param {number[]} customersIds
|
||||||
|
*/
|
||||||
|
async getCustomersPeriodTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
fromDate: Date,
|
||||||
|
toDate: Date,
|
||||||
|
): Promise<ILedgerEntry[]> {
|
||||||
|
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const receivableAccounts = await this.getReceivableAccounts(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', 'debit')))(transactions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve transactions by by the customers.
|
* Retrieve transactions by by the customers.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -50,8 +128,8 @@ export default class TransactionsByCustomersService
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: ITransactionsByCustomersFilter
|
query: ITransactionsByCustomersFilter
|
||||||
): Promise<ITransactionsByCustomersStatement> {
|
): Promise<ITransactionsByCustomersStatement> {
|
||||||
const { transactionsRepository } = this.tenancy.repositories(tenantId);
|
|
||||||
const { Customer } = this.tenancy.models(tenantId);
|
const { Customer } = this.tenancy.models(tenantId);
|
||||||
|
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
// Settings tenant service.
|
// Settings tenant service.
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
@@ -64,21 +142,35 @@ export default class TransactionsByCustomersService
|
|||||||
...this.defaultQuery,
|
...this.defaultQuery,
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||||
const customers = await Customer.query().orderBy('displayName');
|
const customers = await Customer.query().orderBy('displayName');
|
||||||
|
|
||||||
// Retrieve all journal transactions based on the given query.
|
const openingBalanceDate = moment(filter.fromDate).subtract(1, 'days').toDate();
|
||||||
const transactions = await transactionsRepository.journal({
|
|
||||||
fromDate: query.fromDate,
|
// Retrieve all ledger transactions of the opening balance of.
|
||||||
toDate: query.toDate,
|
const openingBalanceTransactions = await this.getCustomersOpeningBalance(
|
||||||
});
|
tenantId,
|
||||||
// Transactions map by contact id.
|
openingBalanceDate,
|
||||||
const transactionsMap = new Map(
|
|
||||||
Object.entries(groupBy(transactions, 'contactId'))
|
|
||||||
);
|
);
|
||||||
|
// Retrieve all ledger transactions between opeing and closing period.
|
||||||
|
const customersTransactions = await this.getCustomersPeriodTransactions(
|
||||||
|
tenantId,
|
||||||
|
query.fromDate,
|
||||||
|
query.toDate,
|
||||||
|
);
|
||||||
|
// Concats the opening balance and period customer ledger transactions.
|
||||||
|
const journalTransactions = [
|
||||||
|
...openingBalanceTransactions,
|
||||||
|
...customersTransactions,
|
||||||
|
];
|
||||||
|
const journal = new Ledger(journalTransactions);
|
||||||
|
|
||||||
// Transactions by customers data mapper.
|
// Transactions by customers data mapper.
|
||||||
const reportInstance = new TransactionsByCustomers(
|
const reportInstance = new TransactionsByCustomers(
|
||||||
customers,
|
customers,
|
||||||
transactionsMap,
|
accountsGraph,
|
||||||
|
journal,
|
||||||
filter,
|
filter,
|
||||||
baseCurrency
|
baseCurrency
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -242,10 +242,11 @@ const formatNumber = (
|
|||||||
decimal = '.',
|
decimal = '.',
|
||||||
zeroSign = '',
|
zeroSign = '',
|
||||||
money = true,
|
money = true,
|
||||||
currencyCode
|
currencyCode,
|
||||||
|
symbol
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const symbol = getCurrencySign(currencyCode);
|
const formattedSymbol = getCurrencySign(currencyCode);
|
||||||
const negForamt = getNegativeFormat(negativeFormat);
|
const negForamt = getNegativeFormat(negativeFormat);
|
||||||
const format = '%s%v';
|
const format = '%s%v';
|
||||||
|
|
||||||
@@ -256,7 +257,7 @@ const formatNumber = (
|
|||||||
}
|
}
|
||||||
return accounting.formatMoney(
|
return accounting.formatMoney(
|
||||||
formattedBalance,
|
formattedBalance,
|
||||||
money ? symbol : '',
|
money ? formattedSymbol : symbol ? symbol : '',
|
||||||
precision,
|
precision,
|
||||||
thousand,
|
thousand,
|
||||||
decimal,
|
decimal,
|
||||||
|
|||||||
Reference in New Issue
Block a user