2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

incr: add tree making functions to PNL

This commit is contained in:
18alantom 2022-05-15 17:43:31 +05:30
parent bf01fa0327
commit 6c350f0974

View File

@ -1,7 +1,11 @@
import { Fyo, t } from 'fyo'; import { t } from 'fyo';
import { Action } from 'fyo/model/types'; import { Action } from 'fyo/model/types';
import { cloneDeep } from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AccountRootType } from 'models/baseModels/Account/types'; import {
AccountRootType,
AccountRootTypeEnum,
} from 'models/baseModels/Account/types';
import { isCredit } from 'models/helpers'; import { isCredit } from 'models/helpers';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { LedgerReport } from 'reports/LedgerReport'; import { LedgerReport } from 'reports/LedgerReport';
@ -20,7 +24,25 @@ type DateRange = { fromDate: DateTime; toDate: DateTime };
type ValueMap = Map<DateRange, number>; type ValueMap = Map<DateRange, number>;
type AccountNameValueMapMap = Map<string, ValueMap>; type AccountNameValueMapMap = Map<string, ValueMap>;
type BasedOn = 'Fiscal Year' | 'Date Range'; type BasedOn = 'Fiscal Year' | 'Date Range';
type Account = { name: string; rootType: AccountRootType; isGroup: boolean };
interface Account {
name: string;
rootType: AccountRootType;
isGroup: boolean;
parentAccount: string | null;
}
interface AccountTreeNode extends Account {
children?: AccountTreeNode[];
valueMap?: ValueMap;
prune?: boolean;
}
type AccountTree = Record<string, AccountTreeNode>;
const PNL_ROOT_TYPES: AccountRootType[] = [
AccountRootTypeEnum.Income,
AccountRootTypeEnum.Expense,
];
export class ProfitAndLoss extends LedgerReport { export class ProfitAndLoss extends LedgerReport {
static title = t`Profit And Loss`; static title = t`Profit And Loss`;
@ -32,25 +54,21 @@ export class ProfitAndLoss extends LedgerReport {
toYear?: number; toYear?: number;
singleColumn: boolean = false; singleColumn: boolean = false;
periodicity: Periodicity = 'Monthly'; periodicity: Periodicity = 'Monthly';
basedOn: BasedOn = 'Fiscal Year'; basedOn: BasedOn = 'Date Range';
_rawData: LedgerEntry[] = []; _rawData: LedgerEntry[] = [];
accountMap?: Record<string, Account>; accountMap?: Record<string, Account>;
constructor(fyo: Fyo) {
super(fyo);
}
async setDefaultFilters(): Promise<void> { async setDefaultFilters(): Promise<void> {
if (this.basedOn === 'Date Range' && !this.toDate) { if (this.basedOn === 'Date Range' && !this.toDate) {
this.toDate = DateTime.now().toISODate(); this.toDate = DateTime.now().toISODate();
this.count = 1; this.count = 3;
} }
if (this.basedOn === 'Fiscal Year' && !this.toYear) { if (this.basedOn === 'Fiscal Year' && !this.toYear) {
this.toYear = DateTime.now().year; this.fromYear = DateTime.now().year;
this.fromYear = this.toYear - 1; this.toYear = this.fromYear + 1;
} }
} }
@ -66,12 +84,30 @@ export class ProfitAndLoss extends LedgerReport {
const map = this._getGroupedMap(sort, 'account'); const map = this._getGroupedMap(sort, 'account');
const rangeGroupedMap = await this._getGroupedByDateRanges(map); const rangeGroupedMap = await this._getGroupedByDateRanges(map);
const accountTree = await this._getAccountTree(rangeGroupedMap);
for (const name of Object.keys(accountTree)) {
const { rootType } = accountTree[name];
if (PNL_ROOT_TYPES.includes(rootType)) {
continue;
}
delete accountTree[name];
}
/** /**
* TODO: Get account tree from accountMap
* TODO: Create Grid from rangeGroupedMap and tree * TODO: Create Grid from rangeGroupedMap and tree
*/ */
} }
async temp() {
await this.setDefaultFilters();
await this._setRawData();
const map = this._getGroupedMap(false, 'account');
const rangeGroupedMap = await this._getGroupedByDateRanges(map);
const accountTree = await this._getAccountTree(rangeGroupedMap);
return accountTree;
}
async _getGroupedByDateRanges( async _getGroupedByDateRanges(
map: GroupedMap map: GroupedMap
): Promise<AccountNameValueMapMap> { ): Promise<AccountNameValueMapMap> {
@ -83,7 +119,7 @@ export class ProfitAndLoss extends LedgerReport {
const valueMap: ValueMap = new Map(); const valueMap: ValueMap = new Map();
for (const entry of map.get(account)!) { for (const entry of map.get(account)!) {
const key = this._getRangeMapKey(entry, dateRanges); const key = this._getRangeMapKey(entry, dateRanges);
if (valueMap === null) { if (key === null) {
continue; continue;
} }
@ -103,6 +139,18 @@ export class ProfitAndLoss extends LedgerReport {
return accountValueMap; return accountValueMap;
} }
async _getAccountTree(rangeGroupedMap: AccountNameValueMapMap) {
const accountTree = cloneDeep(await this._getAccountMap()) as AccountTree;
setPruneFlagOnAccountTreeNodes(accountTree);
setValueMapOnAccountTreeNodes(accountTree, rangeGroupedMap);
setChildrenOnAccountTreeNodes(accountTree);
deleteNonRootAccountTreeNodes(accountTree);
pruneAccountTree(accountTree);
return accountTree;
}
async _getAccountMap() { async _getAccountMap() {
if (this.accountMap) { if (this.accountMap) {
return this.accountMap; return this.accountMap;
@ -110,12 +158,13 @@ export class ProfitAndLoss extends LedgerReport {
const accountList: Account[] = ( const accountList: Account[] = (
await this.fyo.db.getAllRaw('Account', { await this.fyo.db.getAllRaw('Account', {
fields: ['name', 'rootType', 'isGroup'], fields: ['name', 'rootType', 'isGroup', 'parentAccount'],
}) })
).map((rv) => ({ ).map((rv) => ({
name: rv.name as string, name: rv.name as string,
rootType: rv.rootType as AccountRootType, rootType: rv.rootType as AccountRootType,
isGroup: Boolean(rv.isGroup), isGroup: Boolean(rv.isGroup),
parentAccount: rv.parentAccount as string | null,
})); }));
this.accountMap = getMapFromList(accountList, 'name'); this.accountMap = getMapFromList(accountList, 'name');
@ -126,12 +175,15 @@ export class ProfitAndLoss extends LedgerReport {
entry: LedgerEntry, entry: LedgerEntry,
dateRanges: DateRange[] dateRanges: DateRange[]
): DateRange | null { ): DateRange | null {
const entryDate = +DateTime.fromISO( const entryDate = DateTime.fromISO(
entry.date!.toISOString().split('T')[0] entry.date!.toISOString().split('T')[0]
); ).toMillis();
for (const dr of dateRanges) { for (const dr of dateRanges) {
if (entryDate <= +dr.toDate && entryDate > +dr.fromDate) { const toDate = dr.toDate.toMillis();
const fromDate = dr.fromDate.toMillis();
if (entryDate <= toDate && entryDate > fromDate) {
return dr; return dr;
} }
} }
@ -231,7 +283,7 @@ export class ProfitAndLoss extends LedgerReport {
{ label: t`Fiscal Year`, value: 'Fiscal Year' }, { label: t`Fiscal Year`, value: 'Fiscal Year' },
{ label: t`Date Range`, value: 'Date Range' }, { label: t`Date Range`, value: 'Date Range' },
], ],
default: 'Fiscal Year', default: 'Date Range',
label: t`Based On`, label: t`Based On`,
fieldname: 'basedOn', fieldname: 'basedOn',
}, },
@ -332,3 +384,112 @@ const monthsMap: Record<Periodicity, number> = {
'Half Yearly': 6, 'Half Yearly': 6,
Yearly: 12, Yearly: 12,
}; };
function setPruneFlagOnAccountTreeNodes(accountTree: AccountTree) {
for (const account of Object.values(accountTree)) {
account.prune = true;
}
}
function setValueMapOnAccountTreeNodes(
accountTree: AccountTree,
rangeGroupedMap: AccountNameValueMapMap
) {
for (const name of rangeGroupedMap.keys()) {
const valueMap = rangeGroupedMap.get(name)!;
accountTree[name].valueMap = valueMap;
accountTree[name].prune = false;
/**
* Set the update the parent account values recursively
* also prevent pruning of the parent accounts.
*/
let parentAccountName: string | null = accountTree[name].parentAccount;
let parentValueMap = valueMap;
while (parentAccountName !== null) {
const update = updateParentAccountWithChildValues(
accountTree,
parentAccountName,
parentValueMap
);
parentAccountName = update.parentAccountName;
parentValueMap = update.parentValueMap;
}
}
}
function updateParentAccountWithChildValues(
accountTree: AccountTree,
parentAccountName: string,
parentValueMap: ValueMap
): {
parentAccountName: string | null;
parentValueMap: ValueMap;
} {
const parentAccount = accountTree[parentAccountName];
parentAccount.prune = false;
parentAccount.valueMap ??= new Map();
for (const key of parentValueMap.keys()) {
const value = parentAccount.valueMap!.get(key) ?? 0;
parentAccount.valueMap!.set(key, value + parentValueMap.get(key)!);
}
return {
parentAccountName: parentAccount.parentAccount,
parentValueMap: parentAccount.valueMap!,
};
}
function setChildrenOnAccountTreeNodes(accountTree: AccountTree) {
const parentNodes: Set<string> = new Set();
for (const name of Object.keys(accountTree)) {
const ac = accountTree[name];
if (!ac.parentAccount) {
continue;
}
accountTree[ac.parentAccount].children ??= [];
accountTree[ac.parentAccount].children!.push(ac!);
parentNodes.add(ac.parentAccount);
}
}
function deleteNonRootAccountTreeNodes(accountTree: AccountTree) {
for (const name of Object.keys(accountTree)) {
const ac = accountTree[name];
if (!ac.parentAccount) {
continue;
}
delete accountTree[name];
}
}
function pruneAccountTree(accountTree: AccountTree) {
for (const root of Object.keys(accountTree)) {
if (accountTree[root].prune) {
delete accountTree[root];
}
}
for (const root of Object.keys(accountTree)) {
accountTree[root].children = getPrunedChildren(accountTree[root].children!);
}
}
function getPrunedChildren(children: AccountTreeNode[]): AccountTreeNode[] {
return children.filter((child) => {
if (child.children) {
child.children = getPrunedChildren(child.children);
}
return !child.prune;
});
}
function convertAccountTreeToAccountList() {}