2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 23:00:56 +00:00
books/reports/GeneralLedger/GeneralLedger.ts
2023-01-05 10:44:27 +05:30

428 lines
9.9 KiB
TypeScript

import { Fyo, t } from 'fyo';
import { DateTime } from 'luxon';
import { ModelNameEnum } from 'models/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';
type ReferenceType =
| ModelNameEnum.SalesInvoice
| ModelNameEnum.PurchaseInvoice
| ModelNameEnum.Payment
| ModelNameEnum.JournalEntry
| ModelNameEnum.Shipment
| ModelNameEnum.PurchaseReceipt
| 'All';
export class GeneralLedger extends LedgerReport {
static title = t`General Ledger`;
static reportName = 'general-ledger';
usePagination: boolean = true;
loading: boolean = false;
ascending: boolean = false;
reverted: boolean = false;
referenceType: ReferenceType = 'All';
groupBy: 'none' | 'party' | 'account' | 'referenceName' = 'none';
_rawData: LedgerEntry[] = [];
constructor(fyo: Fyo) {
super(fyo);
}
async setDefaultFilters() {
if (!this.toDate) {
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
}
}
async setReportData(filter?: string, force?: boolean) {
this.loading = true;
let sort = true;
if (force || filter !== 'grouped' || this._rawData.length === 0) {
await this._setRawData();
sort = false;
}
const map = this._getGroupedMap(sort);
this._setIndexOnEntries(map);
const { totalDebit, totalCredit } = this._getTotalsAndSetBalance(map);
const consolidated = this._consolidateEntries(map);
/**
* 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 - totalCredit,
referenceType: '',
referenceName: '',
party: '',
reverted: false,
reverts: '',
});
this.reportData = this._convertEntriesToReportData(consolidated);
this.loading = false;
}
_setIndexOnEntries(map: GroupedMap) {
let i = 1;
for (const key of map.keys()) {
for (const entry of map.get(key)!) {
entry.index = String(i);
i = i + 1;
}
}
}
_convertEntriesToReportData(entries: LedgerEntry[]): ReportData {
const reportData = [];
for (const entry of entries) {
const row = this._getRowFromEntry(entry, this.columns);
reportData.push(row);
}
return reportData;
}
_getRowFromEntry(entry: LedgerEntry, columns: ColumnField[]): ReportRow {
if (entry.name === -3) {
return {
isEmpty: true,
cells: columns.map((c) => ({
rawValue: '',
value: '',
width: c.width ?? 1,
})),
};
}
const row: ReportRow = { cells: [] };
for (const col of columns) {
const align = col.align ?? 'left';
const width = col.width ?? 1;
const fieldname = col.fieldname;
let value = entry[fieldname as keyof LedgerEntry];
const rawValue = value;
if (value === null || value === undefined) {
value = '';
}
if (value instanceof Date) {
value = this.fyo.format(value, FieldTypeEnum.Date);
}
if (typeof value === 'number' && fieldname !== 'index') {
value = this.fyo.format(value, FieldTypeEnum.Currency);
}
if (typeof value === 'boolean' && fieldname === 'reverted') {
value = value ? t`Reverted` : '';
} else {
value = String(value);
}
if (fieldname === 'referenceType') {
value = this.fyo.schemaMap[value]?.label ?? value;
}
row.cells.push({
italics: entry.name === -1,
bold: entry.name === -2,
value,
rawValue,
align,
width,
});
}
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 = 0;
let totalCredit = 0;
for (const key of map.keys()) {
let balance = 0;
let debit = 0;
let credit = 0;
for (const entry of map.get(key)!) {
debit += entry.debit!;
credit += entry.credit!;
const diff = entry.debit! - entry.credit!;
balance += 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 - credit,
referenceType: '',
referenceName: '',
party: '',
reverted: false,
reverts: '',
});
}
/**
* Total debit and credit for the final row
*/
totalDebit += debit;
totalCredit += credit;
}
return { totalDebit, totalCredit };
}
async _getQueryFilters(): Promise<QueryFilter> {
const filters: QueryFilter = {};
const stringFilters = ['account', 'party', 'referenceName'];
for (const sf of 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() {
const refTypeOptions = [
{ 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' },
];
if (!this.fyo.singles.AccountingSettings?.enableInventory) {
refTypeOptions.push(
{ label: t`Shipment`, value: 'Shipment' },
{ label: t`Purchase Receipt`, value: 'PurchaseReceipt' }
);
}
return [
{
fieldtype: 'Select',
options: refTypeOptions,
label: t`Ref Type`,
fieldname: 'referenceType',
placeholder: t`Ref Type`,
},
{
fieldtype: 'DynamicLink',
label: t`Ref. Name`,
references: 'referenceType',
placeholder: t`Ref Name`,
emptyMessage: t`Change Ref Type`,
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: 'Select',
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' },
],
},
{
fieldtype: 'Check',
label: t`Include Cancelled`,
fieldname: 'reverted',
},
{
fieldtype: 'Check',
label: t`Ascending Order`,
fieldname: 'ascending',
},
] as Field[];
}
getColumns(): ColumnField[] {
let columns = [
{
label: '#',
fieldtype: 'Int',
fieldname: 'index',
align: 'right',
width: 0.5,
},
{
label: t`Account`,
fieldtype: 'Link',
fieldname: 'account',
width: 1.5,
},
{
label: t`Date`,
fieldtype: 'Date',
fieldname: 'date',
},
{
label: t`Debit`,
fieldtype: 'Currency',
fieldname: 'debit',
align: 'right',
width: 1.25,
},
{
label: t`Credit`,
fieldtype: 'Currency',
fieldname: 'credit',
align: 'right',
width: 1.25,
},
{
label: t`Balance`,
fieldtype: 'Currency',
fieldname: 'balance',
align: 'right',
width: 1.25,
},
{
label: t`Party`,
fieldtype: 'Link',
fieldname: 'party',
},
{
label: t`Ref Name`,
fieldtype: 'Data',
fieldname: 'referenceName',
},
{
label: t`Ref Type`,
fieldtype: 'Data',
fieldname: 'referenceType',
},
{
label: t`Reverted`,
fieldtype: 'Check',
fieldname: 'reverted',
},
] as ColumnField[];
if (!this.reverted) {
columns = columns.filter((f) => f.fieldname !== 'reverted');
}
return columns;
}
}