diff --git a/reports/AccountReport.ts b/reports/AccountReport.ts index 0d8e2bc6..b3ee0a03 100644 --- a/reports/AccountReport.ts +++ b/reports/AccountReport.ts @@ -31,8 +31,8 @@ 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 const ACC_NAME_WIDTH = 2; +export const ACC_BAL_WIDTH = 1; export abstract class AccountReport extends LedgerReport { toDate?: string; @@ -40,7 +40,7 @@ export abstract class AccountReport extends LedgerReport { fromYear?: number; toYear?: number; consolidateColumns: boolean = false; - hideGroupBalance: boolean = false; + hideGroupAmounts: boolean = false; periodicity: Periodicity = 'Monthly'; basedOn: BasedOn = 'Until Date'; @@ -48,12 +48,6 @@ export abstract class AccountReport extends LedgerReport { _dateRanges?: DateRange[]; accountMap?: Record; - abstract get rootTypes(): AccountRootType[]; - - async initialize(): Promise { - await super.initialize(); - await this._setDateRanges(); - } async setDefaultFilters(): Promise { if (this.basedOn === 'Until Date' && !this.toDate) { @@ -105,9 +99,9 @@ export abstract class AccountReport extends LedgerReport { 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); + const bal = acc.get(key)?.balance ?? 0; + const val = node.valueMap?.get(key)?.balance ?? 0; + acc.set(key, { balance: bal + val }); } return acc; @@ -134,9 +128,9 @@ export abstract class AccountReport extends LedgerReport { } as ReportCell; const balanceCells = this._dateRanges!.map((k) => { - const rawValue = al.valueMap?.get(k) ?? 0; + const rawValue = al.valueMap?.get(k)?.balance ?? 0; let value = this.fyo.format(rawValue, 'Currency'); - if (this.hideGroupBalance && al.isGroup) { + if (this.hideGroupAmounts && al.isGroup) { value = ''; } @@ -175,14 +169,14 @@ export abstract class AccountReport extends LedgerReport { continue; } - const totalBalance = valueMap.get(key!) ?? 0; + const totalBalance = valueMap.get(key!)?.balance ?? 0; const balance = (entry.debit ?? 0) - (entry.credit ?? 0); const rootType = accountMap[entry.account].rootType; if (isCredit(rootType)) { - valueMap.set(key!, totalBalance - balance); + valueMap.set(key!, { balance: totalBalance - balance }); } else { - valueMap.set(key!, totalBalance + balance); + valueMap.set(key!, { balance: totalBalance + balance }); } } accountValueMap.set(account, valueMap); @@ -232,7 +226,7 @@ export abstract class AccountReport extends LedgerReport { const toDate = dr.toDate.toMillis(); const fromDate = dr.fromDate.toMillis(); - if (entryDate <= toDate && entryDate > fromDate) { + if (fromDate < entryDate && entryDate <= toDate) { return dr; } } @@ -386,8 +380,8 @@ export abstract class AccountReport extends LedgerReport { } as Field, { fieldtype: 'Check', - label: t`Hide Group Balance`, - fieldname: 'hideGroupBalance', + label: t`Hide Group Amounts`, + fieldname: 'hideGroupAmounts', } as Field, ].flat(); } @@ -426,7 +420,7 @@ export abstract class AccountReport extends LedgerReport { metaFilters: string[] = ['basedOn']; } -async function getFiscalEndpoints(toYear: number, fromYear: number) { +export async function getFiscalEndpoints(toYear: number, fromYear: number) { const fys = (await fyo.getValue( ModelNameEnum.AccountingSettings, 'fiscalYearStart' @@ -503,8 +497,14 @@ function updateParentAccountWithChildValues( 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)!); + const value = parentAccount.valueMap!.get(key); + const childValue = valueMap.get(key); + const map: Record = {}; + + for (const key of Object.keys(childValue!)) { + map[key] = (value?.[key] ?? 0) + (childValue?.[key] ?? 0); + } + parentAccount.valueMap!.set(key, map); } return parentAccount.parentAccount!; diff --git a/reports/BalanceSheet/BalanceSheet.ts b/reports/BalanceSheet/BalanceSheet.ts index e87ac040..bd704b7a 100644 --- a/reports/BalanceSheet/BalanceSheet.ts +++ b/reports/BalanceSheet/BalanceSheet.ts @@ -7,15 +7,9 @@ import { AccountReport, convertAccountRootNodeToAccountList, } from 'reports/AccountReport'; -import { AccountTreeNode, ReportData } from 'reports/types'; +import { ReportData, RootTypeRow } from 'reports/types'; import { getMapFromList } from 'utils'; -type RootTypeRow = { - rootType: AccountRootType; - rootNode: AccountTreeNode; - rows: ReportData; -}; - export class BalanceSheet extends AccountReport { static title = t`Balance Sheet`; static reportName = 'balance-sheet'; @@ -29,7 +23,7 @@ export class BalanceSheet extends AccountReport { } async setReportData(filter?: string) { - if (filter !== 'hideGroupBalance') { + if (filter !== 'hideGroupAmounts') { await this._setRawData(); } diff --git a/reports/ProfitAndLoss/ProfitAndLoss.ts b/reports/ProfitAndLoss/ProfitAndLoss.ts index 83376cb2..e4965624 100644 --- a/reports/ProfitAndLoss/ProfitAndLoss.ts +++ b/reports/ProfitAndLoss/ProfitAndLoss.ts @@ -7,7 +7,12 @@ import { AccountReport, convertAccountRootNodeToAccountList, } from 'reports/AccountReport'; -import { AccountListNode, AccountTreeNode, ReportData } from 'reports/types'; +import { + AccountListNode, + AccountTreeNode, + ReportData, + ValueMap, +} from 'reports/types'; export class ProfitAndLoss extends AccountReport { static title = t`Profit And Loss`; @@ -18,7 +23,7 @@ export class ProfitAndLoss extends AccountReport { } async setReportData(filter?: string) { - if (filter !== 'hideGroupBalance') { + if (filter !== 'hideGroupAmounts') { await this._setRawData(); } @@ -78,11 +83,11 @@ export class ProfitAndLoss extends AccountReport { t`Total Expense (Debit)` ); - const totalValueMap = new Map(); + const totalValueMap: ValueMap = new Map(); for (const key of totalIncome.valueMap!.keys()) { - const income = totalIncome.valueMap!.get(key) ?? 0; - const expense = totalExpense.valueMap!.get(key) ?? 0; - totalValueMap.set(key, income - expense); + const income = totalIncome.valueMap!.get(key)?.balance ?? 0; + const expense = totalExpense.valueMap!.get(key)?.balance ?? 0; + totalValueMap.set(key, { balance: income - expense }); } const totalProfit = { @@ -117,7 +122,6 @@ export class ProfitAndLoss extends AccountReport { expenseRows, totalExpenseRow, emptyRow, - emptyRow, totalProfitRow, ].flat() as ReportData; } diff --git a/reports/TrialBalance/TrialBalance.ts b/reports/TrialBalance/TrialBalance.ts new file mode 100644 index 00000000..9dc9ab54 --- /dev/null +++ b/reports/TrialBalance/TrialBalance.ts @@ -0,0 +1,291 @@ +import { t } from 'fyo'; +import { Action } from 'fyo/model/types'; +import { ValueError } from 'fyo/utils/errors'; +import { DateTime } from 'luxon'; +import { + AccountRootType, + AccountRootTypeEnum, +} from 'models/baseModels/Account/types'; +import { + AccountReport, + ACC_BAL_WIDTH, + ACC_NAME_WIDTH, + convertAccountRootNodeToAccountList, + getFiscalEndpoints, +} from 'reports/AccountReport'; +import { + Account, + AccountListNode, + AccountNameValueMapMap, + ColumnField, + DateRange, + GroupedMap, + LedgerEntry, + ReportCell, + ReportData, + ReportRow, + RootTypeRow, + ValueMap, +} from 'reports/types'; +import { Field } from 'schemas/types'; +import { QueryFilter } from 'utils/db/types'; + +export class TrialBalance extends AccountReport { + static title = t`Trial Balance`; + static reportName = 'trial-balance'; + + fromDate?: string; + toDate?: string; + hideGroupAmounts: boolean = false; + + _rawData: LedgerEntry[] = []; + _dateRanges?: DateRange[]; + + accountMap?: Record; + + get rootTypes(): AccountRootType[] { + return [ + AccountRootTypeEnum.Asset, + AccountRootTypeEnum.Liability, + AccountRootTypeEnum.Income, + AccountRootTypeEnum.Expense, + AccountRootTypeEnum.Equity, + ]; + } + + async setReportData(filter?: string) { + if (filter !== 'hideGroupAmounts') { + await this._setRawData(); + } + + const map = this._getGroupedMap(true, 'account'); + const rangeGroupedMap = await this._getGroupedByDateRanges(map); + const accountTree = await this._getAccountTree(rangeGroupedMap); + + 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(rootTypeRows); + } + + async getReportDataFromRows( + rootTypeRows: RootTypeRow[] + ): Promise { + const reportData = rootTypeRows.reduce((reportData, r) => { + reportData.push(...r.rows); + reportData.push(this.getEmptyRow()); + return reportData; + }, [] as ReportData); + + reportData.pop(); + + return reportData; + } + + async _getGroupedByDateRanges( + map: GroupedMap + ): Promise { + const accountValueMap: AccountNameValueMapMap = new Map(); + + 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) { + throw new ValueError( + `invalid entry in trial balance ${entry.date?.toISOString()}` + ); + } + + const map = valueMap.get(key!); + const totalCredit = map?.credit ?? 0; + const totalDebit = map?.debit ?? 0; + + valueMap.set(key, { + credit: totalCredit + (entry.credit ?? 0), + debit: totalDebit + (entry.debit ?? 0), + }); + } + + accountValueMap.set(account, valueMap); + } + + return accountValueMap; + } + + async _getDateRanges(): Promise { + if (!this.toDate || !this.fromDate) { + await this.setDefaultFilters(); + } + + const toDate = DateTime.fromISO(this.toDate!); + const fromDate = DateTime.fromISO(this.fromDate!); + + return [ + { + fromDate: DateTime.fromISO('0001-01-01'), + toDate: fromDate, + }, + { fromDate, toDate }, + { + fromDate: toDate, + toDate: DateTime.fromISO('9999-12-31'), + }, + ]; + } + + 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 map = al.valueMap?.get(k); + const hide = this.hideGroupAmounts && al.isGroup; + + return [ + { + rawValue: map?.debit ?? 0, + value: hide ? '' : this.fyo.format(map?.debit ?? 0, 'Currency'), + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + rawValue: map?.credit ?? 0, + value: hide ? '' : this.fyo.format(map?.credit ?? 0, 'Currency'), + align: 'right', + width: ACC_BAL_WIDTH, + } as ReportCell, + ]; + }); + + return { + cells: [nameCell, balanceCells].flat(2), + level: al.level, + isGroup: !!al.isGroup, + folded: false, + foldedBelow: false, + } as ReportRow; + } + + async _getQueryFilters(): Promise { + const filters: QueryFilter = {}; + filters.reverted = false; + return filters; + } + + async setDefaultFilters(): Promise { + if (!this.toDate || !this.fromDate) { + const { year } = DateTime.now(); + const endpoints = await getFiscalEndpoints(year + 1, year); + + this.fromDate = endpoints.fromDate; + this.toDate = DateTime.fromISO(endpoints.toDate) + .minus({ days: 1 }) + .toISODate(); + } + + await this._setDateRanges(); + } + + getFilters(): Field[] { + return [ + { + fieldtype: 'Date', + fieldname: 'fromDate', + placeholder: t`From Date`, + label: t`From Date`, + required: true, + }, + { + fieldtype: 'Date', + fieldname: 'toDate', + placeholder: t`To Date`, + label: t`To Date`, + required: true, + }, + { + fieldtype: 'Check', + label: t`Hide Group Amounts`, + fieldname: 'hideGroupAmounts', + } as Field, + ] as Field[]; + } + + getColumns(): ColumnField[] { + return [ + { + label: t`Account`, + fieldtype: 'Link', + fieldname: 'account', + align: 'left', + width: ACC_NAME_WIDTH, + }, + { + label: t`Opening (Dr)`, + fieldtype: 'Data', + fieldname: 'openingDebit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + label: t`Opening (Cr)`, + fieldtype: 'Data', + fieldname: 'openingCredit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + label: t`Debit`, + fieldtype: 'Data', + fieldname: 'debit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + label: t`Credit`, + fieldtype: 'Data', + fieldname: 'credit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + label: t`Closing (Dr)`, + fieldtype: 'Data', + fieldname: 'closingDebit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + { + label: t`Closing (Cr)`, + fieldtype: 'Data', + fieldname: 'closingCredit', + align: 'right', + width: ACC_BAL_WIDTH, + }, + ] as ColumnField[]; + } + + getActions(): Action[] { + return []; + } +} diff --git a/reports/index.ts b/reports/index.ts index 97a80101..1b4cf056 100644 --- a/reports/index.ts +++ b/reports/index.ts @@ -1,5 +1,11 @@ import { BalanceSheet } from './BalanceSheet/BalanceSheet'; import { GeneralLedger } from './GeneralLedger/GeneralLedger'; import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss'; +import { TrialBalance } from './TrialBalance/TrialBalance'; -export const reports = { GeneralLedger, ProfitAndLoss, BalanceSheet }; +export const reports = { + GeneralLedger, + ProfitAndLoss, + BalanceSheet, + TrialBalance, +}; diff --git a/reports/types.ts b/reports/types.ts index 866d79d3..92548bb2 100644 --- a/reports/types.ts +++ b/reports/types.ts @@ -72,7 +72,7 @@ export interface LedgerEntry { export type GroupedMap = Map; export type DateRange = { fromDate: DateTime; toDate: DateTime }; -export type ValueMap = Map; +export type ValueMap = Map>; export interface Account { name: string; @@ -97,7 +97,6 @@ export interface AccountListNode extends Account { export type AccountNameValueMapMap = Map; export type BasedOn = 'Fiscal Year' | 'Until Date'; - export interface TreeNode { name: string; children?: TreeNode[]; @@ -105,4 +104,8 @@ export interface TreeNode { export type Tree = Record; - +export type RootTypeRow = { + rootType: AccountRootType; + rootNode: AccountTreeNode; + rows: ReportData; +}; \ No newline at end of file diff --git a/src/renderer.ts b/src/renderer.ts index 1a58d3fc..5caae725 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,7 +1,6 @@ import { ipcRenderer } from 'electron'; import { ConfigKeys } from 'fyo/core/types'; import { DateTime } from 'luxon'; -import { ProfitAndLoss } from 'reports/ProfitAndLoss/ProfitAndLoss'; import { IPC_ACTIONS } from 'utils/messages'; import { App as VueApp, createApp } from 'vue'; import App from './App.vue'; @@ -107,7 +106,5 @@ function setOnWindow() { window.fyo = fyo; // @ts-ignore window.DateTime = DateTime; - // @ts-ignore - window.pnl = new ProfitAndLoss(fyo); } } diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index 6deacba3..7885db56 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -153,12 +153,12 @@ function getCompleteSidebar(): SidebarConfig { name: 'balance-sheet', route: '/report/BalanceSheet', }, - /* { label: t`Trial Balance`, name: 'trial-balance', - route: '/report/trial-balance', + route: '/report/TrialBalance', }, + /* { label: t`GSTR1`, name: 'gstr1',