mirror of
https://github.com/frappe/books.git
synced 2024-12-22 19:09:01 +00:00
incr: do BalanceSheet (smthn wrng)
This commit is contained in:
parent
d65b04de4c
commit
06b5ac4f90
@ -91,7 +91,7 @@ function formatCurrency(
|
||||
function formatNumber(value: DocValue, fyo: Fyo): string {
|
||||
const numberFormatter = getNumberFormatter(fyo);
|
||||
if (typeof value === 'number') {
|
||||
return numberFormatter.format(value);
|
||||
value = fyo.pesa(value.toFixed(20));
|
||||
}
|
||||
|
||||
if ((value as Money).round) {
|
||||
|
617
reports/AccountReport.ts
Normal file
617
reports/AccountReport.ts
Normal file
@ -0,0 +1,617 @@
|
||||
import { t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AccountRootType } from 'models/baseModels/Account/types';
|
||||
import { isCredit } from 'models/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { LedgerReport } from 'reports/LedgerReport';
|
||||
import {
|
||||
Account,
|
||||
AccountList,
|
||||
AccountListNode,
|
||||
AccountNameValueMapMap,
|
||||
AccountTree,
|
||||
AccountTreeNode,
|
||||
BasedOn,
|
||||
ColumnField,
|
||||
DateRange,
|
||||
GroupedMap,
|
||||
LedgerEntry,
|
||||
Periodicity,
|
||||
ReportCell,
|
||||
ReportData,
|
||||
ReportRow,
|
||||
Tree,
|
||||
TreeNode,
|
||||
ValueMap,
|
||||
} from 'reports/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getMapFromList } from 'utils';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
|
||||
const ACC_NAME_WIDTH = 2;
|
||||
const ACC_BAL_WIDTH = 1;
|
||||
|
||||
export abstract class AccountReport extends LedgerReport {
|
||||
toDate?: string;
|
||||
count: number = 3;
|
||||
fromYear?: number;
|
||||
toYear?: number;
|
||||
consolidateColumns: boolean = false;
|
||||
hideGroupBalance: boolean = false;
|
||||
periodicity: Periodicity = 'Monthly';
|
||||
basedOn: BasedOn = 'Until Date';
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
_dateRanges?: DateRange[];
|
||||
|
||||
accountMap?: Record<string, Account>;
|
||||
abstract get rootTypes(): AccountRootType[];
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
await this._setDateRanges();
|
||||
}
|
||||
|
||||
async setDefaultFilters(): Promise<void> {
|
||||
if (this.basedOn === 'Until Date' && !this.toDate) {
|
||||
this.toDate = DateTime.now().toISODate();
|
||||
}
|
||||
|
||||
if (this.basedOn === 'Fiscal Year' && !this.toYear) {
|
||||
this.fromYear = DateTime.now().year;
|
||||
this.toYear = this.fromYear + 1;
|
||||
}
|
||||
|
||||
await this._setDateRanges();
|
||||
}
|
||||
|
||||
async _setDateRanges() {
|
||||
this._dateRanges = await this._getDateRanges();
|
||||
}
|
||||
|
||||
getRootNode(
|
||||
rootType: AccountRootType,
|
||||
accountTree: AccountTree
|
||||
): AccountTreeNode | undefined {
|
||||
const rootNodeList = Object.values(accountTree);
|
||||
return rootNodeList.find((n) => n.rootType === rootType);
|
||||
}
|
||||
|
||||
getEmptyRow(): ReportRow {
|
||||
const columns = this.getColumns();
|
||||
return {
|
||||
isEmpty: true,
|
||||
cells: columns.map(
|
||||
(c) =>
|
||||
({
|
||||
value: '',
|
||||
rawValue: '',
|
||||
width: c.width,
|
||||
align: 'left',
|
||||
} as ReportCell)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalNode(
|
||||
rootNode: AccountTreeNode,
|
||||
name: string
|
||||
): Promise<AccountListNode> {
|
||||
const accountTree = { [rootNode.name]: rootNode };
|
||||
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
|
||||
|
||||
const totalMap = leafNodes.reduce((acc, node) => {
|
||||
for (const key of this._dateRanges!) {
|
||||
const bal = acc.get(key) ?? 0;
|
||||
const val = node.valueMap?.get(key) ?? 0;
|
||||
acc.set(key, bal + val);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map() as ValueMap);
|
||||
|
||||
return { name, valueMap: totalMap, level: 0 } as AccountListNode;
|
||||
}
|
||||
|
||||
getReportRowsFromAccountList(accountList: AccountList): ReportData {
|
||||
return accountList.map((al) => {
|
||||
return this.getRowFromAccountListNode(al);
|
||||
});
|
||||
}
|
||||
|
||||
getRowFromAccountListNode(al: AccountListNode) {
|
||||
const nameCell = {
|
||||
value: al.name,
|
||||
rawValue: al.name,
|
||||
align: 'left',
|
||||
width: ACC_NAME_WIDTH,
|
||||
bold: !al.level,
|
||||
italics: al.isGroup,
|
||||
indent: al.level ?? 0,
|
||||
} as ReportCell;
|
||||
|
||||
const balanceCells = this._dateRanges!.map((k) => {
|
||||
const rawValue = al.valueMap?.get(k) ?? 0;
|
||||
let value = this.fyo.format(rawValue, 'Currency');
|
||||
if (this.hideGroupBalance && al.isGroup) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
return {
|
||||
rawValue,
|
||||
value,
|
||||
align: 'right',
|
||||
width: ACC_BAL_WIDTH,
|
||||
} as ReportCell;
|
||||
});
|
||||
|
||||
return {
|
||||
cells: [nameCell, balanceCells].flat(),
|
||||
level: al.level,
|
||||
isGroup: !!al.isGroup,
|
||||
folded: false,
|
||||
foldedBelow: false,
|
||||
} as ReportRow;
|
||||
}
|
||||
|
||||
async _getGroupedByDateRanges(
|
||||
map: GroupedMap
|
||||
): Promise<AccountNameValueMapMap> {
|
||||
const accountValueMap: AccountNameValueMapMap = new Map();
|
||||
const accountMap = await this._getAccountMap();
|
||||
|
||||
for (const account of map.keys()) {
|
||||
const valueMap: ValueMap = new Map();
|
||||
|
||||
/**
|
||||
* Set Balance for every DateRange key
|
||||
*/
|
||||
for (const entry of map.get(account)!) {
|
||||
const key = this._getRangeMapKey(entry);
|
||||
if (key === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const totalBalance = valueMap.get(key!) ?? 0;
|
||||
const balance = (entry.debit ?? 0) - (entry.credit ?? 0);
|
||||
const rootType = accountMap[entry.account].rootType;
|
||||
|
||||
if (isCredit(rootType)) {
|
||||
valueMap.set(key!, totalBalance - balance);
|
||||
} else {
|
||||
valueMap.set(key!, totalBalance + balance);
|
||||
}
|
||||
}
|
||||
accountValueMap.set(account, valueMap);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this.accountMap) {
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
const accountList: Account[] = (
|
||||
await this.fyo.db.getAllRaw('Account', {
|
||||
fields: ['name', 'rootType', 'isGroup', 'parentAccount'],
|
||||
})
|
||||
).map((rv) => ({
|
||||
name: rv.name as string,
|
||||
rootType: rv.rootType as AccountRootType,
|
||||
isGroup: Boolean(rv.isGroup),
|
||||
parentAccount: rv.parentAccount as string | null,
|
||||
}));
|
||||
|
||||
this.accountMap = getMapFromList(accountList, 'name');
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
_getRangeMapKey(entry: LedgerEntry): DateRange | null {
|
||||
const entryDate = DateTime.fromISO(
|
||||
entry.date!.toISOString().split('T')[0]
|
||||
).toMillis();
|
||||
|
||||
for (const dr of this._dateRanges!) {
|
||||
const toDate = dr.toDate.toMillis();
|
||||
const fromDate = dr.fromDate.toMillis();
|
||||
|
||||
if (entryDate <= toDate && entryDate > fromDate) {
|
||||
return dr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async _getDateRanges(): Promise<DateRange[]> {
|
||||
const endpoints = await this._getFromAndToDates();
|
||||
const fromDate = DateTime.fromISO(endpoints.fromDate);
|
||||
const toDate = DateTime.fromISO(endpoints.toDate);
|
||||
|
||||
if (this.consolidateColumns) {
|
||||
return [
|
||||
{
|
||||
toDate,
|
||||
fromDate,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const months: number = monthsMap[this.periodicity];
|
||||
const dateRanges: DateRange[] = [
|
||||
{ toDate, fromDate: toDate.minus({ months }) },
|
||||
];
|
||||
|
||||
let count = this.count ?? 1;
|
||||
if (this.basedOn === 'Fiscal Year') {
|
||||
count = Math.ceil(((this.toYear! - this.fromYear!) * 12) / months);
|
||||
}
|
||||
|
||||
for (let i = 1; i < count; i++) {
|
||||
const lastRange = dateRanges.at(-1)!;
|
||||
dateRanges.push({
|
||||
toDate: lastRange.fromDate,
|
||||
fromDate: lastRange.fromDate.minus({ months }),
|
||||
});
|
||||
}
|
||||
|
||||
return dateRanges.sort((b, a) => b.toDate.toMillis() - a.toDate.toMillis());
|
||||
}
|
||||
|
||||
async _getFromAndToDates() {
|
||||
let toDate: string;
|
||||
let fromDate: string;
|
||||
|
||||
if (this.basedOn === 'Until Date') {
|
||||
toDate = this.toDate!;
|
||||
const months = monthsMap[this.periodicity] * Math.max(this.count ?? 1, 1);
|
||||
fromDate = DateTime.fromISO(toDate).minus({ months }).toISODate();
|
||||
} else {
|
||||
const fy = await getFiscalEndpoints(this.toYear!, this.fromYear!);
|
||||
toDate = fy.toDate;
|
||||
fromDate = fy.fromDate;
|
||||
}
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
async _getQueryFilters(): Promise<QueryFilter> {
|
||||
const filters: QueryFilter = {};
|
||||
const { fromDate, toDate } = await this._getFromAndToDates();
|
||||
|
||||
const dateFilter: string[] = [];
|
||||
dateFilter.push('<=', toDate);
|
||||
dateFilter.push('>=', fromDate);
|
||||
|
||||
filters.date = dateFilter;
|
||||
filters.reverted = false;
|
||||
return filters;
|
||||
}
|
||||
|
||||
getFilters(): Field[] {
|
||||
const periodNameMap: Record<Periodicity, string> = {
|
||||
Monthly: t`Months`,
|
||||
Quarterly: t`Quarters`,
|
||||
'Half Yearly': t`Half Years`,
|
||||
Yearly: t`Years`,
|
||||
};
|
||||
|
||||
const filters = [
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`Fiscal Year`, value: 'Fiscal Year' },
|
||||
{ label: t`Until Date`, value: 'Until Date' },
|
||||
],
|
||||
label: t`Based On`,
|
||||
fieldname: 'basedOn',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`Monthly`, value: 'Monthly' },
|
||||
{ label: t`Quarterly`, value: 'Quarterly' },
|
||||
{ label: t`Half Yearly`, value: 'Half Yearly' },
|
||||
{ label: t`Yearly`, value: 'Yearly' },
|
||||
],
|
||||
label: t`Periodicity`,
|
||||
fieldname: 'periodicity',
|
||||
},
|
||||
,
|
||||
] as Field[];
|
||||
|
||||
let dateFilters = [
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'toYear',
|
||||
placeholder: t`To Year`,
|
||||
label: t`To Year`,
|
||||
minvalue: 2000,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'fromYear',
|
||||
placeholder: t`From Year`,
|
||||
label: t`From Year`,
|
||||
minvalue: 2000,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
|
||||
if (this.basedOn === 'Until Date') {
|
||||
dateFilters = [
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toDate',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'count',
|
||||
minvalue: 1,
|
||||
placeholder: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
label: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
}
|
||||
|
||||
return [
|
||||
filters,
|
||||
dateFilters,
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: t`Consolidate Columns`,
|
||||
fieldname: 'consolidateColumns',
|
||||
} as Field,
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: t`Hide Group Balance`,
|
||||
fieldname: 'hideGroupBalance',
|
||||
} as Field,
|
||||
].flat();
|
||||
}
|
||||
|
||||
getColumns(): ColumnField[] {
|
||||
const columns = [
|
||||
{
|
||||
label: t`Account`,
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'account',
|
||||
align: 'left',
|
||||
width: ACC_NAME_WIDTH,
|
||||
},
|
||||
] as ColumnField[];
|
||||
|
||||
const dateColumns = this._dateRanges!.sort(
|
||||
(a, b) => b.toDate.toMillis() - a.toDate.toMillis()
|
||||
).map(
|
||||
(d) =>
|
||||
({
|
||||
label: this.fyo.format(d.toDate.toJSDate(), 'Date'),
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'toDate',
|
||||
align: 'right',
|
||||
width: ACC_BAL_WIDTH,
|
||||
} as ColumnField)
|
||||
);
|
||||
|
||||
return [columns, dateColumns].flat();
|
||||
}
|
||||
|
||||
getActions(): Action[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
metaFilters: string[] = ['basedOn'];
|
||||
}
|
||||
|
||||
async function getFiscalEndpoints(toYear: number, fromYear: number) {
|
||||
const fys = (await fyo.getValue(
|
||||
ModelNameEnum.AccountingSettings,
|
||||
'fiscalYearStart'
|
||||
)) as Date;
|
||||
const fye = (await fyo.getValue(
|
||||
ModelNameEnum.AccountingSettings,
|
||||
'fiscalYearEnd'
|
||||
)) as Date;
|
||||
|
||||
/**
|
||||
* Get the month and the day, and
|
||||
* prepend with the passed year.
|
||||
*/
|
||||
|
||||
const fromDate = [
|
||||
fromYear,
|
||||
fys.toISOString().split('T')[0].split('-').slice(1),
|
||||
]
|
||||
.flat()
|
||||
.join('-');
|
||||
|
||||
const toDate = [toYear, fye.toISOString().split('T')[0].split('-').slice(1)]
|
||||
.flat()
|
||||
.join('-');
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
const monthsMap: Record<Periodicity, number> = {
|
||||
Monthly: 1,
|
||||
Quarterly: 3,
|
||||
'Half Yearly': 6,
|
||||
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;
|
||||
|
||||
while (parentAccountName !== null) {
|
||||
parentAccountName = updateParentAccountWithChildValues(
|
||||
accountTree,
|
||||
parentAccountName,
|
||||
valueMap
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateParentAccountWithChildValues(
|
||||
accountTree: AccountTree,
|
||||
parentAccountName: string,
|
||||
valueMap: ValueMap
|
||||
): string {
|
||||
const parentAccount = accountTree[parentAccountName];
|
||||
parentAccount.prune = false;
|
||||
parentAccount.valueMap ??= new Map();
|
||||
|
||||
for (const key of valueMap.keys()) {
|
||||
const value = parentAccount.valueMap!.get(key) ?? 0;
|
||||
parentAccount.valueMap!.set(key, value + valueMap.get(key)!);
|
||||
}
|
||||
|
||||
return parentAccount.parentAccount!;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
export function convertAccountRootNodeToAccountList(
|
||||
rootNode: AccountTreeNode
|
||||
): AccountList {
|
||||
if (!rootNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const accountList: AccountList = [];
|
||||
pushToAccountList(rootNode, accountList, 0);
|
||||
return accountList;
|
||||
}
|
||||
|
||||
function pushToAccountList(
|
||||
accountTreeNode: AccountTreeNode,
|
||||
accountList: AccountList,
|
||||
level: number
|
||||
) {
|
||||
accountList.push({
|
||||
name: accountTreeNode.name,
|
||||
rootType: accountTreeNode.rootType,
|
||||
isGroup: accountTreeNode.isGroup,
|
||||
parentAccount: accountTreeNode.parentAccount,
|
||||
valueMap: accountTreeNode.valueMap,
|
||||
level,
|
||||
});
|
||||
|
||||
const children = accountTreeNode.children ?? [];
|
||||
const childLevel = level + 1;
|
||||
|
||||
for (const childNode of children) {
|
||||
pushToAccountList(childNode, accountList, childLevel);
|
||||
}
|
||||
}
|
||||
|
||||
function getListOfLeafNodes(tree: Tree): TreeNode[] {
|
||||
const nonGroupChildren: TreeNode[] = [];
|
||||
for (const node of Object.values(tree)) {
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupChildren = node.children ?? [];
|
||||
|
||||
while (groupChildren.length) {
|
||||
const child = groupChildren.shift()!;
|
||||
if (!child?.children?.length) {
|
||||
nonGroupChildren.push(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
groupChildren.unshift(...(child.children ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
return nonGroupChildren;
|
||||
}
|
@ -1,59 +1,106 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { unique } from 'fyo/utils';
|
||||
import { FinancialStatements } from 'reports/FinancialStatements/financialStatements';
|
||||
import { FinancialStatementOptions } from 'reports/types';
|
||||
import { t } from 'fyo';
|
||||
import {
|
||||
AccountRootType,
|
||||
AccountRootTypeEnum,
|
||||
} from 'models/baseModels/Account/types';
|
||||
import {
|
||||
AccountReport,
|
||||
convertAccountRootNodeToAccountList,
|
||||
} from 'reports/AccountReport';
|
||||
import { AccountTreeNode, ReportData } from 'reports/types';
|
||||
import { getMapFromList } from 'utils';
|
||||
|
||||
class BalanceSheet {
|
||||
async run(options: FinancialStatementOptions, fyo: Fyo) {
|
||||
const { fromDate, toDate, periodicity } = options;
|
||||
const fs = new FinancialStatements(fyo);
|
||||
const asset = await fs.getData({
|
||||
rootType: 'Asset',
|
||||
balanceMustBe: 'Debit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
type RootTypeRow = {
|
||||
rootType: AccountRootType;
|
||||
rootNode: AccountTreeNode;
|
||||
rows: ReportData;
|
||||
};
|
||||
|
||||
const liability = await fs.getData({
|
||||
rootType: 'Liability',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
export class BalanceSheet extends AccountReport {
|
||||
static title = t`Balance Sheet`;
|
||||
static reportName = 'balance-sheet';
|
||||
|
||||
const equity = await fs.getData({
|
||||
rootType: 'Equity',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
get rootTypes(): AccountRootType[] {
|
||||
return [
|
||||
AccountRootTypeEnum.Asset,
|
||||
AccountRootTypeEnum.Liability,
|
||||
AccountRootTypeEnum.Equity,
|
||||
];
|
||||
}
|
||||
|
||||
const rows = [
|
||||
...asset.accounts,
|
||||
asset.totalRow,
|
||||
[],
|
||||
...liability.accounts,
|
||||
liability.totalRow,
|
||||
[],
|
||||
...equity.accounts,
|
||||
equity.totalRow,
|
||||
[],
|
||||
async setReportData(filter?: string) {
|
||||
if (filter !== 'hideGroupBalance') {
|
||||
await this._setRawData();
|
||||
}
|
||||
|
||||
const map = this._getGroupedMap(true, 'account');
|
||||
const rangeGroupedMap = await this._getGroupedByDateRanges(map);
|
||||
const accountTree = await this._getAccountTree(rangeGroupedMap);
|
||||
|
||||
for (const name of Object.keys(accountTree)) {
|
||||
const { rootType } = accountTree[name];
|
||||
if (this.rootTypes.includes(rootType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete accountTree[name];
|
||||
}
|
||||
|
||||
const rootTypeRows: RootTypeRow[] = this.rootTypes
|
||||
.map((rootType) => {
|
||||
const rootNode = this.getRootNode(rootType, accountTree)!;
|
||||
const rootList = convertAccountRootNodeToAccountList(rootNode);
|
||||
return {
|
||||
rootType,
|
||||
rootNode,
|
||||
rows: this.getReportRowsFromAccountList(rootList),
|
||||
};
|
||||
})
|
||||
.filter((row) => !!row.rootNode);
|
||||
|
||||
this.reportData = await this.getReportDataFromRows(
|
||||
getMapFromList(rootTypeRows, 'rootType')
|
||||
);
|
||||
}
|
||||
|
||||
async getReportDataFromRows(
|
||||
rootTypeRows: Record<AccountRootType, RootTypeRow | undefined>
|
||||
): Promise<ReportData> {
|
||||
const typeNameList = [
|
||||
{
|
||||
rootType: AccountRootTypeEnum.Asset,
|
||||
totalName: t`Total Asset (Debit)`,
|
||||
},
|
||||
{
|
||||
rootType: AccountRootTypeEnum.Liability,
|
||||
totalName: t`Total Liability (Credit)`,
|
||||
},
|
||||
{
|
||||
rootType: AccountRootTypeEnum.Equity,
|
||||
totalName: t`Total Equity (Credit)`,
|
||||
},
|
||||
];
|
||||
|
||||
const columns = unique([
|
||||
...asset.periodList,
|
||||
...liability.periodList,
|
||||
...equity.periodList,
|
||||
]);
|
||||
const reportData: ReportData = [];
|
||||
const emptyRow = this.getEmptyRow();
|
||||
for (const { rootType, totalName } of typeNameList) {
|
||||
const row = rootTypeRows[rootType];
|
||||
if (!row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return { rows, columns };
|
||||
const totalNode = await this.getTotalNode(row.rootNode, totalName);
|
||||
const totalRow = this.getRowFromAccountListNode(totalNode);
|
||||
|
||||
reportData.push(...row.rows);
|
||||
reportData.push(totalRow);
|
||||
reportData.push(emptyRow);
|
||||
}
|
||||
|
||||
if (reportData.at(-1)?.isEmpty) {
|
||||
reportData.pop();
|
||||
}
|
||||
|
||||
return reportData;
|
||||
}
|
||||
}
|
||||
|
||||
export default BalanceSheet;
|
||||
|
@ -1,56 +0,0 @@
|
||||
import { t } from 'fyo';
|
||||
import getCommonExportActions from '../commonExporter';
|
||||
import { fyo } from 'src/initFyo';
|
||||
|
||||
const periodicityMap = {
|
||||
Monthly: t`Monthly`,
|
||||
Quarterly: t`Quarterly`,
|
||||
'Half Yearly': t`Half Yearly`,
|
||||
Yearly: t`Yearly`,
|
||||
};
|
||||
export default {
|
||||
title: t`Balance Sheet`,
|
||||
method: 'balance-sheet',
|
||||
filterFields: [
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toDate',
|
||||
size: 'small',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
required: 1,
|
||||
default: async () => {
|
||||
return (await fyo.doc.getSingle('AccountingSettings')).fiscalYearEnd;
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
placeholder: t`Select Period`,
|
||||
size: 'small',
|
||||
options: Object.keys(periodicityMap),
|
||||
map: periodicityMap,
|
||||
label: t`Periodicity`,
|
||||
fieldname: 'periodicity',
|
||||
default: 'Monthly',
|
||||
},
|
||||
],
|
||||
actions: getCommonExportActions('balance-sheet'),
|
||||
getColumns({ data }) {
|
||||
const columns = [
|
||||
{ label: t`Account`, fieldtype: 'Data', fieldname: 'account', width: 2 },
|
||||
];
|
||||
|
||||
if (data && data.columns) {
|
||||
const currencyColumns = data.columns;
|
||||
const columnDefs = currencyColumns.map((name) => ({
|
||||
label: name,
|
||||
fieldname: name,
|
||||
fieldtype: 'Currency',
|
||||
}));
|
||||
|
||||
columns.push(...columnDefs);
|
||||
}
|
||||
|
||||
return columns;
|
||||
},
|
||||
};
|
@ -104,6 +104,7 @@ export class GeneralLedger extends LedgerReport {
|
||||
_getRowFromEntry(entry: LedgerEntry, columns: ColumnField[]): ReportRow {
|
||||
if (entry.name === -3) {
|
||||
return {
|
||||
isEmpty: true,
|
||||
cells: columns.map((c) => ({
|
||||
rawValue: '',
|
||||
value: '',
|
||||
|
@ -1,159 +1,80 @@
|
||||
import { t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
AccountRootType,
|
||||
AccountRootTypeEnum
|
||||
AccountRootTypeEnum,
|
||||
} from 'models/baseModels/Account/types';
|
||||
import { isCredit } from 'models/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { LedgerReport } from 'reports/LedgerReport';
|
||||
import {
|
||||
ColumnField,
|
||||
GroupedMap,
|
||||
LedgerEntry,
|
||||
Periodicity,
|
||||
ReportCell,
|
||||
ReportData,
|
||||
ReportRow
|
||||
} from 'reports/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getMapFromList } from 'utils';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
AccountReport,
|
||||
convertAccountRootNodeToAccountList,
|
||||
} from 'reports/AccountReport';
|
||||
import { AccountListNode, AccountTreeNode, ReportData } from 'reports/types';
|
||||
|
||||
type DateRange = { fromDate: DateTime; toDate: DateTime };
|
||||
type ValueMap = Map<DateRange, number>;
|
||||
type AccountNameValueMapMap = Map<string, ValueMap>;
|
||||
type BasedOn = 'Fiscal Year' | 'Until Date';
|
||||
|
||||
interface Account {
|
||||
name: string;
|
||||
rootType: AccountRootType;
|
||||
isGroup: boolean;
|
||||
parentAccount: string | null;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
type Tree = Record<string, TreeNode>;
|
||||
|
||||
type AccountTree = Record<string, AccountTreeNode>;
|
||||
interface AccountTreeNode extends Account {
|
||||
children?: AccountTreeNode[];
|
||||
valueMap?: ValueMap;
|
||||
prune?: boolean;
|
||||
}
|
||||
|
||||
type AccountList = AccountListNode[];
|
||||
interface AccountListNode extends Account {
|
||||
valueMap?: ValueMap;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const PNL_ROOT_TYPES: AccountRootType[] = [
|
||||
AccountRootTypeEnum.Income,
|
||||
AccountRootTypeEnum.Expense,
|
||||
];
|
||||
|
||||
const ACC_NAME_WIDTH = 2;
|
||||
const ACC_BAL_WIDTH = 1;
|
||||
|
||||
export class ProfitAndLoss extends LedgerReport {
|
||||
export class ProfitAndLoss extends AccountReport {
|
||||
static title = t`Profit And Loss`;
|
||||
static reportName = 'profit-and-loss';
|
||||
|
||||
toDate?: string;
|
||||
count: number = 3;
|
||||
fromYear?: number;
|
||||
toYear?: number;
|
||||
singleColumn: boolean = false;
|
||||
hideGroupBalance: boolean = false;
|
||||
periodicity: Periodicity = 'Monthly';
|
||||
basedOn: BasedOn = 'Until Date';
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
_dateRanges?: DateRange[];
|
||||
|
||||
accountMap?: Record<string, Account>;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
await this._setDateRanges();
|
||||
}
|
||||
|
||||
async setDefaultFilters(): Promise<void> {
|
||||
if (this.basedOn === 'Until Date' && !this.toDate) {
|
||||
this.toDate = DateTime.now().toISODate();
|
||||
}
|
||||
|
||||
if (this.basedOn === 'Fiscal Year' && !this.toYear) {
|
||||
this.fromYear = DateTime.now().year;
|
||||
this.toYear = this.fromYear + 1;
|
||||
}
|
||||
|
||||
await this._setDateRanges();
|
||||
}
|
||||
|
||||
async _setDateRanges() {
|
||||
this._dateRanges = await this._getDateRanges();
|
||||
get rootTypes(): AccountRootType[] {
|
||||
return [AccountRootTypeEnum.Income, AccountRootTypeEnum.Expense];
|
||||
}
|
||||
|
||||
async setReportData(filter?: string) {
|
||||
await this._setRawData();
|
||||
if (filter !== 'hideGroupBalance') {
|
||||
await this._setRawData();
|
||||
}
|
||||
|
||||
const map = this._getGroupedMap(true, 'account');
|
||||
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)) {
|
||||
if (this.rootTypes.includes(rootType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete accountTree[name];
|
||||
}
|
||||
|
||||
const rootNodeList = Object.values(accountTree);
|
||||
const incomeTree = {
|
||||
[AccountRootTypeEnum.Income]: rootNodeList.find(
|
||||
(n) => n.rootType === AccountRootTypeEnum.Income
|
||||
)!,
|
||||
};
|
||||
const expenseTree = {
|
||||
[AccountRootTypeEnum.Expense]: rootNodeList.find(
|
||||
(n) => n.rootType === AccountRootTypeEnum.Expense
|
||||
)!,
|
||||
};
|
||||
/**
|
||||
* Income Rows
|
||||
*/
|
||||
const incomeRoot = this.getRootNode(
|
||||
AccountRootTypeEnum.Income,
|
||||
accountTree
|
||||
)!;
|
||||
const incomeList = convertAccountRootNodeToAccountList(incomeRoot);
|
||||
const incomeRows = this.getReportRowsFromAccountList(incomeList);
|
||||
|
||||
/**
|
||||
* Expense Rows
|
||||
*/
|
||||
const expenseRoot = this.getRootNode(
|
||||
AccountRootTypeEnum.Expense,
|
||||
accountTree
|
||||
)!;
|
||||
const expenseList = convertAccountRootNodeToAccountList(expenseRoot);
|
||||
const expenseRows = this.getReportRowsFromAccountList(expenseList);
|
||||
|
||||
const incomeList = convertAccountTreeToAccountList(incomeTree);
|
||||
const expenseList = convertAccountTreeToAccountList(expenseTree);
|
||||
const incomeRows = this.getPnlRowsFromAccountList(incomeList);
|
||||
const expenseRows = this.getPnlRowsFromAccountList(expenseList);
|
||||
this.reportData = await this.getReportDataFromRows(
|
||||
incomeRows,
|
||||
expenseRows,
|
||||
incomeTree,
|
||||
expenseTree
|
||||
incomeRoot,
|
||||
expenseRoot
|
||||
);
|
||||
}
|
||||
|
||||
async getReportDataFromRows(
|
||||
incomeRows: ReportData,
|
||||
expenseRows: ReportData,
|
||||
incomeTree: AccountTree,
|
||||
expenseTree: AccountTree
|
||||
incomeRoot: AccountTreeNode,
|
||||
expenseRoot: AccountTreeNode
|
||||
): Promise<ReportData> {
|
||||
const totalIncome = await this.getTotalNode(
|
||||
incomeTree,
|
||||
incomeRoot,
|
||||
t`Total Income (Credit)`
|
||||
);
|
||||
const totalExpense = await this.getTotalNode(
|
||||
expenseTree,
|
||||
expenseRoot,
|
||||
t`Total Expense (Debit)`
|
||||
);
|
||||
|
||||
@ -187,7 +108,7 @@ export class ProfitAndLoss extends LedgerReport {
|
||||
}
|
||||
});
|
||||
|
||||
const emptyRow = await this.getEmptyRow();
|
||||
const emptyRow = this.getEmptyRow();
|
||||
|
||||
return [
|
||||
incomeRows,
|
||||
@ -200,541 +121,4 @@ export class ProfitAndLoss extends LedgerReport {
|
||||
totalProfitRow,
|
||||
].flat() as ReportData;
|
||||
}
|
||||
|
||||
async getEmptyRow(): Promise<ReportRow> {
|
||||
const columns = await this.getColumns();
|
||||
return {
|
||||
cells: columns.map(
|
||||
(c) =>
|
||||
({
|
||||
value: '',
|
||||
rawValue: '',
|
||||
width: c.width,
|
||||
align: 'left',
|
||||
} as ReportCell)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalNode(
|
||||
accountTree: AccountTree,
|
||||
name: string
|
||||
): Promise<AccountListNode> {
|
||||
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
|
||||
|
||||
const totalMap = leafNodes.reduce((acc, node) => {
|
||||
for (const key of this._dateRanges!) {
|
||||
const bal = acc.get(key) ?? 0;
|
||||
const val = node.valueMap?.get(key) ?? 0;
|
||||
acc.set(key, bal + val);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map() as ValueMap);
|
||||
|
||||
return { name, valueMap: totalMap, level: 0 } as AccountListNode;
|
||||
}
|
||||
|
||||
getPnlRowsFromAccountList(accountList: AccountList): ReportData {
|
||||
return accountList.map((al) => {
|
||||
return this.getRowFromAccountListNode(al);
|
||||
});
|
||||
}
|
||||
|
||||
getRowFromAccountListNode(al: AccountListNode) {
|
||||
const nameCell = {
|
||||
value: al.name,
|
||||
rawValue: al.name,
|
||||
align: 'left',
|
||||
width: ACC_NAME_WIDTH,
|
||||
bold: !al.level,
|
||||
italics: al.isGroup,
|
||||
indent: al.level ?? 0,
|
||||
} as ReportCell;
|
||||
|
||||
const balanceCells = this._dateRanges!.map((k) => {
|
||||
const rawValue = al.valueMap?.get(k) ?? 0;
|
||||
let value = this.fyo.format(rawValue, 'Currency');
|
||||
if (this.hideGroupBalance && al.isGroup) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
return {
|
||||
rawValue,
|
||||
value,
|
||||
align: 'right',
|
||||
width: ACC_BAL_WIDTH,
|
||||
} as ReportCell;
|
||||
});
|
||||
|
||||
return {
|
||||
cells: [nameCell, balanceCells].flat(),
|
||||
level: al.level,
|
||||
isGroup: !!al.isGroup,
|
||||
folded: false,
|
||||
foldedBelow: false,
|
||||
} as ReportRow;
|
||||
}
|
||||
|
||||
async _getGroupedByDateRanges(
|
||||
map: GroupedMap
|
||||
): Promise<AccountNameValueMapMap> {
|
||||
const accountValueMap: AccountNameValueMapMap = new Map();
|
||||
const accountMap = await this._getAccountMap();
|
||||
|
||||
for (const account of map.keys()) {
|
||||
const valueMap: ValueMap = new Map();
|
||||
|
||||
/**
|
||||
* Set Balance for every DateRange key
|
||||
*/
|
||||
for (const entry of map.get(account)!) {
|
||||
const key = this._getRangeMapKey(entry);
|
||||
if (key === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const totalBalance = valueMap.get(key!) ?? 0;
|
||||
const balance = (entry.debit ?? 0) - (entry.credit ?? 0);
|
||||
const rootType = accountMap[entry.account].rootType;
|
||||
|
||||
if (isCredit(rootType)) {
|
||||
valueMap.set(key!, totalBalance - balance);
|
||||
} else {
|
||||
valueMap.set(key!, totalBalance + balance);
|
||||
}
|
||||
}
|
||||
accountValueMap.set(account, valueMap);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this.accountMap) {
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
const accountList: Account[] = (
|
||||
await this.fyo.db.getAllRaw('Account', {
|
||||
fields: ['name', 'rootType', 'isGroup', 'parentAccount'],
|
||||
})
|
||||
).map((rv) => ({
|
||||
name: rv.name as string,
|
||||
rootType: rv.rootType as AccountRootType,
|
||||
isGroup: Boolean(rv.isGroup),
|
||||
parentAccount: rv.parentAccount as string | null,
|
||||
}));
|
||||
|
||||
this.accountMap = getMapFromList(accountList, 'name');
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
_getRangeMapKey(entry: LedgerEntry): DateRange | null {
|
||||
const entryDate = DateTime.fromISO(
|
||||
entry.date!.toISOString().split('T')[0]
|
||||
).toMillis();
|
||||
|
||||
for (const dr of this._dateRanges!) {
|
||||
const toDate = dr.toDate.toMillis();
|
||||
const fromDate = dr.fromDate.toMillis();
|
||||
|
||||
if (entryDate <= toDate && entryDate > fromDate) {
|
||||
return dr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async _getDateRanges(): Promise<DateRange[]> {
|
||||
const endpoints = await this._getFromAndToDates();
|
||||
const fromDate = DateTime.fromISO(endpoints.fromDate);
|
||||
const toDate = DateTime.fromISO(endpoints.toDate);
|
||||
|
||||
if (this.singleColumn) {
|
||||
return [
|
||||
{
|
||||
toDate,
|
||||
fromDate,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const months: number = monthsMap[this.periodicity];
|
||||
const dateRanges: DateRange[] = [
|
||||
{ toDate, fromDate: toDate.minus({ months }) },
|
||||
];
|
||||
|
||||
let count = this.count ?? 1;
|
||||
if (this.basedOn === 'Fiscal Year') {
|
||||
count = Math.ceil(((this.toYear! - this.fromYear!) * 12) / months);
|
||||
}
|
||||
|
||||
for (let i = 1; i < count; i++) {
|
||||
const lastRange = dateRanges.at(-1)!;
|
||||
dateRanges.push({
|
||||
toDate: lastRange.fromDate,
|
||||
fromDate: lastRange.fromDate.minus({ months }),
|
||||
});
|
||||
}
|
||||
|
||||
return dateRanges.sort((b, a) => b.toDate.toMillis() - a.toDate.toMillis());
|
||||
}
|
||||
|
||||
async _getFromAndToDates() {
|
||||
let toDate: string;
|
||||
let fromDate: string;
|
||||
|
||||
if (this.basedOn === 'Until Date') {
|
||||
toDate = this.toDate!;
|
||||
const months = monthsMap[this.periodicity] * Math.max(this.count ?? 1, 1);
|
||||
fromDate = DateTime.fromISO(toDate).minus({ months }).toISODate();
|
||||
} else {
|
||||
const fy = await getFiscalEndpoints(this.toYear!, this.fromYear!);
|
||||
toDate = fy.toDate;
|
||||
fromDate = fy.fromDate;
|
||||
}
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
async _getQueryFilters(): Promise<QueryFilter> {
|
||||
const filters: QueryFilter = {};
|
||||
const { fromDate, toDate } = await this._getFromAndToDates();
|
||||
|
||||
const dateFilter: string[] = [];
|
||||
dateFilter.push('<=', toDate);
|
||||
dateFilter.push('>=', fromDate);
|
||||
|
||||
filters.date = dateFilter;
|
||||
filters.reverted = false;
|
||||
return filters;
|
||||
}
|
||||
|
||||
getFilters(): Field[] {
|
||||
const periodNameMap: Record<Periodicity, string> = {
|
||||
Monthly: t`Months`,
|
||||
Quarterly: t`Quarters`,
|
||||
'Half Yearly': t`Half Years`,
|
||||
Yearly: t`Years`,
|
||||
};
|
||||
|
||||
const filters = [
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`Fiscal Year`, value: 'Fiscal Year' },
|
||||
{ label: t`Until Date`, value: 'Until Date' },
|
||||
],
|
||||
label: t`Based On`,
|
||||
fieldname: 'basedOn',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`Monthly`, value: 'Monthly' },
|
||||
{ label: t`Quarterly`, value: 'Quarterly' },
|
||||
{ label: t`Half Yearly`, value: 'Half Yearly' },
|
||||
{ label: t`Yearly`, value: 'Yearly' },
|
||||
],
|
||||
label: t`Periodicity`,
|
||||
fieldname: 'periodicity',
|
||||
},
|
||||
,
|
||||
] as Field[];
|
||||
|
||||
let dateFilters = [
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'toYear',
|
||||
placeholder: t`To Year`,
|
||||
label: t`To Year`,
|
||||
minvalue: 2000,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'fromYear',
|
||||
placeholder: t`From Year`,
|
||||
label: t`From Year`,
|
||||
minvalue: 2000,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
|
||||
if (this.basedOn === 'Until Date') {
|
||||
dateFilters = [
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toDate',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'count',
|
||||
minvalue: 1,
|
||||
placeholder: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
label: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
}
|
||||
|
||||
return [
|
||||
filters,
|
||||
dateFilters,
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: t`Single Column`,
|
||||
fieldname: 'singleColumn',
|
||||
} as Field,
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: t`Hide Group Balance`,
|
||||
fieldname: 'hideGroupBalance',
|
||||
} as Field,
|
||||
].flat();
|
||||
}
|
||||
|
||||
getColumns(): ColumnField[] {
|
||||
const columns = [
|
||||
{
|
||||
label: t`Account`,
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'account',
|
||||
align: 'left',
|
||||
width: ACC_NAME_WIDTH,
|
||||
},
|
||||
] as ColumnField[];
|
||||
|
||||
const dateColumns = this._dateRanges!.sort(
|
||||
(a, b) => b.toDate.toMillis() - a.toDate.toMillis()
|
||||
).map(
|
||||
(d) =>
|
||||
({
|
||||
label: this.fyo.format(d.toDate.toJSDate(), 'Date'),
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'toDate',
|
||||
align: 'right',
|
||||
width: ACC_BAL_WIDTH,
|
||||
} as ColumnField)
|
||||
);
|
||||
|
||||
return [columns, dateColumns].flat();
|
||||
}
|
||||
|
||||
getActions(): Action[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
metaFilters: string[] = ['basedOn'];
|
||||
}
|
||||
|
||||
async function getFiscalEndpoints(toYear: number, fromYear: number) {
|
||||
const fys = (await fyo.getValue(
|
||||
ModelNameEnum.AccountingSettings,
|
||||
'fiscalYearStart'
|
||||
)) as Date;
|
||||
const fye = (await fyo.getValue(
|
||||
ModelNameEnum.AccountingSettings,
|
||||
'fiscalYearEnd'
|
||||
)) as Date;
|
||||
|
||||
/**
|
||||
* Get the month and the day, and
|
||||
* prepend with the passed year.
|
||||
*/
|
||||
|
||||
const fromDate = [
|
||||
fromYear,
|
||||
fys.toISOString().split('T')[0].split('-').slice(1),
|
||||
]
|
||||
.flat()
|
||||
.join('-');
|
||||
|
||||
const toDate = [toYear, fye.toISOString().split('T')[0].split('-').slice(1)]
|
||||
.flat()
|
||||
.join('-');
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
const monthsMap: Record<Periodicity, number> = {
|
||||
Monthly: 1,
|
||||
Quarterly: 3,
|
||||
'Half Yearly': 6,
|
||||
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;
|
||||
|
||||
while (parentAccountName !== null) {
|
||||
parentAccountName = updateParentAccountWithChildValues(
|
||||
accountTree,
|
||||
parentAccountName,
|
||||
valueMap
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateParentAccountWithChildValues(
|
||||
accountTree: AccountTree,
|
||||
parentAccountName: string,
|
||||
valueMap: ValueMap
|
||||
): string {
|
||||
const parentAccount = accountTree[parentAccountName];
|
||||
parentAccount.prune = false;
|
||||
parentAccount.valueMap ??= new Map();
|
||||
|
||||
for (const key of valueMap.keys()) {
|
||||
const value = parentAccount.valueMap!.get(key) ?? 0;
|
||||
parentAccount.valueMap!.set(key, value + valueMap.get(key)!);
|
||||
}
|
||||
|
||||
return parentAccount.parentAccount!;
|
||||
}
|
||||
|
||||
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(
|
||||
accountTree: AccountTree
|
||||
): AccountList {
|
||||
const accountList: AccountList = [];
|
||||
|
||||
for (const rootNode of Object.values(accountTree)) {
|
||||
if (!rootNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pushToAccountList(rootNode, accountList, 0);
|
||||
}
|
||||
|
||||
return accountList;
|
||||
}
|
||||
|
||||
function pushToAccountList(
|
||||
accountTreeNode: AccountTreeNode,
|
||||
accountList: AccountList,
|
||||
level: number
|
||||
) {
|
||||
accountList.push({
|
||||
name: accountTreeNode.name,
|
||||
rootType: accountTreeNode.rootType,
|
||||
isGroup: accountTreeNode.isGroup,
|
||||
parentAccount: accountTreeNode.parentAccount,
|
||||
valueMap: accountTreeNode.valueMap,
|
||||
level,
|
||||
});
|
||||
|
||||
const children = accountTreeNode.children ?? [];
|
||||
const childLevel = level + 1;
|
||||
|
||||
for (const childNode of children) {
|
||||
pushToAccountList(childNode, accountList, childLevel);
|
||||
}
|
||||
}
|
||||
|
||||
function getListOfLeafNodes(tree: Tree): TreeNode[] {
|
||||
const nonGroupChildren: TreeNode[] = [];
|
||||
for (const node of Object.values(tree)) {
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupChildren = node.children ?? [];
|
||||
|
||||
while (groupChildren.length) {
|
||||
const child = groupChildren.shift()!;
|
||||
if (!child?.children?.length) {
|
||||
nonGroupChildren.push(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
groupChildren.unshift(...(child.children ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
return nonGroupChildren;
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
import AccountsReceivablePayable from './AccountsReceivablePayable/AccountsReceivablePayable';
|
||||
import BalanceSheet from './BalanceSheet/BalanceSheet';
|
||||
import BankReconciliation from './BankReconciliation/BankReconciliation';
|
||||
import GeneralLedger from './GeneralLedger/GeneralLedger';
|
||||
import GSTR1 from './GoodsAndServiceTax/GSTR1';
|
||||
import GSTR2 from './GoodsAndServiceTax/GSTR2';
|
||||
import ProfitAndLoss from './ProfitAndLoss/ProfitAndLoss';
|
||||
import PurchaseRegister from './PurchaseRegister/PurchaseRegister';
|
||||
import SalesRegister from './SalesRegister/SalesRegister';
|
||||
import TrialBalance from './TrialBalance/TrialBalance';
|
||||
|
||||
export function getReportData(method, filters) {
|
||||
const reports = {
|
||||
'general-ledger': GeneralLedger,
|
||||
'profit-and-loss': ProfitAndLoss,
|
||||
'balance-sheet': BalanceSheet,
|
||||
'trial-balance': TrialBalance,
|
||||
'gstr-1': GSTR1,
|
||||
'gstr-2': GSTR2,
|
||||
'sales-register': SalesRegister,
|
||||
'purchase-register': PurchaseRegister,
|
||||
'bank-reconciliation': BankReconciliation,
|
||||
};
|
||||
|
||||
if (method === 'accounts-receivable') {
|
||||
return new AccountsReceivablePayable().run('Receivable', filters);
|
||||
}
|
||||
|
||||
if (method === 'accounts-payable') {
|
||||
return new AccountsReceivablePayable().run('Payable', filters);
|
||||
}
|
||||
|
||||
const ReportClass = reports[method];
|
||||
return new ReportClass().run(filters);
|
||||
}
|
||||
*/
|
||||
export function getReportData(method, filters) {
|
||||
return { rows: [], columns: [] };
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { BalanceSheet } from './BalanceSheet/BalanceSheet';
|
||||
import { GeneralLedger } from './GeneralLedger/GeneralLedger';
|
||||
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
|
||||
|
||||
export const reports = { GeneralLedger, ProfitAndLoss };
|
||||
export const reports = { GeneralLedger, ProfitAndLoss, BalanceSheet };
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { AccountRootType } from 'models/baseModels/Account/types';
|
||||
import { BaseField, RawValue } from 'schemas/types';
|
||||
|
||||
@ -18,6 +19,7 @@ export interface ReportRow {
|
||||
cells: ReportCell[];
|
||||
level?: number;
|
||||
isGroup?: boolean;
|
||||
isEmpty?: boolean;
|
||||
folded?: boolean;
|
||||
foldedBelow?: boolean;
|
||||
}
|
||||
@ -68,3 +70,39 @@ export interface LedgerEntry {
|
||||
}
|
||||
|
||||
export type GroupedMap = Map<string, LedgerEntry[]>;
|
||||
|
||||
export type DateRange = { fromDate: DateTime; toDate: DateTime };
|
||||
export type ValueMap = Map<DateRange, number>;
|
||||
|
||||
export interface Account {
|
||||
name: string;
|
||||
rootType: AccountRootType;
|
||||
isGroup: boolean;
|
||||
parentAccount: string | null;
|
||||
}
|
||||
|
||||
export type AccountTree = Record<string, AccountTreeNode>;
|
||||
export interface AccountTreeNode extends Account {
|
||||
children?: AccountTreeNode[];
|
||||
valueMap?: ValueMap;
|
||||
prune?: boolean;
|
||||
}
|
||||
|
||||
export type AccountList = AccountListNode[];
|
||||
export interface AccountListNode extends Account {
|
||||
valueMap?: ValueMap;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export type AccountNameValueMapMap = Map<string, ValueMap>;
|
||||
export type BasedOn = 'Fiscal Year' | 'Until Date';
|
||||
|
||||
|
||||
export interface TreeNode {
|
||||
name: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
export type Tree = Record<string, TreeNode>;
|
||||
|
||||
|
||||
|
@ -148,12 +148,12 @@ function getCompleteSidebar(): SidebarConfig {
|
||||
name: 'profit-and-loss',
|
||||
route: '/report/ProfitAndLoss',
|
||||
},
|
||||
/*
|
||||
{
|
||||
label: t`Balance Sheet`,
|
||||
name: 'balance-sheet',
|
||||
route: '/report/balance-sheet',
|
||||
route: '/report/BalanceSheet',
|
||||
},
|
||||
/*
|
||||
{
|
||||
label: t`Trial Balance`,
|
||||
name: 'trial-balance',
|
||||
|
Loading…
Reference in New Issue
Block a user