-
Notifications
You must be signed in to change notification settings - Fork 609
/
LedgerPosting.ts
209 lines (179 loc) · 5.45 KB
/
LedgerPosting.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { Fyo, t } from 'fyo';
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
import { AccountingLedgerEntry } from 'models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { Transactional } from './Transactional';
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.
*
* 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)
const entryDateTime = this.refDoc.date as string | Date;
let dateTimeValue: Date;
if (typeof entryDateTime === 'string' || entryDateTime instanceof String) {
dateTimeValue = new Date(entryDateTime);
} else {
dateTimeValue = entryDateTime;
}
const dtFixedValue = dateTimeValue;
const dtMinutes = dtFixedValue.getTimezoneOffset() % 60;
const dtHours = (dtFixedValue.getTimezoneOffset() - dtMinutes) / 60;
// Forcing the time to always be set to 00:00.000 for locale time
dtFixedValue.setHours(0 - dtHours);
dtFixedValue.setMinutes(0 - dtMinutes);
dtFixedValue.setSeconds(0);
dtFixedValue.setMilliseconds(0);
// 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;
}
}