WIP: transactions by customers.

This commit is contained in:
a.bouhuolia
2021-05-07 23:49:38 +02:00
parent 5f2e90b234
commit b5ed7af7eb
16 changed files with 425 additions and 76 deletions

View File

@@ -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,

View 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;
}

View File

@@ -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;

View File

@@ -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';

View File

@@ -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');
}
}; };
} }

View File

@@ -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;
});
}
} }

View 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,
}
}
}

View File

@@ -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,
}; };
} }

View File

@@ -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),
}; };
} }

View File

@@ -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();

View File

@@ -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,
})
} }
} }

View File

@@ -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,
};
}
} }

View File

@@ -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',

View File

@@ -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
), ),

View File

@@ -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
); );

View File

@@ -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,