mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
incr: shift report commonalities to LedgerReport
- start with Profit and Loss
This commit is contained in:
parent
3cdba15b81
commit
bf01fa0327
@ -5,6 +5,10 @@ import { NotFoundError } from 'fyo/utils/errors';
|
||||
import { DateTime } from 'luxon';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { Router } from 'vue-router';
|
||||
import {
|
||||
AccountRootType,
|
||||
AccountRootTypeEnum,
|
||||
} from './baseModels/Account/types';
|
||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
||||
|
||||
export function getInvoiceActions(
|
||||
@ -169,3 +173,20 @@ export async function getExchangeRate({
|
||||
|
||||
return exchangeRate;
|
||||
}
|
||||
|
||||
export function isCredit(rootType: AccountRootType) {
|
||||
switch (rootType) {
|
||||
case AccountRootTypeEnum.Asset:
|
||||
return false;
|
||||
case AccountRootTypeEnum.Liability:
|
||||
return true;
|
||||
case AccountRootTypeEnum.Equity:
|
||||
return true;
|
||||
case AccountRootTypeEnum.Expense:
|
||||
return false;
|
||||
case AccountRootTypeEnum.Income:
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,18 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Report } from 'reports/Report';
|
||||
import { ColumnField, ReportData, ReportRow } from 'reports/types';
|
||||
import { Field, FieldTypeEnum, RawValue } from 'schemas/types';
|
||||
import { LedgerReport } from 'reports/LedgerReport';
|
||||
import {
|
||||
ColumnField,
|
||||
GroupedMap,
|
||||
LedgerEntry,
|
||||
ReportData,
|
||||
ReportRow,
|
||||
} from 'reports/types';
|
||||
import { Field, FieldTypeEnum } from 'schemas/types';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
|
||||
interface RawLedgerEntry {
|
||||
name: string;
|
||||
account: string;
|
||||
date: string;
|
||||
debit: string;
|
||||
credit: string;
|
||||
referenceType: string;
|
||||
referenceName: string;
|
||||
party: string;
|
||||
reverted: number;
|
||||
reverts: string;
|
||||
[key: string]: RawValue;
|
||||
}
|
||||
|
||||
interface LedgerEntry {
|
||||
index?: string;
|
||||
name: number;
|
||||
account: string;
|
||||
date: Date | null;
|
||||
debit: number | null;
|
||||
credit: number | null;
|
||||
balance: number | null;
|
||||
referenceType: string;
|
||||
referenceName: string;
|
||||
party: string;
|
||||
reverted: boolean;
|
||||
reverts: string;
|
||||
}
|
||||
|
||||
type GroupedMap = Map<string, LedgerEntry[]>;
|
||||
|
||||
export class GeneralLedger extends Report {
|
||||
export class GeneralLedger extends LedgerReport {
|
||||
static title = t`General Ledger`;
|
||||
static reportName = 'general-ledger';
|
||||
|
||||
@ -48,8 +22,10 @@ export class GeneralLedger extends Report {
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
super(fyo);
|
||||
}
|
||||
|
||||
if (!this.toField) {
|
||||
async setDefaultFilters() {
|
||||
if (!this.toDate) {
|
||||
this.toDate = DateTime.now().toISODate();
|
||||
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
|
||||
}
|
||||
@ -238,85 +214,7 @@ export class GeneralLedger extends Report {
|
||||
return { totalDebit, totalCredit };
|
||||
}
|
||||
|
||||
_getGroupedMap(sort: boolean): GroupedMap {
|
||||
let groupBy: keyof LedgerEntry = 'referenceName';
|
||||
if (this.groupBy !== 'none') {
|
||||
groupBy = this.groupBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort rows by ascending or descending
|
||||
*/
|
||||
if (sort) {
|
||||
this._rawData.sort((a, b) => {
|
||||
if (this.ascending) {
|
||||
return +a.date! - +b.date!;
|
||||
}
|
||||
|
||||
return +b.date! - +a.date!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map remembers the order of insertion
|
||||
* ∴ presorting maintains grouping order
|
||||
*/
|
||||
const map: GroupedMap = new Map();
|
||||
for (const entry of this._rawData) {
|
||||
const groupingKey = entry[groupBy];
|
||||
if (!map.has(groupingKey)) {
|
||||
map.set(groupingKey, []);
|
||||
}
|
||||
|
||||
map.get(groupingKey)!.push(entry);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
async _setRawData() {
|
||||
const fields = [
|
||||
'name',
|
||||
'account',
|
||||
'date',
|
||||
'debit',
|
||||
'credit',
|
||||
'referenceType',
|
||||
'referenceName',
|
||||
'party',
|
||||
'reverted',
|
||||
'reverts',
|
||||
];
|
||||
|
||||
const filters = this._getQueryFilters();
|
||||
const entries = (await this.fyo.db.getAllRaw(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
fields,
|
||||
filters,
|
||||
orderBy: 'date',
|
||||
order: this.ascending ? 'asc' : 'desc',
|
||||
}
|
||||
)) as RawLedgerEntry[];
|
||||
|
||||
this._rawData = entries.map((entry) => {
|
||||
return {
|
||||
name: parseInt(entry.name),
|
||||
account: entry.account,
|
||||
date: new Date(entry.date),
|
||||
debit: parseFloat(entry.debit),
|
||||
credit: parseFloat(entry.credit),
|
||||
balance: 0,
|
||||
referenceType: entry.referenceType,
|
||||
referenceName: entry.referenceName,
|
||||
party: entry.party,
|
||||
reverted: Boolean(entry.reverted),
|
||||
reverts: entry.reverts,
|
||||
} as LedgerEntry;
|
||||
});
|
||||
}
|
||||
|
||||
_getQueryFilters(): QueryFilter {
|
||||
async _getQueryFilters(): Promise<QueryFilter> {
|
||||
const filters: QueryFilter = {};
|
||||
const stringFilters = ['account', 'party', 'referenceName'];
|
||||
|
||||
|
98
reports/LedgerReport.ts
Normal file
98
reports/LedgerReport.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { t } from 'fyo';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Report } from 'reports/Report';
|
||||
import { GroupedMap, LedgerEntry, RawLedgerEntry } from 'reports/types';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
|
||||
type GroupByKey = 'account' | 'party' | 'referenceName';
|
||||
|
||||
export abstract class LedgerReport extends Report {
|
||||
static title = t`General Ledger`;
|
||||
static reportName = 'general-ledger';
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
|
||||
_getGroupByKey() {
|
||||
let groupBy: GroupByKey = 'referenceName';
|
||||
if (this.groupBy && this.groupBy !== 'none') {
|
||||
groupBy = this.groupBy as GroupByKey;
|
||||
}
|
||||
return groupBy;
|
||||
}
|
||||
|
||||
_getGroupedMap(sort: boolean, groupBy?: GroupByKey): GroupedMap {
|
||||
groupBy ??= this._getGroupByKey();
|
||||
/**
|
||||
* Sort rows by ascending or descending
|
||||
*/
|
||||
if (sort) {
|
||||
this._rawData.sort((a, b) => {
|
||||
if (this.ascending) {
|
||||
return +a.date! - +b.date!;
|
||||
}
|
||||
|
||||
return +b.date! - +a.date!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map remembers the order of insertion
|
||||
* ∴ presorting maintains grouping order
|
||||
*/
|
||||
const map: GroupedMap = new Map();
|
||||
for (const entry of this._rawData) {
|
||||
const groupingKey = entry[groupBy];
|
||||
if (!map.has(groupingKey)) {
|
||||
map.set(groupingKey, []);
|
||||
}
|
||||
|
||||
map.get(groupingKey)!.push(entry);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
async _setRawData() {
|
||||
const fields = [
|
||||
'name',
|
||||
'account',
|
||||
'date',
|
||||
'debit',
|
||||
'credit',
|
||||
'referenceType',
|
||||
'referenceName',
|
||||
'party',
|
||||
'reverted',
|
||||
'reverts',
|
||||
];
|
||||
|
||||
const filters = await this._getQueryFilters();
|
||||
const entries = (await this.fyo.db.getAllRaw(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
fields,
|
||||
filters,
|
||||
orderBy: 'date',
|
||||
order: this.ascending ? 'asc' : 'desc',
|
||||
}
|
||||
)) as RawLedgerEntry[];
|
||||
|
||||
this._rawData = entries.map((entry) => {
|
||||
return {
|
||||
name: parseInt(entry.name),
|
||||
account: entry.account,
|
||||
date: new Date(entry.date),
|
||||
debit: parseFloat(entry.debit),
|
||||
credit: parseFloat(entry.credit),
|
||||
balance: 0,
|
||||
referenceType: entry.referenceType,
|
||||
referenceName: entry.referenceName,
|
||||
party: entry.party,
|
||||
reverted: Boolean(entry.reverted),
|
||||
reverts: entry.reverts,
|
||||
} as LedgerEntry;
|
||||
});
|
||||
}
|
||||
|
||||
abstract _getQueryFilters(): Promise<QueryFilter>;
|
||||
}
|
@ -1,107 +1,334 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { unique } from 'fyo/utils';
|
||||
import { FinancialStatements } from 'reports/FinancialStatements/financialStatements';
|
||||
import { FinancialStatementOptions } from 'reports/types';
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
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 {
|
||||
ColumnField,
|
||||
GroupedMap,
|
||||
LedgerEntry,
|
||||
Periodicity,
|
||||
} from 'reports/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getMapFromList } from 'utils';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
|
||||
interface Row {
|
||||
indent?: number;
|
||||
account: string | { template: string };
|
||||
isGroup?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
type DateRange = { fromDate: DateTime; toDate: DateTime };
|
||||
type ValueMap = Map<DateRange, number>;
|
||||
type AccountNameValueMapMap = Map<string, ValueMap>;
|
||||
type BasedOn = 'Fiscal Year' | 'Date Range';
|
||||
type Account = { name: string; rootType: AccountRootType; isGroup: boolean };
|
||||
|
||||
export class ProfitAndLoss extends LedgerReport {
|
||||
static title = t`Profit And Loss`;
|
||||
static reportName = 'profit-and-loss';
|
||||
|
||||
toDate?: string;
|
||||
count?: number;
|
||||
fromYear?: number;
|
||||
toYear?: number;
|
||||
singleColumn: boolean = false;
|
||||
periodicity: Periodicity = 'Monthly';
|
||||
basedOn: BasedOn = 'Fiscal Year';
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
|
||||
accountMap?: Record<string, Account>;
|
||||
|
||||
export default class ProfitAndLoss {
|
||||
fyo: Fyo;
|
||||
constructor(fyo: Fyo) {
|
||||
this.fyo = fyo;
|
||||
super(fyo);
|
||||
}
|
||||
|
||||
async run(options: FinancialStatementOptions) {
|
||||
const { fromDate, toDate, periodicity } = options;
|
||||
const fs = new FinancialStatements(this.fyo);
|
||||
const income = await fs.getData({
|
||||
rootType: 'Income',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
});
|
||||
async setDefaultFilters(): Promise<void> {
|
||||
if (this.basedOn === 'Date Range' && !this.toDate) {
|
||||
this.toDate = DateTime.now().toISODate();
|
||||
this.count = 1;
|
||||
}
|
||||
|
||||
const expense = await fs.getData({
|
||||
rootType: 'Expense',
|
||||
balanceMustBe: 'Debit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
});
|
||||
if (this.basedOn === 'Fiscal Year' && !this.toYear) {
|
||||
this.toYear = DateTime.now().year;
|
||||
this.fromYear = this.toYear - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const incomeAccount = income.totalRow.account as string;
|
||||
const incomeTotalRow = {
|
||||
...income.totalRow,
|
||||
account: {
|
||||
template: `<span class="font-semibold">${incomeAccount}</span>`,
|
||||
},
|
||||
};
|
||||
async setReportData(filter?: string) {
|
||||
let sort = true;
|
||||
if (
|
||||
this._rawData.length === 0 &&
|
||||
!['periodicity', 'singleColumn'].includes(filter!)
|
||||
) {
|
||||
await this._setRawData();
|
||||
sort = false;
|
||||
}
|
||||
|
||||
const expenseAccount = expense.totalRow.account as string;
|
||||
const expenseTotalRow = {
|
||||
...expense.totalRow,
|
||||
account: {
|
||||
template: `<span class="font-semibold">${expenseAccount}</span>`,
|
||||
},
|
||||
};
|
||||
const map = this._getGroupedMap(sort, 'account');
|
||||
const rangeGroupedMap = await this._getGroupedByDateRanges(map);
|
||||
/**
|
||||
* TODO: Get account tree from accountMap
|
||||
* TODO: Create Grid from rangeGroupedMap and tree
|
||||
*/
|
||||
}
|
||||
|
||||
let rows = [
|
||||
...income.accounts,
|
||||
incomeTotalRow,
|
||||
{
|
||||
account: {
|
||||
template: '<span> </span>',
|
||||
},
|
||||
isGroup: true,
|
||||
},
|
||||
...expense.accounts,
|
||||
expenseTotalRow,
|
||||
{
|
||||
account: {
|
||||
template: '<span> </span>',
|
||||
},
|
||||
isGroup: true,
|
||||
},
|
||||
] as Row[];
|
||||
async _getGroupedByDateRanges(
|
||||
map: GroupedMap
|
||||
): Promise<AccountNameValueMapMap> {
|
||||
const dateRanges = await this._getDateRanges();
|
||||
const accountValueMap: AccountNameValueMapMap = new Map();
|
||||
const accountMap = await this._getAccountMap();
|
||||
|
||||
rows = rows.map((row) => {
|
||||
if (row.indent === 0) {
|
||||
row.account = {
|
||||
template: `<span class="font-semibold">${row.account}</span>`,
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
const columns = unique([...income.periodList, ...expense.periodList]);
|
||||
|
||||
const profitRow: Row = {
|
||||
account: 'Total Profit',
|
||||
};
|
||||
|
||||
for (const column of columns) {
|
||||
const incomeAmount =
|
||||
(income.totalRow[column] as number | undefined) ?? 0.0;
|
||||
const expenseAmount =
|
||||
(expense.totalRow[column] as number | undefined) ?? 0.0;
|
||||
|
||||
profitRow[column] = incomeAmount - expenseAmount;
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (!row.isGroup) {
|
||||
row[column] = row[column] || 0.0;
|
||||
for (const account of map.keys()) {
|
||||
const valueMap: ValueMap = new Map();
|
||||
for (const entry of map.get(account)!) {
|
||||
const key = this._getRangeMapKey(entry, dateRanges);
|
||||
if (valueMap === 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 _getAccountMap() {
|
||||
if (this.accountMap) {
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
const accountList: Account[] = (
|
||||
await this.fyo.db.getAllRaw('Account', {
|
||||
fields: ['name', 'rootType', 'isGroup'],
|
||||
})
|
||||
).map((rv) => ({
|
||||
name: rv.name as string,
|
||||
rootType: rv.rootType as AccountRootType,
|
||||
isGroup: Boolean(rv.isGroup),
|
||||
}));
|
||||
|
||||
this.accountMap = getMapFromList(accountList, 'name');
|
||||
return this.accountMap;
|
||||
}
|
||||
|
||||
_getRangeMapKey(
|
||||
entry: LedgerEntry,
|
||||
dateRanges: DateRange[]
|
||||
): DateRange | null {
|
||||
const entryDate = +DateTime.fromISO(
|
||||
entry.date!.toISOString().split('T')[0]
|
||||
);
|
||||
|
||||
for (const dr of dateRanges) {
|
||||
if (entryDate <= +dr.toDate && entryDate > +dr.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 }),
|
||||
});
|
||||
}
|
||||
|
||||
rows.push(profitRow);
|
||||
|
||||
return { rows, columns };
|
||||
return dateRanges;
|
||||
}
|
||||
|
||||
async _getFromAndToDates() {
|
||||
let toDate: string;
|
||||
let fromDate: string;
|
||||
|
||||
if (this.basedOn === 'Date Range') {
|
||||
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() {
|
||||
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`Monthly`, value: 'Monthly' },
|
||||
{ label: t`Quarterly`, value: 'Quarterly' },
|
||||
{ label: t`Half Yearly`, value: 'Half Yearly' },
|
||||
{ label: t`Yearly`, value: 'Yearly' },
|
||||
],
|
||||
default: 'Monthly',
|
||||
label: t`Periodicity`,
|
||||
fieldname: 'periodicity',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
{ label: t`Fiscal Year`, value: 'Fiscal Year' },
|
||||
{ label: t`Date Range`, value: 'Date Range' },
|
||||
],
|
||||
default: 'Fiscal Year',
|
||||
label: t`Based On`,
|
||||
fieldname: 'basedOn',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
default: false,
|
||||
label: t`Single Column`,
|
||||
fieldname: 'singleColumn',
|
||||
},
|
||||
] as Field[];
|
||||
|
||||
if (this.basedOn === 'Date Range') {
|
||||
return [
|
||||
...filters,
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toDate',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'count',
|
||||
placeholder: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
label: t`Number of ${periodNameMap[this.periodicity]}`,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
}
|
||||
|
||||
const thisYear = DateTime.local().year;
|
||||
return [
|
||||
...filters,
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'fromYear',
|
||||
placeholder: t`From Date`,
|
||||
label: t`From Date`,
|
||||
default: thisYear - 1,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toYear',
|
||||
placeholder: t`To Year`,
|
||||
label: t`To Year`,
|
||||
default: thisYear,
|
||||
required: true,
|
||||
},
|
||||
] as Field[];
|
||||
}
|
||||
|
||||
getColumns(): ColumnField[] {
|
||||
const columns = [] as ColumnField[];
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
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),
|
||||
].join('-');
|
||||
|
||||
const toDate = [
|
||||
toYear,
|
||||
fye.toISOString().split('T')[0].split('-').slice(1),
|
||||
].join('-');
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
|
||||
const monthsMap: Record<Periodicity, number> = {
|
||||
Monthly: 1,
|
||||
Quarterly: 3,
|
||||
'Half Yearly': 6,
|
||||
Yearly: 12,
|
||||
};
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { t } from 'fyo';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import getCommonExportActions from '../commonExporter';
|
||||
|
||||
const title = t`Profit and Loss`;
|
||||
|
||||
const periodicityMap = {
|
||||
Monthly: t`Monthly`,
|
||||
Quarterly: t`Quarterly`,
|
||||
'Half Yearly': t`Half Yearly`,
|
||||
Yearly: t`Yearly`,
|
||||
};
|
||||
export default {
|
||||
title: title,
|
||||
method: 'profit-and-loss',
|
||||
treeView: true,
|
||||
filterFields: [
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'fromDate',
|
||||
size: 'small',
|
||||
placeholder: t`From Date`,
|
||||
label: t`From Date`,
|
||||
required: 1,
|
||||
default: async () => {
|
||||
return (await fyo.getSingle('AccountingSettings')).fiscalYearStart;
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: 'Date',
|
||||
fieldname: 'toDate',
|
||||
size: 'small',
|
||||
placeholder: t`To Date`,
|
||||
label: t`To Date`,
|
||||
required: 1,
|
||||
default: async () => {
|
||||
return (await fyo.getSingle('AccountingSettings')).fiscalYearEnd;
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
size: 'small',
|
||||
options: Object.keys(periodicityMap),
|
||||
map: periodicityMap,
|
||||
default: 'Monthly',
|
||||
label: t`Periodicity`,
|
||||
placeholder: t`Select Period...`,
|
||||
fieldname: 'periodicity',
|
||||
},
|
||||
],
|
||||
actions: getCommonExportActions('profit-and-loss'),
|
||||
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',
|
||||
width: 1,
|
||||
}));
|
||||
|
||||
columns.push(...columnDefs);
|
||||
}
|
||||
|
||||
return columns;
|
||||
},
|
||||
};
|
@ -14,9 +14,6 @@ export abstract class Report extends Observable<RawValue> {
|
||||
filters: Field[];
|
||||
reportData: ReportData;
|
||||
|
||||
abstract getActions(): Action[];
|
||||
abstract getFilters(): Field[];
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
super();
|
||||
this.fyo = fyo;
|
||||
@ -57,7 +54,9 @@ export abstract class Report extends Observable<RawValue> {
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
this.filters = this.getFilters();
|
||||
this.columns = this.getColumns();
|
||||
await this.setDefaultFilters();
|
||||
await this.setReportData(key);
|
||||
}
|
||||
|
||||
@ -72,6 +71,13 @@ export abstract class Report extends Observable<RawValue> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should first check if filter value is set
|
||||
* and update only if it is not set.
|
||||
*/
|
||||
async setDefaultFilters() {}
|
||||
abstract getActions(): Action[];
|
||||
abstract getFilters(): Field[];
|
||||
abstract getColumns(): ColumnField[];
|
||||
abstract setReportData(filter?: string): Promise<void>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AccountRootType } from 'models/baseModels/Account/types';
|
||||
import { BaseField } from 'schemas/types';
|
||||
import { BaseField, RawValue } from 'schemas/types';
|
||||
|
||||
export type ExportExtension = 'csv' | 'json';
|
||||
|
||||
@ -28,3 +28,34 @@ export interface FinancialStatementOptions {
|
||||
periodicity?: Periodicity;
|
||||
accumulateValues?: boolean;
|
||||
}
|
||||
|
||||
export interface RawLedgerEntry {
|
||||
name: string;
|
||||
account: string;
|
||||
date: string;
|
||||
debit: string;
|
||||
credit: string;
|
||||
referenceType: string;
|
||||
referenceName: string;
|
||||
party: string;
|
||||
reverted: number;
|
||||
reverts: string;
|
||||
[key: string]: RawValue;
|
||||
}
|
||||
|
||||
export interface LedgerEntry {
|
||||
index?: string;
|
||||
name: number;
|
||||
account: string;
|
||||
date: Date | null;
|
||||
debit: number | null;
|
||||
credit: number | null;
|
||||
balance: number | null;
|
||||
referenceType: string;
|
||||
referenceName: string;
|
||||
party: string;
|
||||
reverted: boolean;
|
||||
reverts: string;
|
||||
}
|
||||
|
||||
export type GroupedMap = Map<string, LedgerEntry[]>;
|
@ -4,7 +4,9 @@ import Link from './Link.vue';
|
||||
export default {
|
||||
name: 'DynamicLink',
|
||||
props: ['target'],
|
||||
inject: ['report'],
|
||||
inject: {
|
||||
report: { default: null },
|
||||
},
|
||||
extends: Link,
|
||||
created() {
|
||||
const watchKey = `doc.${this.df.references}`;
|
||||
|
@ -127,7 +127,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { t } from 'fyo';
|
||||
import { AccountRootTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { isCredit } from 'models/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import PageHeader from 'src/components/PageHeader';
|
||||
import { fyo } from 'src/initFyo';
|
||||
@ -154,24 +154,8 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isCredit(rootType) {
|
||||
switch (rootType) {
|
||||
case AccountRootTypeEnum.Asset:
|
||||
return false;
|
||||
case AccountRootTypeEnum.Liability:
|
||||
return true;
|
||||
case AccountRootTypeEnum.Equity:
|
||||
return true;
|
||||
case AccountRootTypeEnum.Expense:
|
||||
return false;
|
||||
case AccountRootTypeEnum.Income:
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
getBalanceString(account) {
|
||||
const suffix = this.isCredit(account.rootType) ? t`Cr.` : t`Dr.`;
|
||||
const suffix = isCredit(account.rootType) ? t`Cr.` : t`Dr.`;
|
||||
return `${fyo.format(account.balance, 'Currency')} ${suffix}`;
|
||||
},
|
||||
async fetchAccounts() {
|
||||
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<PageHeader :title="t`Dashboard`" />
|
||||
|
||||
<div class="mx-4 overflow-y-scroll no-scrollbar">
|
||||
<!--
|
||||
<Cashflow class="mt-5" />
|
||||
<hr class="border-t mt-10" />
|
||||
<UnpaidInvoices class="mt-10 ml-4 mr-4" />
|
||||
@ -10,25 +12,28 @@
|
||||
<ProfitAndLoss class="w-1/2" />
|
||||
<Expenses class="w-1/2" />
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageHeader from 'src/components/PageHeader';
|
||||
import Cashflow from './Cashflow';
|
||||
import Expenses from './Expenses';
|
||||
import ProfitAndLoss from './ProfitAndLoss';
|
||||
import UnpaidInvoices from './UnpaidInvoices';
|
||||
// import Cashflow from './Cashflow';
|
||||
// import Expenses from './Expenses';
|
||||
// import ProfitAndLoss from './ProfitAndLoss';
|
||||
// import UnpaidInvoices from './UnpaidInvoices';
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
PageHeader,
|
||||
/*
|
||||
Cashflow,
|
||||
UnpaidInvoices,
|
||||
ProfitAndLoss,
|
||||
Expenses,
|
||||
*/
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -20,11 +20,12 @@
|
||||
<div v-if="report" class="mx-4 grid grid-cols-5 gap-2">
|
||||
<FormControl
|
||||
v-for="field in report.filters"
|
||||
size="small"
|
||||
:show-label="field.fieldtype === 'Check'"
|
||||
:key="field.fieldname + '-filter'"
|
||||
class="bg-gray-100 rounded"
|
||||
:class="field.fieldtype === 'Check' ? 'flex pl-3' : ''"
|
||||
input-class="bg-transparent px-3 py-2 text-base"
|
||||
:class="field.fieldtype === 'Check' ? 'flex pl-2' : ''"
|
||||
input-class="bg-transparent text-sm"
|
||||
:df="field"
|
||||
:value="report.get(field.fieldname)"
|
||||
:read-only="loading"
|
||||
@ -85,6 +86,8 @@ export default defineComponent({
|
||||
if (!this.report.reportData.length) {
|
||||
await this.report.setReportData();
|
||||
}
|
||||
|
||||
await this.report.setDefaultFilters();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { App as VueApp, createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
@ -103,5 +104,7 @@ function setOnWindow() {
|
||||
window.router = router;
|
||||
// @ts-ignore
|
||||
window.fyo = fyo;
|
||||
// @ts-ignore
|
||||
window.DateTime = DateTime;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user