mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +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 TransactionsByVendorsService from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
||||
import { ITransactionsByVendorsStatement } from 'interfaces';
|
||||
|
||||
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByVendorsService: TransactionsByVendorsService;
|
||||
@@ -44,7 +45,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
transformToTableRows({ data }: ITransactionsByVendorsStatement) {
|
||||
private transformToTableRows({ data }: ITransactionsByVendorsStatement) {
|
||||
return {
|
||||
table: {
|
||||
data: this.transactionsByVendorsTableRows.tableRows(data),
|
||||
@@ -56,7 +57,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
* Transformes the report statement to json response.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
transformToJsonResponse({
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
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,
|
||||
credit: ITransactionsByContactsAmount;
|
||||
debit: ITransactionsByContactsAmount;
|
||||
accountName: string,
|
||||
runningBalance: ITransactionsByContactsAmount;
|
||||
currencyCode: string;
|
||||
referenceNumber: string;
|
||||
|
||||
@@ -49,4 +49,5 @@ export * from './ContactBalanceSummary';
|
||||
export * from './TransactionsByCustomers';
|
||||
export * from './TransactionsByContacts';
|
||||
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('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) {
|
||||
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 {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount),
|
||||
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet {
|
||||
protected getTotalFormat(amount: number): IContactBalanceSummaryAmount {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount),
|
||||
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { get } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IJournalPoster,
|
||||
@@ -45,11 +45,12 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||
* @returns {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 {
|
||||
customerName: customer.displayName,
|
||||
total: this.getContactTotalFormat(balance),
|
||||
total: this.getContactTotalFormat(balanceAmount),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { map } from 'lodash';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import * as R from 'ramda';
|
||||
import { transformToMap } from 'utils';
|
||||
import {
|
||||
ICustomerBalanceSummaryService,
|
||||
ICustomerBalanceSummaryQuery,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
} from 'interfaces';
|
||||
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
|
||||
export default class CustomerBalanceSummaryService
|
||||
implements ICustomerBalanceSummaryService {
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@@ -40,61 +42,83 @@ 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.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerBalanceSummaryQuery} query
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerBalanceSummaryQuery} query
|
||||
* @return {Promise<ICustomerBalanceSummaryStatement>}
|
||||
*/
|
||||
async customerBalanceSummary(
|
||||
tenantId: number,
|
||||
query: ICustomerBalanceSummaryQuery
|
||||
): Promise<ICustomerBalanceSummaryStatement> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const { Customer } = 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,
|
||||
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
|
||||
this.logger.info(
|
||||
'[customer_balance_summary] trying to calculate the report.',
|
||||
{
|
||||
filter,
|
||||
tenantId,
|
||||
}
|
||||
);
|
||||
// Retrieve the customers list ordered by the display name.
|
||||
const customers = await Customer.query().orderBy('displayName');
|
||||
|
||||
// Retrieve the customers debit/credit totals.
|
||||
const customersBalances = await this.getCustomersCreditDebitTotals(tenantId);
|
||||
|
||||
// Report instance.
|
||||
const reportInstance = new CustomerBalanceSummaryReport(
|
||||
transactionsJournal,
|
||||
customersBalances,
|
||||
customers,
|
||||
filter,
|
||||
baseCurrency,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve the report statement.
|
||||
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(
|
||||
amount
|
||||
): 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 {
|
||||
ITransactionsByContactsTransaction,
|
||||
ITransactionsByContactsAmount,
|
||||
ITransactionsByContacts,
|
||||
ITransactionsByContactsFilter,
|
||||
IContact,
|
||||
ILedger,
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class TransactionsByContact extends FinancialSheet {
|
||||
readonly contacts: IContact[];
|
||||
readonly ledger: ILedger;
|
||||
readonly filter: ITransactionsByContactsFilter;
|
||||
readonly accountsGraph: any;
|
||||
|
||||
/**
|
||||
* Customer transaction mapper.
|
||||
@@ -18,11 +22,13 @@ export default class TransactionsByContact extends FinancialSheet {
|
||||
protected contactTransactionMapper(
|
||||
transaction
|
||||
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
||||
const account = this.accountsGraph.getNodeData(transaction.accountId);
|
||||
const currencyCode = 'USD';
|
||||
|
||||
return {
|
||||
credit: this.getContactAmount(transaction.credit, currencyCode),
|
||||
debit: this.getContactAmount(transaction.debit, currencyCode),
|
||||
accountName: account.name,
|
||||
currencyCode: 'USD',
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
referenceNumber: transaction.referenceNumber,
|
||||
@@ -69,8 +75,8 @@ export default class TransactionsByContact extends FinancialSheet {
|
||||
): number {
|
||||
const closingBalance = openingBalance;
|
||||
|
||||
const totalCredit = sumBy(customerTransactions, 'credit');
|
||||
const totalDebit = sumBy(customerTransactions, 'debit');
|
||||
const totalCredit = sumBy(customerTransactions, 'credit.amount');
|
||||
const totalDebit = sumBy(customerTransactions, 'debit.amount');
|
||||
|
||||
return closingBalance + (totalDebit - totalCredit);
|
||||
}
|
||||
@@ -81,7 +87,14 @@ export default class TransactionsByContact extends FinancialSheet {
|
||||
* @returns {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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 * as R from 'ramda';
|
||||
import { tableMapper, tableRowMapper } from 'utils';
|
||||
import {
|
||||
ITransactionsByContactsContact,
|
||||
@@ -28,11 +29,12 @@ export default class TransactionsByContactsTableRows {
|
||||
): ITableRow[] {
|
||||
const columns = [
|
||||
{ key: 'date', accessor: this.dateAccessor },
|
||||
{ key: 'account', accessor: 'account.name' },
|
||||
{ key: 'referenceType', accessor: 'referenceType' },
|
||||
{ key: 'transactionType', accessor: 'transactionType' },
|
||||
{ key: 'account', accessor: 'accountName' },
|
||||
{ key: 'referenceNumber', accessor: 'referenceNumber' },
|
||||
{ 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],
|
||||
@@ -49,6 +51,7 @@ export default class TransactionsByContactsTableRows {
|
||||
): ITableRow {
|
||||
const columns = [
|
||||
{ key: 'openingBalanceLabel', value: 'Opening balance' },
|
||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||
{
|
||||
key: 'openingBalanceValue',
|
||||
accessor: 'openingBalance.formattedAmount',
|
||||
@@ -69,6 +72,7 @@ export default class TransactionsByContactsTableRows {
|
||||
): ITableRow {
|
||||
const columns = [
|
||||
{ key: 'closingBalanceLabel', value: 'Closing balance' },
|
||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||
{
|
||||
key: 'closingBalanceValue',
|
||||
accessor: 'closingBalance.formattedAmount',
|
||||
|
||||
@@ -11,30 +11,34 @@ import {
|
||||
ICustomer,
|
||||
} from 'interfaces';
|
||||
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
|
||||
export default class TransactionsByCustomers extends TransactionsByContact {
|
||||
readonly customers: ICustomer[];
|
||||
readonly transactionsByContact: any;
|
||||
readonly ledger: Ledger;
|
||||
readonly filter: ITransactionsByCustomersFilter;
|
||||
readonly baseCurrency: string;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
readonly accountsGraph: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ICustomer} customers
|
||||
* @param {Map<number, IAccountTransaction[]>} transactionsByContact
|
||||
* @param {Map<number, IAccountTransaction[]>} transactionsLedger
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
customers: ICustomer[],
|
||||
transactionsByContact: Map<number, IAccountTransaction[]>,
|
||||
accountsGraph: any,
|
||||
ledger: Ledger,
|
||||
filter: ITransactionsByCustomersFilter,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.customers = customers;
|
||||
this.transactionsByContact = transactionsByContact;
|
||||
this.accountsGraph = accountsGraph;
|
||||
this.ledger = ledger;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.filter = filter;
|
||||
this.numberFormat = this.filter.numberFormat;
|
||||
@@ -50,12 +54,17 @@ export default class TransactionsByCustomers extends TransactionsByContact {
|
||||
customerId: number,
|
||||
openingBalance: number
|
||||
): ITransactionsByCustomersTransaction[] {
|
||||
const transactions = this.transactionsByContact.get(customerId + '') || [];
|
||||
const ledger = this.ledger
|
||||
.whereContactId(customerId)
|
||||
.whereFromDate(this.filter.fromDate)
|
||||
.whereToDate(this.filter.toDate);
|
||||
|
||||
return R.compose(
|
||||
const ledgerEntries = ledger.getEntries();
|
||||
|
||||
return R.compose(
|
||||
R.curry(this.contactTransactionRunningBalance)(openingBalance),
|
||||
R.map(this.contactTransactionMapper.bind(this))
|
||||
).bind(this)(transactions);
|
||||
).bind(this)(ledgerEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,17 +75,17 @@ export default class TransactionsByCustomers extends TransactionsByContact {
|
||||
private customerMapper(
|
||||
customer: ICustomer
|
||||
): ITransactionsByCustomersCustomer {
|
||||
const openingBalance = this.getContactOpeningBalance(1);
|
||||
const openingBalance = this.getContactOpeningBalance(customer.id);
|
||||
const transactions = this.customerTransactions(customer.id, openingBalance);
|
||||
const closingBalance = this.getContactClosingBalance(transactions, 0);
|
||||
const closingBalance = this.getContactClosingBalance(transactions, openingBalance);
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
openingBalance: this.getContactAmount(
|
||||
openingBalance: this.getTotalAmountMeta(
|
||||
openingBalance,
|
||||
customer.currencyCode
|
||||
),
|
||||
closingBalance: this.getContactAmount(
|
||||
closingBalance: this.getTotalAmountMeta(
|
||||
closingBalance,
|
||||
customer.currencyCode
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { groupBy } from 'lodash';
|
||||
import { map } from 'lodash';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
ITransactionsByCustomersService,
|
||||
@@ -8,6 +9,9 @@ import {
|
||||
ITransactionsByCustomersStatement,
|
||||
} from 'interfaces';
|
||||
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
|
||||
implements ITransactionsByCustomersService {
|
||||
@@ -23,8 +27,8 @@ export default class TransactionsByCustomersService
|
||||
*/
|
||||
get defaultQuery(): ITransactionsByCustomersFilter {
|
||||
return {
|
||||
fromDate: moment().format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
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.
|
||||
* @param {number} tenantId
|
||||
@@ -50,8 +128,8 @@ export default class TransactionsByCustomersService
|
||||
tenantId: number,
|
||||
query: ITransactionsByCustomersFilter
|
||||
): Promise<ITransactionsByCustomersStatement> {
|
||||
const { transactionsRepository } = this.tenancy.repositories(tenantId);
|
||||
const { Customer } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
@@ -64,21 +142,35 @@ export default class TransactionsByCustomersService
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
const customers = await Customer.query().orderBy('displayName');
|
||||
|
||||
// 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'))
|
||||
const openingBalanceDate = moment(filter.fromDate).subtract(1, 'days').toDate();
|
||||
|
||||
// Retrieve all ledger transactions of the opening balance of.
|
||||
const openingBalanceTransactions = await this.getCustomersOpeningBalance(
|
||||
tenantId,
|
||||
openingBalanceDate,
|
||||
);
|
||||
// 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.
|
||||
const reportInstance = new TransactionsByCustomers(
|
||||
customers,
|
||||
transactionsMap,
|
||||
accountsGraph,
|
||||
journal,
|
||||
filter,
|
||||
baseCurrency
|
||||
);
|
||||
|
||||
@@ -242,10 +242,11 @@ const formatNumber = (
|
||||
decimal = '.',
|
||||
zeroSign = '',
|
||||
money = true,
|
||||
currencyCode
|
||||
currencyCode,
|
||||
symbol
|
||||
}
|
||||
) => {
|
||||
const symbol = getCurrencySign(currencyCode);
|
||||
const formattedSymbol = getCurrencySign(currencyCode);
|
||||
const negForamt = getNegativeFormat(negativeFormat);
|
||||
const format = '%s%v';
|
||||
|
||||
@@ -256,7 +257,7 @@ const formatNumber = (
|
||||
}
|
||||
return accounting.formatMoney(
|
||||
formattedBalance,
|
||||
money ? symbol : '',
|
||||
money ? formattedSymbol : symbol ? symbol : '',
|
||||
precision,
|
||||
thousand,
|
||||
decimal,
|
||||
|
||||
Reference in New Issue
Block a user