mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
485 lines
11 KiB
TypeScript
485 lines
11 KiB
TypeScript
|
import { Fyo, t } from 'fyo';
|
||
|
import { Action } from 'fyo/model/types';
|
||
|
import { isPesa } from 'fyo/utils';
|
||
|
import { DateTime } from 'luxon';
|
||
|
import { ModelNameEnum } from 'models/types';
|
||
|
import Money from 'pesa/dist/types/src/money';
|
||
|
import { Report } from 'reports/Report';
|
||
|
import { ColumnField, ReportData } from 'reports/types';
|
||
|
import { Field, FieldTypeEnum, RawValue } 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 {
|
||
|
name: number;
|
||
|
account: string;
|
||
|
date: Date | null;
|
||
|
debit: Money | null;
|
||
|
credit: Money | null;
|
||
|
balance: Money | null;
|
||
|
referenceType: string;
|
||
|
referenceName: string;
|
||
|
party: string;
|
||
|
reverted: boolean;
|
||
|
reverts: string;
|
||
|
}
|
||
|
|
||
|
type GroupedMap = Map<string, LedgerEntry[]>;
|
||
|
|
||
|
export class GeneralLedger extends Report {
|
||
|
static title = t`General Ledger`;
|
||
|
static reportName = 'general-ledger';
|
||
|
|
||
|
ascending!: boolean;
|
||
|
groupBy!: 'none' | 'party' | 'account' | 'referenceName';
|
||
|
_rawData: LedgerEntry[] = [];
|
||
|
|
||
|
constructor(fyo: Fyo) {
|
||
|
super(fyo);
|
||
|
|
||
|
if (!this.toField) {
|
||
|
this.toField = DateTime.now().toISODate();
|
||
|
this.fromField = DateTime.now().minus({ years: 1 }).toISODate();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async setReportData(filter?: string) {
|
||
|
if (filter !== 'grouped' || this._rawData.length === 0) {
|
||
|
console.time('_setRawData');
|
||
|
await this._setRawData();
|
||
|
console.timeEnd('_setRawData');
|
||
|
}
|
||
|
|
||
|
console.time('_getGroupedMap');
|
||
|
const map = this._getGroupedMap();
|
||
|
console.timeEnd('_getGroupedMap');
|
||
|
|
||
|
console.time('_getTotalsAndSetBalance');
|
||
|
const { totalDebit, totalCredit } = this._getTotalsAndSetBalance(map);
|
||
|
console.timeEnd('_getTotalsAndSetBalance');
|
||
|
|
||
|
console.time('_consolidateEntries');
|
||
|
const consolidated = this._consolidateEntries(map);
|
||
|
console.timeEnd('_consolidateEntries');
|
||
|
|
||
|
/**
|
||
|
* Push a blank row if last row isn't blank
|
||
|
*/
|
||
|
if (consolidated.at(-1)!.name !== -3) {
|
||
|
this._pushBlankEntry(consolidated);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the closing row
|
||
|
*/
|
||
|
consolidated.push({
|
||
|
name: -2, // Bold
|
||
|
account: t`Closing`,
|
||
|
date: null,
|
||
|
debit: totalDebit,
|
||
|
credit: totalCredit,
|
||
|
balance: totalDebit.sub(totalCredit),
|
||
|
referenceType: '',
|
||
|
referenceName: '',
|
||
|
party: '',
|
||
|
reverted: false,
|
||
|
reverts: '',
|
||
|
});
|
||
|
|
||
|
console.time('_convertEntriesToReportData');
|
||
|
this.reportData = this._convertEntriesToReportData(consolidated);
|
||
|
console.timeEnd('_convertEntriesToReportData');
|
||
|
}
|
||
|
|
||
|
_convertEntriesToReportData(entries: LedgerEntry[]): ReportData {
|
||
|
const reportData = [];
|
||
|
const fieldnames = this.columns.map((f) => f.fieldname);
|
||
|
for (const entry of entries) {
|
||
|
const row = this._getRowFromEntry(entry, fieldnames);
|
||
|
reportData.push(row);
|
||
|
}
|
||
|
|
||
|
return reportData;
|
||
|
}
|
||
|
|
||
|
_getRowFromEntry(entry: LedgerEntry, fieldnames: string[]) {
|
||
|
if (entry.name === -3) {
|
||
|
return Array(fieldnames.length).fill({ value: '' });
|
||
|
}
|
||
|
|
||
|
const row = [];
|
||
|
for (const n of fieldnames) {
|
||
|
let value = entry[n as keyof LedgerEntry];
|
||
|
if (value === null || value === undefined) {
|
||
|
row.push({ value: '' });
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
let align = 'left';
|
||
|
if (value instanceof Date) {
|
||
|
value = this.fyo.format(value, FieldTypeEnum.Date);
|
||
|
}
|
||
|
|
||
|
if (isPesa(value)) {
|
||
|
align = 'right';
|
||
|
value = this.fyo.format(value, FieldTypeEnum.Currency);
|
||
|
}
|
||
|
|
||
|
if (typeof value === 'boolean' && n === 'reverted' && value) {
|
||
|
value = t`Reverted`;
|
||
|
}
|
||
|
|
||
|
row.push({
|
||
|
italics: entry.name === -1,
|
||
|
bold: entry.name === -2,
|
||
|
value,
|
||
|
align,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return row;
|
||
|
}
|
||
|
|
||
|
_consolidateEntries(map: GroupedMap) {
|
||
|
const entries: LedgerEntry[] = [];
|
||
|
for (const key of map.keys()) {
|
||
|
entries.push(...map.get(key)!);
|
||
|
|
||
|
/**
|
||
|
* Add blank row for spacing if groupBy
|
||
|
*/
|
||
|
if (this.groupBy !== 'none') {
|
||
|
this._pushBlankEntry(entries);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return entries;
|
||
|
}
|
||
|
|
||
|
_pushBlankEntry(entries: LedgerEntry[]) {
|
||
|
entries.push({
|
||
|
name: -3, // Empty
|
||
|
account: '',
|
||
|
date: null,
|
||
|
debit: null,
|
||
|
credit: null,
|
||
|
balance: null,
|
||
|
referenceType: '',
|
||
|
referenceName: '',
|
||
|
party: '',
|
||
|
reverted: false,
|
||
|
reverts: '',
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_getTotalsAndSetBalance(map: GroupedMap) {
|
||
|
let totalDebit = this.fyo.pesa(0);
|
||
|
let totalCredit = this.fyo.pesa(0);
|
||
|
|
||
|
for (const key of map.keys()) {
|
||
|
let balance = this.fyo.pesa(0);
|
||
|
let debit = this.fyo.pesa(0);
|
||
|
let credit = this.fyo.pesa(0);
|
||
|
|
||
|
for (const entry of map.get(key)!) {
|
||
|
debit = debit.add(entry.debit!);
|
||
|
credit = credit.add(entry.credit!);
|
||
|
|
||
|
const diff = entry.debit!.sub(entry.credit!);
|
||
|
balance = balance.add(diff);
|
||
|
entry.balance = balance;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Total row incase groupBy is used
|
||
|
*/
|
||
|
if (this.groupBy !== 'none') {
|
||
|
map.get(key)?.push({
|
||
|
name: -1, // Italics
|
||
|
account: t`Total`,
|
||
|
date: null,
|
||
|
debit,
|
||
|
credit,
|
||
|
balance: debit.sub(credit),
|
||
|
referenceType: '',
|
||
|
referenceName: '',
|
||
|
party: '',
|
||
|
reverted: false,
|
||
|
reverts: '',
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Total debit and credit for the final row
|
||
|
*/
|
||
|
totalDebit = totalDebit.add(debit);
|
||
|
totalCredit = totalCredit.add(credit);
|
||
|
}
|
||
|
|
||
|
return { totalDebit, totalCredit };
|
||
|
}
|
||
|
|
||
|
_getGroupedMap(): GroupedMap {
|
||
|
let groupBy: keyof LedgerEntry = 'referenceName';
|
||
|
if (this.groupBy !== 'none') {
|
||
|
groupBy = this.groupBy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sort rows by ascending or descending
|
||
|
*/
|
||
|
this._rawData.sort((a, b) => {
|
||
|
if (this.ascending) {
|
||
|
return a.name - b.name;
|
||
|
}
|
||
|
|
||
|
return b.name - a.name;
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 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._getFilters();
|
||
|
const entries = (await this.fyo.db.getAllRaw(
|
||
|
ModelNameEnum.AccountingLedgerEntry,
|
||
|
{
|
||
|
fields,
|
||
|
filters,
|
||
|
}
|
||
|
)) as RawLedgerEntry[];
|
||
|
|
||
|
this._rawData = entries.map((entry) => {
|
||
|
return {
|
||
|
name: parseInt(entry.name),
|
||
|
account: entry.account,
|
||
|
date: new Date(entry.date),
|
||
|
debit: this.fyo.pesa(entry.debit),
|
||
|
credit: this.fyo.pesa(entry.credit),
|
||
|
balance: this.fyo.pesa(0),
|
||
|
referenceType: entry.referenceType,
|
||
|
referenceName: entry.referenceName,
|
||
|
party: entry.party,
|
||
|
reverted: Boolean(entry.reverted),
|
||
|
reverts: entry.reverts,
|
||
|
} as LedgerEntry;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_getFilters(): QueryFilter {
|
||
|
const filters: QueryFilter = {};
|
||
|
const stringFilters = ['account', 'party', 'referenceName'];
|
||
|
|
||
|
for (const sf in stringFilters) {
|
||
|
const value = this[sf];
|
||
|
if (value === undefined) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
filters[sf] = value as string;
|
||
|
}
|
||
|
|
||
|
if (this.referenceType !== 'All') {
|
||
|
filters.referenceType = this.referenceType as string;
|
||
|
}
|
||
|
|
||
|
if (this.toDate) {
|
||
|
filters.date ??= [];
|
||
|
(filters.date as string[]).push('<=', this.toDate as string);
|
||
|
}
|
||
|
|
||
|
if (this.fromDate) {
|
||
|
filters.date ??= [];
|
||
|
(filters.date as string[]).push('>=', this.fromDate as string);
|
||
|
}
|
||
|
|
||
|
if (!this.reverted) {
|
||
|
filters.reverted = false;
|
||
|
}
|
||
|
|
||
|
return filters;
|
||
|
}
|
||
|
|
||
|
getFilters() {
|
||
|
return [
|
||
|
{
|
||
|
fieldtype: 'Select',
|
||
|
options: [
|
||
|
{ label: t`All`, value: 'All' },
|
||
|
{ label: t`Sales Invoices`, value: 'SalesInvoice' },
|
||
|
{ label: t`Purchase Invoices`, value: 'PurchaseInvoice' },
|
||
|
{ label: t`Payments`, value: 'Payment' },
|
||
|
{ label: t`Journal Entries`, value: 'JournalEntry' },
|
||
|
],
|
||
|
|
||
|
label: t`Reference Type`,
|
||
|
fieldname: 'referenceType',
|
||
|
placeholder: t`Reference Type`,
|
||
|
default: 'All',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'DynamicLink',
|
||
|
placeholder: t`Reference Name`,
|
||
|
references: 'referenceType',
|
||
|
label: t`Reference Name`,
|
||
|
fieldname: 'referenceName',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Link',
|
||
|
target: 'Account',
|
||
|
placeholder: t`Account`,
|
||
|
label: t`Account`,
|
||
|
fieldname: 'account',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Link',
|
||
|
target: 'Party',
|
||
|
label: t`Party`,
|
||
|
placeholder: t`Party`,
|
||
|
fieldname: 'party',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Date',
|
||
|
placeholder: t`From Date`,
|
||
|
label: t`From Date`,
|
||
|
fieldname: 'fromDate',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Date',
|
||
|
placeholder: t`To Date`,
|
||
|
label: t`To Date`,
|
||
|
fieldname: 'toDate',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Check',
|
||
|
default: false,
|
||
|
label: t`Cancelled`,
|
||
|
fieldname: 'reverted',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Check',
|
||
|
default: false,
|
||
|
label: t`Ascending`,
|
||
|
fieldname: 'ascending',
|
||
|
},
|
||
|
{
|
||
|
fieldtype: 'Check',
|
||
|
default: 'none',
|
||
|
label: t`Group By`,
|
||
|
fieldname: 'groupBy',
|
||
|
options: [
|
||
|
{ label: t`None`, value: 'none' },
|
||
|
{ label: t`Party`, value: 'party' },
|
||
|
{ label: t`Account`, value: 'account' },
|
||
|
{ label: t`Reference`, value: 'referenceName' },
|
||
|
],
|
||
|
},
|
||
|
] as Field[];
|
||
|
}
|
||
|
|
||
|
getColumns(): ColumnField[] {
|
||
|
let columns = [
|
||
|
{
|
||
|
label: t`Account`,
|
||
|
fieldtype: 'Link',
|
||
|
fieldname: 'account',
|
||
|
width: 1.5,
|
||
|
},
|
||
|
{
|
||
|
label: t`Date`,
|
||
|
fieldtype: 'Date',
|
||
|
fieldname: 'date',
|
||
|
width: 0.75,
|
||
|
},
|
||
|
{
|
||
|
label: t`Debit`,
|
||
|
fieldtype: 'Currency',
|
||
|
fieldname: 'debit',
|
||
|
width: 1.25,
|
||
|
},
|
||
|
{
|
||
|
label: t`Credit`,
|
||
|
fieldtype: 'Currency',
|
||
|
fieldname: 'credit',
|
||
|
width: 1.25,
|
||
|
},
|
||
|
{
|
||
|
label: t`Balance`,
|
||
|
fieldtype: 'Currency',
|
||
|
fieldname: 'balance',
|
||
|
width: 1.25,
|
||
|
},
|
||
|
{
|
||
|
label: t`Reference Type`,
|
||
|
fieldtype: 'Data',
|
||
|
fieldname: 'referenceType',
|
||
|
},
|
||
|
{
|
||
|
label: t`Reference Name`,
|
||
|
fieldtype: 'Data',
|
||
|
fieldname: 'referenceName',
|
||
|
},
|
||
|
{
|
||
|
label: t`Party`,
|
||
|
fieldtype: 'Link',
|
||
|
fieldname: 'party',
|
||
|
},
|
||
|
{
|
||
|
label: t`Reverted`,
|
||
|
fieldtype: 'Check',
|
||
|
fieldname: 'reverted',
|
||
|
},
|
||
|
] as ColumnField[];
|
||
|
|
||
|
if (!this.reverted) {
|
||
|
columns = columns.filter((f) => f.fieldname !== 'reverted');
|
||
|
}
|
||
|
|
||
|
return columns;
|
||
|
}
|
||
|
|
||
|
getActions(): Action[] {
|
||
|
return [];
|
||
|
}
|
||
|
}
|