mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
add server to monorepo.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { InventoryTransactionsWarehouses } from './AcountsTransactionsWarehouses';
|
||||
import { IBranchesActivatedPayload } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class AccountsTransactionsWarehousesSubscribe {
|
||||
@Inject()
|
||||
accountsTransactionsWarehouses: InventoryTransactionsWarehouses;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach = (bus) => {
|
||||
bus.subscribe(
|
||||
events.branch.onActivated,
|
||||
this.updateGLTransactionsToPrimaryBranchOnActivated
|
||||
);
|
||||
return bus;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates all GL transactions to primary branch once
|
||||
* the multi-branches activated.
|
||||
* @param {IBranchesActivatedPayload}
|
||||
*/
|
||||
private updateGLTransactionsToPrimaryBranchOnActivated = async ({
|
||||
tenantId,
|
||||
primaryBranch,
|
||||
trx,
|
||||
}: IBranchesActivatedPayload) => {
|
||||
await this.accountsTransactionsWarehouses.updateTransactionsWithWarehouse(
|
||||
tenantId,
|
||||
primaryBranch.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class InventoryTransactionsWarehouses {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Updates all accounts transctions with the priamry branch.
|
||||
* @param tenantId
|
||||
* @param primaryBranchId
|
||||
*/
|
||||
public updateTransactionsWithWarehouse = async (
|
||||
tenantId: number,
|
||||
primaryBranchId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { AccountTransaction } = await this.tenancy.models(tenantId);
|
||||
|
||||
await AccountTransaction.query(trx).update({
|
||||
branchId: primaryBranchId,
|
||||
});
|
||||
};
|
||||
}
|
||||
71
packages/server/src/services/Accounting/JournalCommands.ts
Normal file
71
packages/server/src/services/Accounting/JournalCommands.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import moment from 'moment';
|
||||
import { castArray, sumBy, toArray } from 'lodash';
|
||||
import { IBill, ISystemUser, IAccount } from '@/interfaces';
|
||||
import JournalPoster from './JournalPoster';
|
||||
import JournalEntry from './JournalEntry';
|
||||
import { IExpense, IExpenseCategory } from '@/interfaces';
|
||||
import { increment } from 'utils';
|
||||
export default class JournalCommands {
|
||||
journal: JournalPoster;
|
||||
models: any;
|
||||
repositories: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {JournalPoster} journal -
|
||||
*/
|
||||
constructor(journal: JournalPoster) {
|
||||
this.journal = journal;
|
||||
|
||||
this.repositories = this.journal.repositories;
|
||||
this.models = this.journal.models;
|
||||
}
|
||||
/**
|
||||
* Reverts the jouranl entries.
|
||||
* @param {number|number[]} referenceId - Reference id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
*/
|
||||
async revertJournalEntries(
|
||||
referenceId: number | number[],
|
||||
referenceType: string | string[]
|
||||
) {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', castArray(referenceType))
|
||||
.whereIn('reference_id', castArray(referenceId))
|
||||
.withGraphFetched('account');
|
||||
|
||||
this.journal.fromTransactions(transactions);
|
||||
this.journal.removeEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the sale invoice cost journal entries.
|
||||
* @param {Date|string} startingDate
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async revertInventoryCostJournalEntries(
|
||||
startingDate: Date | string
|
||||
): Promise<void> {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
this.journal.fromTransactions(transactions);
|
||||
this.journal.removeEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts sale invoice the income journal entries.
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
async revertInvoiceIncomeEntries(saleInvoiceId: number) {
|
||||
const { transactionsRepository } = this.repositories;
|
||||
|
||||
const transactions = await transactionsRepository.journal({
|
||||
referenceType: ['SaleInvoice'],
|
||||
referenceId: [saleInvoiceId],
|
||||
});
|
||||
this.journal.fromTransactions(transactions);
|
||||
this.journal.removeEntries();
|
||||
}
|
||||
}
|
||||
74
packages/server/src/services/Accounting/JournalContacts.ts
Normal file
74
packages/server/src/services/Accounting/JournalContacts.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import async from 'async';
|
||||
|
||||
export default class JournalContacts {
|
||||
saveContactBalanceQueue: any;
|
||||
contactsBalanceTable: {
|
||||
[key: number]: { credit: number; debit: number };
|
||||
} = {};
|
||||
|
||||
constructor(journal) {
|
||||
this.journal = journal;
|
||||
this.saveContactBalanceQueue = async.queue(
|
||||
this.saveContactBalanceChangeTask.bind(this),
|
||||
10
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Sets the contact balance change.
|
||||
*/
|
||||
private getContactsBalanceChanges(entry) {
|
||||
if (!entry.contactId) {
|
||||
return;
|
||||
}
|
||||
const change = {
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
};
|
||||
if (!this.contactsBalanceTable[entry.contactId]) {
|
||||
this.contactsBalanceTable[entry.contactId] = { credit: 0, debit: 0 };
|
||||
}
|
||||
if (change.credit) {
|
||||
this.contactsBalanceTable[entry.contactId].credit += change.credit;
|
||||
}
|
||||
if (change.debit) {
|
||||
this.contactsBalanceTable[entry.contactId].debit += change.debit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save contacts balance change.
|
||||
*/
|
||||
saveContactsBalance() {
|
||||
const balanceChanges = Object.entries(
|
||||
this.contactsBalanceTable
|
||||
).map(([contactId, { credit, debit }]) => ({ contactId, credit, debit }));
|
||||
|
||||
return this.saveContactBalanceQueue.pushAsync(balanceChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves contact balance change task.
|
||||
* @param {number} contactId - Contact id.
|
||||
* @param {number} credit - Credit amount.
|
||||
* @param {number} debit - Debit amount.
|
||||
*/
|
||||
async saveContactBalanceChangeTask({ contactId, credit, debit }, callback) {
|
||||
const { contactRepository } = this.repositories;
|
||||
|
||||
const contact = await contactRepository.findOneById(contactId);
|
||||
let balanceChange = 0;
|
||||
|
||||
if (contact.contactNormal === 'credit') {
|
||||
balanceChange += credit - debit;
|
||||
} else {
|
||||
balanceChange += debit - credit;
|
||||
}
|
||||
// Contact change balance.
|
||||
await contactRepository.changeNumber(
|
||||
{ id: contactId },
|
||||
'balance',
|
||||
balanceChange
|
||||
);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
10
packages/server/src/services/Accounting/JournalEntry.ts
Normal file
10
packages/server/src/services/Accounting/JournalEntry.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export default class JournalEntry {
|
||||
constructor(entry) {
|
||||
const defaults = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
};
|
||||
this.entry = { ...defaults, ...entry };
|
||||
}
|
||||
}
|
||||
17
packages/server/src/services/Accounting/JournalFinancial.ts
Normal file
17
packages/server/src/services/Accounting/JournalFinancial.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import moment from 'moment';
|
||||
import { IJournalPoster } from '@/interfaces';
|
||||
|
||||
export default class JournalFinancial {
|
||||
journal: IJournalPoster;
|
||||
|
||||
accountsDepGraph: any;
|
||||
|
||||
/**
|
||||
* Journal poster.
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(journal: IJournalPoster) {
|
||||
this.journal = journal;
|
||||
this.accountsDepGraph = this.journal.accountsDepGraph;
|
||||
}
|
||||
}
|
||||
759
packages/server/src/services/Accounting/JournalPoster.ts
Normal file
759
packages/server/src/services/Accounting/JournalPoster.ts
Normal file
@@ -0,0 +1,759 @@
|
||||
import { omit, get, chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Container } from 'typedi';
|
||||
import async from 'async';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
IJournalEntry,
|
||||
IJournalPoster,
|
||||
IAccountChange,
|
||||
IAccountsChange,
|
||||
TEntryType,
|
||||
} from '@/interfaces';
|
||||
import Knex from 'knex';
|
||||
|
||||
const CONTACTS_CONFIG = [
|
||||
{
|
||||
accountBySlug: 'accounts-receivable',
|
||||
contactService: 'customer',
|
||||
assignRequired: true,
|
||||
},
|
||||
{
|
||||
accountBySlug: 'accounts-payable',
|
||||
contactService: 'vendor',
|
||||
assignRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default class JournalPoster implements IJournalPoster {
|
||||
tenantId: number;
|
||||
tenancy: TenancyService;
|
||||
logger: any;
|
||||
models: any;
|
||||
repositories: any;
|
||||
|
||||
deletedEntriesIds: number[] = [];
|
||||
entries: IJournalEntry[] = [];
|
||||
balancesChange: IAccountsChange = {};
|
||||
accountsDepGraph: IAccountsChange;
|
||||
|
||||
accountsBalanceTable: { [key: number]: number } = {};
|
||||
contactsBalanceTable: {
|
||||
[key: number]: { credit: number; debit: number }[];
|
||||
} = {};
|
||||
saveContactBalanceQueue: any;
|
||||
|
||||
/**
|
||||
* Journal poster constructor.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
constructor(tenantId: number, accountsGraph?: any, trx?: Knex.Transaction) {
|
||||
this.initTenancy();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.repositories = this.tenancy.repositories(tenantId);
|
||||
|
||||
if (accountsGraph) {
|
||||
this.accountsDepGraph = accountsGraph;
|
||||
}
|
||||
this.trx = trx;
|
||||
this.saveContactBalanceQueue = async.queue(
|
||||
this.saveContactBalanceChangeTask.bind(this),
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial tenancy.
|
||||
* @private
|
||||
*/
|
||||
private initTenancy() {
|
||||
try {
|
||||
this.tenancy = Container.get(TenancyService);
|
||||
this.logger = Container.get('logger');
|
||||
} catch (exception) {
|
||||
throw new Error('Should execute this class inside tenancy area.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initialize acccounts dependency graph.
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async initAccountsDepGraph(): Promise<void> {
|
||||
const { accountRepository } = this.repositories;
|
||||
|
||||
if (!this.accountsDepGraph) {
|
||||
const accountsDepGraph = await accountRepository.getDependencyGraph();
|
||||
this.accountsDepGraph = accountsDepGraph;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the ledger is empty.
|
||||
*/
|
||||
public isEmpty() {
|
||||
return this.entries.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the credit entry for the given account.
|
||||
* @param {IJournalEntry} entry -
|
||||
*/
|
||||
public credit(entryModel: IJournalEntry): void {
|
||||
if (entryModel instanceof JournalEntry === false) {
|
||||
throw new Error('The entry is not instance of JournalEntry.');
|
||||
}
|
||||
this.entries.push(entryModel.entry);
|
||||
this.setAccountBalanceChange(entryModel.entry);
|
||||
this.setContactBalanceChange(entryModel.entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the debit entry for the given account.
|
||||
* @param {JournalEntry} entry -
|
||||
*/
|
||||
public debit(entryModel: IJournalEntry): void {
|
||||
if (entryModel instanceof JournalEntry === false) {
|
||||
throw new Error('The entry is not instance of JournalEntry.');
|
||||
}
|
||||
this.entries.push(entryModel.entry);
|
||||
this.setAccountBalanceChange(entryModel.entry);
|
||||
this.setContactBalanceChange(entryModel.entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the contact balance change.
|
||||
*/
|
||||
private setContactBalanceChange(entry) {
|
||||
if (!entry.contactId) {
|
||||
return;
|
||||
}
|
||||
const change = {
|
||||
debit: entry.debit || 0,
|
||||
credit: entry.credit || 0,
|
||||
account: entry.account,
|
||||
};
|
||||
if (!this.contactsBalanceTable[entry.contactId]) {
|
||||
this.contactsBalanceTable[entry.contactId] = [];
|
||||
}
|
||||
this.contactsBalanceTable[entry.contactId].push(change);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save contacts balance change.
|
||||
*/
|
||||
async saveContactsBalance() {
|
||||
await this.initAccountsDepGraph();
|
||||
|
||||
const balanceChanges = Object.entries(this.contactsBalanceTable).map(
|
||||
([contactId, entries]) => ({
|
||||
contactId,
|
||||
entries: entries.filter((entry) => {
|
||||
const account = this.accountsDepGraph.getNodeData(entry.account);
|
||||
|
||||
return (
|
||||
account &&
|
||||
CONTACTS_CONFIG.some((config) => {
|
||||
return config.accountBySlug === account.slug;
|
||||
})
|
||||
);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const balanceEntries = chain(balanceChanges)
|
||||
.map((change) =>
|
||||
change.entries.map((entry) => ({
|
||||
...entry,
|
||||
contactId: change.contactId,
|
||||
}))
|
||||
)
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
return this.saveContactBalanceQueue.pushAsync(balanceEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves contact balance change task.
|
||||
* @param {number} contactId - Contact id.
|
||||
* @param {number} credit - Credit amount.
|
||||
* @param {number} debit - Debit amount.
|
||||
*/
|
||||
async saveContactBalanceChangeTask({ contactId, credit, debit }) {
|
||||
const { contactRepository } = this.repositories;
|
||||
|
||||
const contact = await contactRepository.findOneById(contactId);
|
||||
let balanceChange = 0;
|
||||
|
||||
if (contact.contactNormal === 'credit') {
|
||||
balanceChange += credit - debit;
|
||||
} else {
|
||||
balanceChange += debit - credit;
|
||||
}
|
||||
// Contact change balance.
|
||||
await contactRepository.changeNumber(
|
||||
{ id: contactId },
|
||||
'balance',
|
||||
balanceChange,
|
||||
this.trx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets account balance change.
|
||||
* @param {JournalEntry} entry
|
||||
* @param {String} type
|
||||
*/
|
||||
private setAccountBalanceChange(entry: IJournalEntry): void {
|
||||
const accountChange: IAccountChange = {
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
};
|
||||
this._setAccountBalanceChange(entry.account, accountChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets account balance change.
|
||||
* @private
|
||||
* @param {number} accountId -
|
||||
* @param {IAccountChange} accountChange
|
||||
*/
|
||||
private _setAccountBalanceChange(
|
||||
accountId: number,
|
||||
accountChange: IAccountChange
|
||||
) {
|
||||
this.balancesChange = this.accountBalanceChangeReducer(
|
||||
this.balancesChange,
|
||||
accountId,
|
||||
accountChange
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts balance change reducer.
|
||||
* @param {IAccountsChange} balancesChange
|
||||
* @param {number} accountId
|
||||
* @param {IAccountChange} accountChange
|
||||
* @return {IAccountChange}
|
||||
*/
|
||||
private accountBalanceChangeReducer(
|
||||
balancesChange: IAccountsChange,
|
||||
accountId: number,
|
||||
accountChange: IAccountChange
|
||||
) {
|
||||
const change = { ...balancesChange };
|
||||
|
||||
if (!change[accountId]) {
|
||||
change[accountId] = { credit: 0, debit: 0 };
|
||||
}
|
||||
if (accountChange.credit) {
|
||||
change[accountId].credit += accountChange.credit;
|
||||
}
|
||||
if (accountChange.debit) {
|
||||
change[accountId].debit += accountChange.debit;
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts balance changes to array.
|
||||
* @private
|
||||
* @param {IAccountsChange} accountsChange -
|
||||
* @return {Promise<{ account: number, change: number }>}
|
||||
*/
|
||||
private async convertBalanceChangesToArr(
|
||||
accountsChange: IAccountsChange
|
||||
): Promise<{ account: number; change: number }[]> {
|
||||
const mappedList: { account: number; change: number }[] = [];
|
||||
const accountsIds: number[] = Object.keys(accountsChange).map((id) =>
|
||||
parseInt(id, 10)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
accountsIds.map(async (account: number) => {
|
||||
const accountChange = accountsChange[account];
|
||||
const accountNode = this.accountsDepGraph.getNodeData(account);
|
||||
const normal = accountNode.accountNormal;
|
||||
|
||||
let change = 0;
|
||||
|
||||
if (accountChange.credit) {
|
||||
change +=
|
||||
normal === 'credit'
|
||||
? accountChange.credit
|
||||
: -1 * accountChange.credit;
|
||||
}
|
||||
if (accountChange.debit) {
|
||||
change +=
|
||||
normal === 'debit' ? accountChange.debit : -1 * accountChange.debit;
|
||||
}
|
||||
mappedList.push({ account, change });
|
||||
})
|
||||
);
|
||||
return mappedList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the balance change of journal entries.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async saveBalance() {
|
||||
await this.initAccountsDepGraph();
|
||||
|
||||
const { Account } = this.models;
|
||||
const accountsChange = this.balanceChangeWithDepends(this.balancesChange);
|
||||
const balancesList = await this.convertBalanceChangesToArr(accountsChange);
|
||||
const balancesAccounts = balancesList.map((b) => b.account);
|
||||
|
||||
// Ensure the accounts has atleast zero in amount.
|
||||
await Account.query(this.trx)
|
||||
.where('amount', null)
|
||||
.whereIn('id', balancesAccounts)
|
||||
.patch({ amount: 0 });
|
||||
|
||||
const balanceUpdateOpers: Promise<void>[] = [];
|
||||
|
||||
balancesList.forEach((balance: { account: number; change: number }) => {
|
||||
const method: string = balance.change < 0 ? 'decrement' : 'increment';
|
||||
|
||||
this.logger.info(
|
||||
'[journal_poster] increment/decrement account balance.',
|
||||
{
|
||||
balance,
|
||||
tenantId: this.tenantId,
|
||||
}
|
||||
);
|
||||
const query = Account.query(this.trx)
|
||||
[method]('amount', Math.abs(balance.change))
|
||||
.where('id', balance.account);
|
||||
|
||||
balanceUpdateOpers.push(query);
|
||||
});
|
||||
|
||||
await Promise.all(balanceUpdateOpers);
|
||||
this.resetAccountsBalanceChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes all accounts that dependencies of changed accounts.
|
||||
* @param {IAccountsChange} accountsChange
|
||||
* @returns {IAccountsChange}
|
||||
*/
|
||||
private balanceChangeWithDepends(
|
||||
accountsChange: IAccountsChange
|
||||
): IAccountsChange {
|
||||
const accountsIds = Object.keys(accountsChange);
|
||||
let changes: IAccountsChange = {};
|
||||
|
||||
accountsIds.forEach((accountId) => {
|
||||
const accountChange = accountsChange[accountId];
|
||||
const depAccountsIds = this.accountsDepGraph.dependantsOf(accountId);
|
||||
|
||||
[accountId, ...depAccountsIds].forEach((account) => {
|
||||
changes = this.accountBalanceChangeReducer(
|
||||
changes,
|
||||
account,
|
||||
accountChange
|
||||
);
|
||||
});
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets accounts balance change.
|
||||
* @private
|
||||
*/
|
||||
private resetAccountsBalanceChange() {
|
||||
this.balancesChange = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the stacked journal entries to the storage.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async saveEntries() {
|
||||
const { transactionsRepository } = this.repositories;
|
||||
const saveOperations: Promise<void>[] = [];
|
||||
|
||||
this.logger.info('[journal] trying to insert accounts transactions.');
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
const oper = transactionsRepository.create(
|
||||
{
|
||||
accountId: entry.account,
|
||||
...omit(entry, ['account']),
|
||||
},
|
||||
this.trx
|
||||
);
|
||||
saveOperations.push(oper);
|
||||
});
|
||||
await Promise.all(saveOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the stacked journal entries.
|
||||
*/
|
||||
public reverseEntries() {
|
||||
const reverseEntries: IJournalEntry[] = [];
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
const reverseEntry = { ...entry };
|
||||
|
||||
if (entry.credit) {
|
||||
reverseEntry.debit = entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
reverseEntry.credit = entry.debit;
|
||||
}
|
||||
reverseEntries.push(reverseEntry);
|
||||
});
|
||||
this.entries = reverseEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all stored entries or by the given in ids.
|
||||
* @param {Array} ids -
|
||||
*/
|
||||
removeEntries(ids: number[] = []) {
|
||||
const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids;
|
||||
const removeEntries = this.entries.filter(
|
||||
(e) => targetIds.indexOf(e.id) !== -1
|
||||
);
|
||||
this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1);
|
||||
|
||||
removeEntries.forEach((entry) => {
|
||||
entry.credit = -1 * entry.credit;
|
||||
entry.debit = -1 * entry.debit;
|
||||
|
||||
this.setAccountBalanceChange(entry);
|
||||
this.setContactBalanceChange(entry);
|
||||
});
|
||||
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the stacked entries.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteEntries() {
|
||||
const { transactionsRepository } = this.repositories;
|
||||
|
||||
if (this.deletedEntriesIds.length > 0) {
|
||||
await transactionsRepository.deleteWhereIdIn(
|
||||
this.deletedEntriesIds,
|
||||
this.trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load fetched accounts journal entries.
|
||||
* @param {IJournalEntry[]} entries -
|
||||
*/
|
||||
fromTransactions(transactions) {
|
||||
transactions.forEach((transaction) => {
|
||||
this.entries.push({
|
||||
...transaction,
|
||||
referenceTypeFormatted: transaction.referenceTypeFormatted,
|
||||
account: transaction.accountId,
|
||||
accountNormal: get(transaction, 'account.accountNormal'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the entries balance change.
|
||||
* @public
|
||||
*/
|
||||
public calculateEntriesBalanceChange() {
|
||||
this.entries.forEach((entry) => {
|
||||
if (entry.credit) {
|
||||
this.setAccountBalanceChange(entry, 'credit');
|
||||
}
|
||||
if (entry.debit) {
|
||||
this.setAccountBalanceChange(entry, 'debit');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static fromTransactions(entries, ...args: [number, ...any]) {
|
||||
const journal = new this(...args);
|
||||
journal.fromTransactions(entries);
|
||||
|
||||
return journal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance for the given account and closing date.
|
||||
* @param {Number} accountId -
|
||||
* @param {Date} closingDate -
|
||||
* @param {string} dataType? -
|
||||
* @return {number}
|
||||
*/
|
||||
getClosingBalance(
|
||||
accountId: number,
|
||||
closingDate: Date | string,
|
||||
dateType: string = 'day'
|
||||
): number {
|
||||
let closingBalance = 0;
|
||||
const momentClosingDate = moment(closingDate);
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
// Can not continue if not before or event same closing date.
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given account balance with dependencies accounts.
|
||||
* @param {Number} accountId -
|
||||
* @param {Date} closingDate -
|
||||
* @param {String} dateType -
|
||||
* @return {Number}
|
||||
*/
|
||||
getAccountBalance(
|
||||
accountId: number,
|
||||
closingDate: Date | string,
|
||||
dateType: string
|
||||
) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsDepGraph.getNodeData(id)
|
||||
);
|
||||
|
||||
let balance: number = 0;
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const closingBalance = this.getClosingBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
this.accountsBalanceTable[account.id] = closingBalance;
|
||||
balance += this.accountsBalanceTable[account.id];
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit/debit sumation for the given account and date.
|
||||
* @param {Number} account -
|
||||
* @param {Date|String} closingDate -
|
||||
*/
|
||||
getTrialBalance(accountId, closingDate) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const result = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
result.credit += entry.credit;
|
||||
result.debit += entry.debit;
|
||||
|
||||
if (entry.accountNormal === 'credit') {
|
||||
result.balance += entry.credit - entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
result.balance += entry.debit - entry.credit;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve trial balance of the given account with depends.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
getTrialBalanceWithDepands(
|
||||
accountId: number,
|
||||
closingDate: Date,
|
||||
dateType: string
|
||||
) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsDepGraph.getNodeData(id)
|
||||
);
|
||||
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const _trialBalance = this.getTrialBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
|
||||
trialBalance.credit += _trialBalance.credit;
|
||||
trialBalance.debit += _trialBalance.debit;
|
||||
trialBalance.balance += _trialBalance.balance;
|
||||
});
|
||||
return trialBalance;
|
||||
}
|
||||
|
||||
getContactTrialBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate?: Date | string,
|
||||
openingDate?: Date | string
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const momentOpeningDate = moment(openingDate);
|
||||
const trial = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(openingDate &&
|
||||
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||
!momentOpeningDate.isSame(entry.date)) ||
|
||||
(accountId && entry.account !== accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
trial.balance -= entry.credit;
|
||||
trial.credit += entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
trial.balance += entry.debit;
|
||||
trial.debit += entry.debit;
|
||||
}
|
||||
});
|
||||
return trial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total balnace of the given customer/vendor contact.
|
||||
* @param {Number} accountId
|
||||
* @param {Number} contactId
|
||||
* @param {String} contactType
|
||||
* @param {Date} closingDate
|
||||
*/
|
||||
getContactBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate: Date,
|
||||
openingDate: Date
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
let balance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(entry.account !== accountId && accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
balance -= entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
balance += entry.debit;
|
||||
}
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
getAccountEntries(accountId: number) {
|
||||
return this.entries.filter((entry) => entry.account === accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account entries with depents accounts.
|
||||
* @param {number} accountId -
|
||||
*/
|
||||
getAccountEntriesWithDepents(accountId: number) {
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const accountsIds = [accountId, ...depAccountsIds];
|
||||
|
||||
return this.entries.filter(
|
||||
(entry) => accountsIds.indexOf(entry.account) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
249
packages/server/src/services/Accounting/Ledger.ts
Normal file
249
packages/server/src/services/Accounting/Ledger.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import moment from 'moment';
|
||||
import { defaultTo, uniqBy } from 'lodash';
|
||||
import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces';
|
||||
|
||||
export default class Ledger implements ILedger {
|
||||
readonly entries: ILedgerEntry[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ILedgerEntry[]} entries
|
||||
*/
|
||||
constructor(entries: ILedgerEntry[]) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ledegr entries.
|
||||
* @param callback
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public filter(callback): ILedger {
|
||||
const entries = this.entries.filter(callback);
|
||||
return new Ledger(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the all entries of the ledger.
|
||||
* @return {ILedgerEntry[]}
|
||||
*/
|
||||
public getEntries(): ILedgerEntry[] {
|
||||
return this.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters entries by th given contact id and returns a new ledger.
|
||||
* @param {number} contactId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereContactId(contactId: number): ILedger {
|
||||
return this.filter((entry) => entry.contactId === contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters entries by the given account id and returns a new ledger.
|
||||
* @param {number} accountId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereAccountId(accountId: number): ILedger {
|
||||
return this.filter((entry) => entry.accountId === accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters entries that before or same the given date and returns a new ledger.
|
||||
* @param {Date|string} fromDate
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereFromDate(fromDate: Date | string): ILedger {
|
||||
const fromDateParsed = moment(fromDate);
|
||||
|
||||
return this.filter(
|
||||
(entry) =>
|
||||
fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters ledger entries that after the given date and retruns a new ledger.
|
||||
* @param {Date|string} toDate
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereToDate(toDate: Date | string): ILedger {
|
||||
const toDateParsed = moment(toDate);
|
||||
|
||||
return this.filter(
|
||||
(entry) =>
|
||||
toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ledget entries by the given currency code.
|
||||
* @param {string} currencyCode -
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereCurrencyCode(currencyCode: string): ILedger {
|
||||
return this.filter((entry) => entry.currencyCode === currencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ledger entries by the given branch id.
|
||||
* @param {number} branchId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereBranch(branchId: number): ILedger {
|
||||
return this.filter((entry) => entry.branchId === branchId);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} projectId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereProject(projectId: number): ILedger {
|
||||
return this.filter((entry) => entry.projectId === projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ledger entries by the given item id.
|
||||
* @param {number} itemId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public whereItem(itemId: number): ILedger {
|
||||
return this.filter((entry) => entry.itemId === itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance of the entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getClosingBalance(): number {
|
||||
let closingBalance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += entry.credit - entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += entry.debit - entry.credit;
|
||||
}
|
||||
});
|
||||
return closingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance of the entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getForeignClosingBalance(): number {
|
||||
let closingBalance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
const exchangeRate = entry.exchangeRate || 1;
|
||||
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += (entry.credit - entry.debit) / exchangeRate;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += (entry.debit - entry.credit) / exchangeRate;
|
||||
}
|
||||
});
|
||||
return closingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the ledger has no entries.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this.entries.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the accounts ids of the entries uniquely.
|
||||
* @returns {number[]}
|
||||
*/
|
||||
public getAccountsIds = (): number[] => {
|
||||
return uniqBy(this.entries, 'accountId').map(
|
||||
(e: ILedgerEntry) => e.accountId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the contacts ids of the entries uniquely.
|
||||
* @returns {number[]}
|
||||
*/
|
||||
public getContactsIds = (): number[] => {
|
||||
return uniqBy(this.entries, 'contactId')
|
||||
.filter((e: ILedgerEntry) => e.contactId)
|
||||
.map((e: ILedgerEntry) => e.contactId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverses the ledger entries.
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
public reverse = (): Ledger => {
|
||||
const newEntries = this.entries.map((e) => {
|
||||
const credit = e.debit;
|
||||
const debit = e.credit;
|
||||
|
||||
return { ...e, credit, debit };
|
||||
});
|
||||
return new Ledger(newEntries);
|
||||
};
|
||||
|
||||
// ---------------------------------
|
||||
// # STATIC METHODS.
|
||||
// ----------------------------------
|
||||
|
||||
/**
|
||||
* Mappes the account transactions to ledger entries.
|
||||
* @param {IAccountTransaction[]} entries
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
static mappingTransactions(entries: IAccountTransaction[]): ILedgerEntry[] {
|
||||
return entries.map(this.mapTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the account transaction to ledger entry.
|
||||
* @param {IAccountTransaction} entry
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
static mapTransaction(entry: IAccountTransaction): ILedgerEntry {
|
||||
return {
|
||||
credit: defaultTo(entry.credit, 0),
|
||||
debit: defaultTo(entry.debit, 0),
|
||||
exchangeRate: entry.exchangeRate,
|
||||
currencyCode: entry.currencyCode,
|
||||
|
||||
accountNormal: entry.account.accountNormal,
|
||||
accountId: entry.accountId,
|
||||
contactId: entry.contactId,
|
||||
|
||||
date: entry.date,
|
||||
|
||||
transactionId: entry.referenceId,
|
||||
transactionType: entry.referenceType,
|
||||
|
||||
transactionNumber: entry.transactionNumber,
|
||||
referenceNumber: entry.referenceNumber,
|
||||
|
||||
index: entry.index,
|
||||
indexGroup: entry.indexGroup,
|
||||
|
||||
entryId: entry.id,
|
||||
branchId: entry.branchId,
|
||||
projectId: entry.projectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the account transactions to ledger entries.
|
||||
* @param {IAccountTransaction[]} transactions
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
static fromTransactions(transactions: IAccountTransaction[]): Ledger {
|
||||
const entries = Ledger.mappingTransactions(transactions);
|
||||
return new Ledger(entries);
|
||||
}
|
||||
}
|
||||
103
packages/server/src/services/Accounting/LedgerContactStorage.ts
Normal file
103
packages/server/src/services/Accounting/LedgerContactStorage.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import async from 'async';
|
||||
import { Knex } from 'knex';
|
||||
import { ILedger, ISaleContactsBalanceQueuePayload } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class LedgerContactsBalanceStorage {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId -
|
||||
* @param {ILedger} ledger -
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public saveContactsBalance = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
// Save contact balance queue.
|
||||
const saveContactsBalanceQueue = async.queue(
|
||||
this.saveContactBalanceTask,
|
||||
10
|
||||
);
|
||||
// Retrieves the effected contacts ids.
|
||||
const effectedContactsIds = ledger.getContactsIds();
|
||||
|
||||
effectedContactsIds.forEach((contactId: number) => {
|
||||
saveContactsBalanceQueue.push({ tenantId, contactId, ledger, trx });
|
||||
});
|
||||
if (effectedContactsIds.length > 0) await saveContactsBalanceQueue.drain();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ISaleContactsBalanceQueuePayload} task
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveContactBalanceTask = async (
|
||||
task: ISaleContactsBalanceQueuePayload
|
||||
) => {
|
||||
const { tenantId, contactId, ledger, trx } = task;
|
||||
|
||||
await this.saveContactBalance(tenantId, ledger, contactId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {number} contactId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveContactBalance = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
contactId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const contact = await Contact.query().findById(contactId);
|
||||
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Detarmines whether the contact has foreign currency.
|
||||
const isForeignContact = contact.currencyCode !== tenantMeta.baseCurrency;
|
||||
|
||||
// Filters the ledger base on the given contact id.
|
||||
const contactLedger = ledger.whereContactId(contactId);
|
||||
|
||||
const closingBalance = isForeignContact
|
||||
? contactLedger
|
||||
.whereCurrencyCode(contact.currencyCode)
|
||||
.getForeignClosingBalance()
|
||||
: contactLedger.getClosingBalance();
|
||||
|
||||
await this.changeContactBalance(tenantId, contactId, closingBalance, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} contactId
|
||||
* @param {number} change
|
||||
* @returns
|
||||
*/
|
||||
private changeContactBalance = (
|
||||
tenantId: number,
|
||||
contactId: number,
|
||||
change: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
return Contact.changeAmount({ id: contactId }, 'balance', change, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import async from 'async';
|
||||
import {
|
||||
ILedgerEntry,
|
||||
ISaveLedgerEntryQueuePayload,
|
||||
ILedger,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { transformLedgerEntryToTransaction } from './utils';
|
||||
|
||||
@Service()
|
||||
export class LedgerEntriesStorage {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
/**
|
||||
* Saves entries of the given ledger.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} knex
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public saveEntries = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
|
||||
const entries = ledger.getEntries();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
saveEntryQueue.push({ tenantId, entry, trx });
|
||||
});
|
||||
if (entries.length > 0) await saveEntryQueue.drain();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public deleteEntries = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesIds = ledger
|
||||
.getEntries()
|
||||
.filter((e) => e.entryId)
|
||||
.map((e) => e.entryId);
|
||||
|
||||
await AccountTransaction.query(trx).whereIn('id', entriesIds).delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the ledger entry to the account transactions repository.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedgerEntry} entry
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveEntry = async (
|
||||
tenantId: number,
|
||||
entry: ILedgerEntry,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const transaction = transformLedgerEntryToTransaction(entry);
|
||||
|
||||
await AccountTransaction.query(trx).insert(transaction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the ledger entry to the transactions repository async task.
|
||||
* @param {ISaveLedgerEntryQueuePayload} task
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveEntryTask = async (
|
||||
task: ISaveLedgerEntryQueuePayload
|
||||
): Promise<void> => {
|
||||
const { entry, tenantId, trx } = task;
|
||||
|
||||
await this.saveEntry(tenantId, entry, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { castArray } from 'lodash';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import LedgerStorageService from './LedgerStorageService';
|
||||
import Ledger from './Ledger';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export class LedgerRevert {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Reverts the jouranl entries.
|
||||
* @param {number|number[]} referenceId - Reference id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
*/
|
||||
public getTransactionsByReference = async (
|
||||
tenantId: number,
|
||||
referenceId: number | number[],
|
||||
referenceType: string | string[]
|
||||
) => {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', castArray(referenceType))
|
||||
.whereIn('reference_id', castArray(referenceId))
|
||||
.withGraphFetched('account');
|
||||
|
||||
return transactions;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tenantId
|
||||
* @param referenceId
|
||||
* @param referenceType
|
||||
* @param trx
|
||||
*/
|
||||
public revertGLEntries = async (
|
||||
tenantId: number,
|
||||
referenceId: number | number[],
|
||||
referenceType: string | string[],
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
//
|
||||
const transactions = await this.getTransactionsByReference(
|
||||
tenantId,
|
||||
referenceId,
|
||||
referenceType
|
||||
);
|
||||
// Creates a new ledger from transaction and reverse the entries.
|
||||
const ledger = Ledger.fromTransactions(transactions);
|
||||
const reversedLedger = ledger.reverse();
|
||||
|
||||
//
|
||||
await this.ledgerStorage.commit(tenantId, reversedLedger, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { ILedger } from '@/interfaces';
|
||||
import { LedgerContactsBalanceStorage } from './LedgerContactStorage';
|
||||
import { LedegrAccountsStorage } from './LedgetAccountStorage';
|
||||
import { LedgerEntriesStorage } from './LedgerEntriesStorage';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Ledger from './Ledger';
|
||||
@Service()
|
||||
export default class LedgerStorageService {
|
||||
@Inject()
|
||||
private ledgerEntriesService: LedgerEntriesStorage;
|
||||
|
||||
@Inject()
|
||||
private ledgerContactsBalance: LedgerContactsBalanceStorage;
|
||||
|
||||
@Inject()
|
||||
private ledgerAccountsBalance: LedegrAccountsStorage;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Commit the ledger to the storage layer as one unit-of-work.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public commit = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const tasks = [
|
||||
// Saves the ledger entries.
|
||||
this.ledgerEntriesService.saveEntries(tenantId, ledger, trx),
|
||||
|
||||
// Mutates the assocaited accounts balances.
|
||||
this.ledgerAccountsBalance.saveAccountsBalance(tenantId, ledger, trx),
|
||||
|
||||
// Mutates the associated contacts balances.
|
||||
this.ledgerContactsBalance.saveContactsBalance(tenantId, ledger, trx),
|
||||
];
|
||||
await Promise.all(tasks);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given ledger and revert balances.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public delete = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const tasks = [
|
||||
// Deletes the ledger entries.
|
||||
this.ledgerEntriesService.deleteEntries(tenantId, ledger, trx),
|
||||
|
||||
// Mutates the assocaited accounts balances.
|
||||
this.ledgerAccountsBalance.saveAccountsBalance(tenantId, ledger, trx),
|
||||
|
||||
// Mutates the associated contacts balances.
|
||||
this.ledgerContactsBalance.saveContactsBalance(tenantId, ledger, trx),
|
||||
];
|
||||
await Promise.all(tasks);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param tenantId
|
||||
* @param referenceId
|
||||
* @param referenceType
|
||||
* @param trx
|
||||
*/
|
||||
public deleteByReference = async (
|
||||
tenantId: number,
|
||||
referenceId: number | number[],
|
||||
referenceType: string | string[],
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { transactionsRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Retrieves the transactions of the given reference.
|
||||
const transactions =
|
||||
await transactionsRepository.getTransactionsByReference(
|
||||
referenceId,
|
||||
referenceType
|
||||
);
|
||||
// Creates a new ledger from transaction and reverse the entries.
|
||||
const reversedLedger = Ledger.fromTransactions(transactions).reverse();
|
||||
|
||||
// Deletes and reverts the balances.
|
||||
await this.delete(tenantId, reversedLedger, trx);
|
||||
};
|
||||
}
|
||||
155
packages/server/src/services/Accounting/LedgetAccountStorage.ts
Normal file
155
packages/server/src/services/Accounting/LedgetAccountStorage.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import async from 'async';
|
||||
import { Knex } from 'knex';
|
||||
import { uniq } from 'lodash';
|
||||
import { ILedger, ISaveAccountsBalanceQueuePayload } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class LedegrAccountsStorage {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve depepants ids of the give accounts ids.
|
||||
* @param {number[]} accountsIds
|
||||
* @param depGraph
|
||||
* @returns {number[]}
|
||||
*/
|
||||
private getDependantsAccountsIds = (
|
||||
accountsIds: number[],
|
||||
depGraph
|
||||
): number[] => {
|
||||
const depAccountsIds = [];
|
||||
|
||||
accountsIds.forEach((accountId: number) => {
|
||||
const depAccountIds = depGraph.dependantsOf(accountId);
|
||||
depAccountsIds.push(accountId, ...depAccountIds);
|
||||
});
|
||||
return uniq(depAccountsIds);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} accountsIds
|
||||
* @returns {number[]}
|
||||
*/
|
||||
private findDependantsAccountsIds = async (
|
||||
tenantId: number,
|
||||
accountsIds: number[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<number[]> => {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const accountsGraph = await accountRepository.getDependencyGraph(null, trx);
|
||||
|
||||
return this.getDependantsAccountsIds(accountsIds, accountsGraph);
|
||||
};
|
||||
|
||||
/**
|
||||
* Atomic mutation for accounts balances.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public saveAccountsBalance = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
// Initiate a new queue for accounts balance mutation.
|
||||
const saveAccountsBalanceQueue = async.queue(
|
||||
this.saveAccountBalanceTask,
|
||||
10
|
||||
);
|
||||
const effectedAccountsIds = ledger.getAccountsIds();
|
||||
const dependAccountsIds = await this.findDependantsAccountsIds(
|
||||
tenantId,
|
||||
effectedAccountsIds,
|
||||
trx
|
||||
);
|
||||
dependAccountsIds.forEach((accountId: number) => {
|
||||
saveAccountsBalanceQueue.push({ tenantId, ledger, accountId, trx });
|
||||
});
|
||||
if (dependAccountsIds.length > 0) {
|
||||
await saveAccountsBalanceQueue.drain();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Async task mutates the given account balance.
|
||||
* @param {ISaveAccountsBalanceQueuePayload} task
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveAccountBalanceTask = async (
|
||||
task: ISaveAccountsBalanceQueuePayload
|
||||
): Promise<void> => {
|
||||
const { tenantId, ledger, accountId, trx } = task;
|
||||
|
||||
await this.saveAccountBalanceFromLedger(tenantId, ledger, accountId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves specific account balance from the given ledger.
|
||||
* @param {number} tenantId
|
||||
* @param {ILedger} ledger
|
||||
* @param {number} accountId
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveAccountBalanceFromLedger = async (
|
||||
tenantId: number,
|
||||
ledger: ILedger,
|
||||
accountId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const account = await Account.query(trx).findById(accountId);
|
||||
|
||||
// Filters the ledger entries by the current acount.
|
||||
const accountLedger = ledger.whereAccountId(accountId);
|
||||
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Detarmines whether the account has foreign currency.
|
||||
const isAccountForeign = account.currencyCode !== tenantMeta.baseCurrency;
|
||||
|
||||
// Calculates the closing foreign balance by the given currency if account was has
|
||||
// foreign currency otherwise get closing balance.
|
||||
const closingBalance = isAccountForeign
|
||||
? accountLedger
|
||||
.whereCurrencyCode(account.currencyCode)
|
||||
.getForeignClosingBalance()
|
||||
: accountLedger.getClosingBalance();
|
||||
|
||||
await this.saveAccountBalance(tenantId, accountId, closingBalance, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the account balance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @param {number} change
|
||||
* @param {Knex.Transaction} trx -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveAccountBalance = async (
|
||||
tenantId: number,
|
||||
accountId: number,
|
||||
change: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
// Ensure the account has atleast zero in amount.
|
||||
await Account.query(trx)
|
||||
.findById(accountId)
|
||||
.whereNull('amount')
|
||||
.patch({ amount: 0 });
|
||||
|
||||
await Account.changeAmount({ id: accountId }, 'amount', change, trx);
|
||||
};
|
||||
}
|
||||
34
packages/server/src/services/Accounting/utils.ts
Normal file
34
packages/server/src/services/Accounting/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IAccountTransaction, ILedgerEntry } from '@/interfaces';
|
||||
|
||||
export const transformLedgerEntryToTransaction = (
|
||||
entry: ILedgerEntry
|
||||
): IAccountTransaction => {
|
||||
return {
|
||||
date: entry.date,
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
|
||||
currencyCode: entry.currencyCode,
|
||||
exchangeRate: entry.exchangeRate,
|
||||
|
||||
accountId: entry.accountId,
|
||||
contactId: entry.contactId,
|
||||
|
||||
referenceType: entry.transactionType,
|
||||
referenceId: entry.transactionId,
|
||||
|
||||
transactionNumber: entry.transactionNumber,
|
||||
referenceNumber: entry.referenceNumber,
|
||||
|
||||
index: entry.index,
|
||||
indexGroup: entry.indexGroup,
|
||||
|
||||
branchId: entry.branchId,
|
||||
userId: entry.userId,
|
||||
itemId: entry.itemId,
|
||||
projectId: entry.projectId,
|
||||
|
||||
costable: entry.costable,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user