2
0
mirror of https://github.com/frappe/books.git synced 2024-11-15 09:54:04 +00:00
books/models/Transactional/LedgerPosting.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

207 lines
5.4 KiB
TypeScript
Raw Normal View History

import { Fyo, t } from 'fyo';
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
import { AccountingLedgerEntry } from 'models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
import { ModelNameEnum } from 'models/types';
2022-05-23 05:30:54 +00:00
import { Money } from 'pesa';
import { Transactional } from './Transactional';
2022-11-07 07:13:01 +00:00
import { 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.
*
2022-11-07 07:13:01 +00:00
* This is done using the `debit(...)` and `credit(...)` methods.
*
* Unless `post()` or `postReverese()` is called the entries aren't made.
*/
export class LedgerPosting {
fyo: Fyo;
refDoc: Transactional;
entries: AccountingLedgerEntry[];
creditMap: Record<string, AccountingLedgerEntry>;
debitMap: Record<string, AccountingLedgerEntry>;
reverted: boolean;
constructor(refDoc: Transactional, fyo: Fyo) {
this.fyo = fyo;
this.refDoc = refDoc;
this.entries = [];
this.creditMap = {};
this.debitMap = {};
this.reverted = false;
}
async debit(account: string, amount: Money) {
const ledgerEntry = this._getLedgerEntry(account, 'debit');
await ledgerEntry.set('debit', ledgerEntry.debit!.add(amount));
}
async credit(account: string, amount: Money) {
const ledgerEntry = this._getLedgerEntry(account, 'credit');
await ledgerEntry.set('credit', ledgerEntry.credit!.add(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)) {
await this.credit(roundOffAccount, absoluteValue);
} else {
await this.debit(roundOffAccount, absoluteValue);
}
}
_getLedgerEntry(
account: string,
type: TransactionType
): AccountingLedgerEntry {
let map = this.creditMap;
if (type === 'debit') {
map = this.debitMap;
}
if (map[account]) {
return map[account];
}
// Timezone inconsistency fix (very ugly code for now)
let entryDateTime = this.refDoc.date as string | Date;
let dateTimeValue: Date;
if (typeof entryDateTime === 'string' || entryDateTime instanceof String) {
dateTimeValue = new Date(entryDateTime);
} else {
dateTimeValue = entryDateTime;
}
let dtFixedValue = dateTimeValue;
let dtMinutes = dtFixedValue.getTimezoneOffset() % 60;
let dtHours = (dtFixedValue.getTimezoneOffset() - dtMinutes) / 60;
dtFixedValue.setHours(dtFixedValue.getHours() - dtHours);
dtFixedValue.setMinutes(dtFixedValue.getMinutes() - dtMinutes);
// end ugly timezone fix code
const ledgerEntry = this.fyo.doc.getNewDoc(
ModelNameEnum.AccountingLedgerEntry,
{
account: account,
party: (this.refDoc.party as string) ?? '',
date: dtFixedValue,
referenceType: this.refDoc.schemaName,
referenceName: this.refDoc.name!,
reverted: this.reverted,
debit: this.fyo.pesa(0),
credit: this.fyo.pesa(0),
},
false
) as AccountingLedgerEntry;
this.entries.push(ledgerEntry);
map[account] = ledgerEntry;
return map[account];
}
_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();
}
async _syncLedgerEntries() {
for (const entry of this.entries) {
await entry.sync();
}
}
async _syncReverse() {
await this._syncReverseLedgerEntries();
}
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() {
const roundOffAccount = (await this.fyo.getValue(
ModelNameEnum.AccountingSettings,
'roundOffAccount'
)) as string;
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;
}
}