2022-05-03 13:13:47 +00:00
|
|
|
import { Fyo, t } from 'fyo';
|
2022-07-28 07:55:48 +00:00
|
|
|
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
|
2022-05-03 13:13:47 +00:00
|
|
|
import { Account } from 'models/baseModels/Account/Account';
|
|
|
|
import { AccountingLedgerEntry } from 'models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
|
|
|
|
import { ModelNameEnum } from 'models/types';
|
2022-05-23 05:30:54 +00:00
|
|
|
import { Money } from 'pesa';
|
2022-05-03 13:13:47 +00:00
|
|
|
import { Transactional } from './Transactional';
|
|
|
|
import { AccountBalanceChange, TransactionType } from './types';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* # LedgerPosting
|
|
|
|
*
|
|
|
|
* Class that maintains a set of AccountingLedgerEntries pertaining to
|
|
|
|
* a single Transactional doc, example a Payment entry.
|
|
|
|
*
|
|
|
|
* For each account touched in a transactional doc a separate ledger entry is
|
|
|
|
* created.
|
|
|
|
*
|
|
|
|
* This is done using the `debit(...)` and `credit(...)` methods which also
|
|
|
|
* keep track of the change in balance to an account.
|
|
|
|
*
|
|
|
|
* Unless `post()` or `postReverese()` is called the entries aren't made.
|
|
|
|
*/
|
|
|
|
|
|
|
|
export class LedgerPosting {
|
|
|
|
fyo: Fyo;
|
|
|
|
refDoc: Transactional;
|
|
|
|
entries: AccountingLedgerEntry[];
|
2022-05-04 12:47:45 +00:00
|
|
|
creditMap: Record<string, AccountingLedgerEntry>;
|
|
|
|
debitMap: Record<string, AccountingLedgerEntry>;
|
2022-05-03 13:13:47 +00:00
|
|
|
reverted: boolean;
|
|
|
|
accountBalanceChanges: AccountBalanceChange[];
|
|
|
|
|
|
|
|
constructor(refDoc: Transactional, fyo: Fyo) {
|
|
|
|
this.fyo = fyo;
|
|
|
|
this.refDoc = refDoc;
|
|
|
|
this.entries = [];
|
2022-05-04 12:47:45 +00:00
|
|
|
this.creditMap = {};
|
|
|
|
this.debitMap = {};
|
2022-05-03 13:13:47 +00:00
|
|
|
this.reverted = false;
|
|
|
|
this.accountBalanceChanges = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
async debit(account: string, amount: Money) {
|
2022-05-04 12:47:45 +00:00
|
|
|
const ledgerEntry = this._getLedgerEntry(account, 'debit');
|
2022-05-03 13:13:47 +00:00
|
|
|
await ledgerEntry.set('debit', ledgerEntry.debit!.add(amount));
|
|
|
|
await this._updateAccountBalanceChange(account, 'debit', amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
async credit(account: string, amount: Money) {
|
2022-05-04 12:47:45 +00:00
|
|
|
const ledgerEntry = this._getLedgerEntry(account, 'credit');
|
2022-05-03 13:13:47 +00:00
|
|
|
await ledgerEntry.set('credit', ledgerEntry.credit!.add(amount));
|
|
|
|
await this._updateAccountBalanceChange(account, 'credit', amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
async post() {
|
|
|
|
this._validateIsEqual();
|
|
|
|
await this._sync();
|
|
|
|
}
|
|
|
|
|
|
|
|
async postReverse() {
|
|
|
|
this._validateIsEqual();
|
|
|
|
await this._syncReverse();
|
|
|
|
}
|
|
|
|
|
|
|
|
validate() {
|
|
|
|
this._validateIsEqual();
|
|
|
|
}
|
|
|
|
|
|
|
|
async makeRoundOffEntry() {
|
|
|
|
const { debit, credit } = this._getTotalDebitAndCredit();
|
|
|
|
const difference = debit.sub(credit);
|
|
|
|
const absoluteValue = difference.abs();
|
|
|
|
if (absoluteValue.eq(0)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const roundOffAccount = await this._getRoundOffAccount();
|
|
|
|
if (difference.gt(0)) {
|
|
|
|
this.credit(roundOffAccount, absoluteValue);
|
|
|
|
} else {
|
|
|
|
this.debit(roundOffAccount, absoluteValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _updateAccountBalanceChange(
|
|
|
|
name: string,
|
|
|
|
type: TransactionType,
|
|
|
|
amount: Money
|
|
|
|
) {
|
|
|
|
const accountDoc = (await this.fyo.doc.getDoc('Account', name)) as Account;
|
|
|
|
|
|
|
|
let change: Money;
|
|
|
|
if (accountDoc.isDebit) {
|
|
|
|
change = type === 'debit' ? amount : amount.neg();
|
|
|
|
} else {
|
|
|
|
change = type === 'credit' ? amount : amount.neg();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.accountBalanceChanges.push({
|
|
|
|
name,
|
|
|
|
change,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-05-04 12:47:45 +00:00
|
|
|
_getLedgerEntry(
|
|
|
|
account: string,
|
|
|
|
type: TransactionType
|
|
|
|
): AccountingLedgerEntry {
|
|
|
|
let map = this.creditMap;
|
|
|
|
if (type === 'debit') {
|
|
|
|
map = this.debitMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (map[account]) {
|
|
|
|
return map[account];
|
2022-05-03 13:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const ledgerEntry = this.fyo.doc.getNewDoc(
|
|
|
|
ModelNameEnum.AccountingLedgerEntry,
|
|
|
|
{
|
|
|
|
account: account,
|
|
|
|
party: (this.refDoc.party as string) ?? '',
|
|
|
|
date: this.refDoc.date as string | Date,
|
|
|
|
referenceType: this.refDoc.schemaName,
|
|
|
|
referenceName: this.refDoc.name!,
|
|
|
|
reverted: this.reverted,
|
|
|
|
debit: this.fyo.pesa(0),
|
|
|
|
credit: this.fyo.pesa(0),
|
2022-05-04 08:19:40 +00:00
|
|
|
},
|
|
|
|
false
|
2022-05-03 13:13:47 +00:00
|
|
|
) as AccountingLedgerEntry;
|
|
|
|
|
|
|
|
this.entries.push(ledgerEntry);
|
2022-05-04 12:47:45 +00:00
|
|
|
map[account] = ledgerEntry;
|
2022-05-03 13:13:47 +00:00
|
|
|
|
2022-05-04 12:47:45 +00:00
|
|
|
return map[account];
|
2022-05-03 13:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_validateIsEqual() {
|
|
|
|
const { debit, credit } = this._getTotalDebitAndCredit();
|
|
|
|
if (debit.eq(credit)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Total Debit: ${this.fyo.format(
|
|
|
|
debit,
|
|
|
|
'Currency'
|
|
|
|
)} must be equal to Total Credit: ${this.fyo.format(credit, 'Currency')}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
_getTotalDebitAndCredit() {
|
|
|
|
let debit = this.fyo.pesa(0);
|
|
|
|
let credit = this.fyo.pesa(0);
|
|
|
|
|
|
|
|
for (const entry of this.entries) {
|
|
|
|
debit = debit.add(entry.debit!);
|
|
|
|
credit = credit.add(entry.credit!);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { debit, credit };
|
|
|
|
}
|
|
|
|
|
|
|
|
async _sync() {
|
|
|
|
await this._syncLedgerEntries();
|
|
|
|
await this._syncBalanceChanges();
|
|
|
|
}
|
|
|
|
|
|
|
|
async _syncLedgerEntries() {
|
|
|
|
for (const entry of this.entries) {
|
|
|
|
await entry.sync();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _syncReverse() {
|
|
|
|
await this._syncReverseLedgerEntries();
|
|
|
|
for (const entry of this.accountBalanceChanges) {
|
|
|
|
entry.change = (entry.change as Money).neg();
|
|
|
|
}
|
|
|
|
await this._syncBalanceChanges();
|
|
|
|
}
|
|
|
|
|
|
|
|
async _syncBalanceChanges() {
|
|
|
|
for (const { name, change } of this.accountBalanceChanges) {
|
|
|
|
const accountDoc = await this.fyo.doc.getDoc(ModelNameEnum.Account, name);
|
|
|
|
const balance = accountDoc.get('balance') as Money;
|
|
|
|
await accountDoc.setAndSync('balance', balance.add(change));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _syncReverseLedgerEntries() {
|
|
|
|
const data = (await this.fyo.db.getAll('AccountingLedgerEntry', {
|
|
|
|
fields: ['name'],
|
|
|
|
filters: {
|
|
|
|
referenceType: this.refDoc.schemaName,
|
|
|
|
referenceName: this.refDoc.name!,
|
|
|
|
reverted: false,
|
|
|
|
},
|
|
|
|
})) as { name: string }[];
|
|
|
|
|
|
|
|
for (const { name } of data) {
|
|
|
|
const doc = (await this.fyo.doc.getDoc(
|
|
|
|
'AccountingLedgerEntry',
|
|
|
|
name
|
|
|
|
)) as AccountingLedgerEntry;
|
|
|
|
await doc.revert();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _getRoundOffAccount() {
|
2022-07-28 07:55:48 +00:00
|
|
|
const roundOffAccount = (await this.fyo.getValue(
|
2022-05-03 13:13:47 +00:00
|
|
|
ModelNameEnum.AccountingSettings,
|
|
|
|
'roundOffAccount'
|
|
|
|
)) as string;
|
2022-07-28 07:55:48 +00:00
|
|
|
|
|
|
|
if (!roundOffAccount) {
|
|
|
|
const notFoundError = new NotFoundError(
|
|
|
|
t`Please set Round Off Account in the Settings.`,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
notFoundError.name = t`Round Off Account Not Found`;
|
|
|
|
throw notFoundError;
|
|
|
|
}
|
|
|
|
|
|
|
|
return roundOffAccount;
|
2022-05-03 13:13:47 +00:00
|
|
|
}
|
|
|
|
}
|