diff --git a/accounting/gst.js b/accounting/gst.js index e068f44b..65fdb261 100644 --- a/accounting/gst.js +++ b/accounting/gst.js @@ -1,5 +1,6 @@ -import frappe, { t } from 'fyo'; +import { t } from 'fyo'; import { DateTime } from 'luxon'; +import { fyo } from 'src/initFyo'; import { showMessageDialog } from 'src/utils'; import { stateCodeMap } from '../regional/in'; import { exportCsv, saveExportData } from '../reports/commonExporter'; @@ -46,7 +47,7 @@ const IGST = { }; export async function generateGstr1Json(getReportData) { - const { gstin } = frappe.AccountingSettings; + const { gstin } = fyo.AccountingSettings; if (!gstin) { showMessageDialog({ message: t`Export Failed`, @@ -106,7 +107,7 @@ async function generateB2bData(rows) { itms: [], }; - const items = await frappe.db.getAllRaw('SalesInvoiceItem', { + const items = await fyo.db.getAllRaw('SalesInvoiceItem', { fields: ['*'], filters: { parent: invRecord.inum }, }); @@ -115,18 +116,18 @@ async function generateB2bData(rows) { const itemRecord = { num: item.hsnCode, itm_det: { - txval: frappe.pesa(item.baseAmount).float, + txval: fyo.pesa(item.baseAmount).float, rt: GST[item.tax], csamt: 0, - camt: frappe + camt: fyo .pesa(CSGST[item.tax] || 0) .mul(item.baseAmount) .div(100).float, - samt: frappe + samt: fyo .pesa(CSGST[item.tax] || 0) .mul(item.baseAmount) .div(100).float, - iamt: frappe + iamt: fyo .pesa(IGST[item.tax] || 0) .mul(item.baseAmount) .div(100).float, @@ -167,7 +168,7 @@ async function generateB2clData(invoices) { itms: [], }; - const items = await frappe.db.getAllRaw('SalesInvoiceItem', { + const items = await fyo.db.getAllRaw('SalesInvoiceItem', { fields: ['*'], filters: { parent: invRecord.inum }, }); @@ -176,10 +177,10 @@ async function generateB2clData(invoices) { const itemRecord = { num: item.hsnCode, itm_det: { - txval: frappe.pesa(item.baseAmount).float, + txval: fyo.pesa(item.baseAmount).float, rt: GST[item.tax], csamt: 0, - iamt: frappe + iamt: fyo .pesa(invoice.rate || 0) .mul(item.baseAmount) .div(100).float, @@ -228,7 +229,7 @@ async function generateB2csData(invoices) { } export async function generateGstr2Csv(getReportData) { - const { gstin } = frappe.AccountingSettings; + const { gstin } = fyo.AccountingSettings; if (!gstin) { showMessageDialog({ message: t`Export Failed`, @@ -309,7 +310,7 @@ async function generateB2bCsvGstr2(rows, columns) { } export async function generateGstr1Csv(getReportData) { - const { gstin } = frappe.AccountingSettings; + const { gstin } = fyo.AccountingSettings; if (!gstin) { showMessageDialog({ message: t`Export Failed`, diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index 84b4e395..69dc67e1 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -1,8 +1,8 @@ import Doc from 'fyo/model/doc'; import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types'; import { coreModels } from 'fyo/models'; -import { getRandomString } from 'fyo/utils'; import Observable from 'fyo/utils/observable'; +import { getRandomString } from 'utils'; import { Fyo } from '..'; import { DocValue, DocValueMap } from './types'; diff --git a/fyo/core/types.ts b/fyo/core/types.ts index 7dd4cac6..c4af5d15 100644 --- a/fyo/core/types.ts +++ b/fyo/core/types.ts @@ -4,7 +4,14 @@ import { RawValue } from 'schemas/types'; import { AuthDemuxBase } from 'utils/auth/types'; import { DatabaseDemuxBase } from 'utils/db/types'; -export type DocValue = string | number | boolean | Date | Money | null; +export type DocValue = + | string + | number + | boolean + | Date + | Money + | null + | undefined; export type DocValueMap = Record; export type RawValueMap = Record; diff --git a/fyo/index.ts b/fyo/index.ts index eef20690..cc8286c8 100644 --- a/fyo/index.ts +++ b/fyo/index.ts @@ -165,6 +165,11 @@ export class Fyo { await this.auth.logout(); } + getField(schemaName: string, fieldname: string) { + const schema = this.schemaMap[schemaName]; + return schema?.fields.find((f) => f.fieldname === fieldname); + } + store = { isDevelopment: false, appVersion: '', diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 4e7535ca..39e69698 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -16,8 +16,8 @@ import { Schema, TargetField, } from 'schemas/types'; -import { getIsNullOrUndef, getMapFromList } from 'utils'; -import { getRandomString, isPesa } from '../utils/index'; +import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils'; +import { isPesa } from '../utils/index'; import { areDocValuesEqual, getMissingMandatoryMessage, diff --git a/fyo/model/naming.ts b/fyo/model/naming.ts index 75c86279..e8c7a59e 100644 --- a/fyo/model/naming.ts +++ b/fyo/model/naming.ts @@ -1,9 +1,9 @@ import { Fyo } from 'fyo'; import NumberSeries from 'fyo/models/NumberSeries'; -import { getRandomString } from 'fyo/utils'; import { DEFAULT_SERIES_START } from 'fyo/utils/consts'; import { BaseError } from 'fyo/utils/errors'; import { Field, Schema } from 'schemas/types'; +import { getRandomString } from 'utils'; import Doc from './doc'; export function getNumberSeries(schema: Schema): Field | undefined { diff --git a/fyo/tests/testFyo.spec.ts b/fyo/tests/testFyo.spec.ts index 34e6ce27..ed2c266b 100644 --- a/fyo/tests/testFyo.spec.ts +++ b/fyo/tests/testFyo.spec.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import 'mocha'; -import models, { getRegionalModels } from 'models'; +import { getRegionalModels, models } from 'models'; import { getSchemas } from 'schemas'; import { Fyo } from '..'; import { DatabaseManager } from '../../backend/database/manager'; diff --git a/fyo/utils/index.ts b/fyo/utils/index.ts index b77ffb25..187e22e8 100644 --- a/fyo/utils/index.ts +++ b/fyo/utils/index.ts @@ -1,4 +1,3 @@ -import { Fyo } from 'fyo'; import Doc from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { pesa } from 'pesa'; @@ -11,23 +10,13 @@ export function slug(str: string) { .replace(/\s+/g, ''); } -export function getRandomString() { - return Math.random().toString(36).substr(3); -} - -export async function sleep(seconds: number) { - return new Promise((resolve) => { - setTimeout(resolve, seconds * 1000); - }); -} - export function range(n: number) { return Array(n) .fill(null) .map((_, i) => i); } -export function unique(list: unknown[], key = (it: unknown) => String(it)) { +export function unique(list: T[], key = (it: T) => String(it)) { const seen: Record = {}; return list.filter((item) => { const k = key(item); @@ -54,11 +43,11 @@ export function isPesa(value: unknown): boolean { return value instanceof pesa().constructor; } -export function getActions(doc: Doc, fyo: Fyo): Action[] { - const Model = fyo.models[doc.schemaName]; +export function getActions(doc: Doc): Action[] { + const Model = doc.fyo.models[doc.schemaName]; if (Model === undefined) { return []; } - return Model.getActions(fyo); + return Model.getActions(doc.fyo); } diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts index e7b54e2a..97da5eb1 100644 --- a/main/registerIpcMainActionListeners.ts +++ b/main/registerIpcMainActionListeners.ts @@ -134,7 +134,7 @@ export default function registerIpcMainActionListeners(main: Main) { countryCode ); } catch (error) { - response.error = error.toString(); + response.error = (error as Error).toString(); } return response; @@ -151,7 +151,7 @@ export default function registerIpcMainActionListeners(main: Main) { countryCode ); } catch (error) { - response.error = error.toString(); + response.error = (error as Error).toString(); } return response; @@ -165,7 +165,7 @@ export default function registerIpcMainActionListeners(main: Main) { try { response.data = await databaseManager.call(method, ...args); } catch (error) { - response.error = error.toString(); + response.error = (error as Error).toString(); } return response; @@ -179,7 +179,7 @@ export default function registerIpcMainActionListeners(main: Main) { try { response.data = await databaseManager.callBespoke(method, ...args); } catch (error) { - response.error = error.toString(); + response.error = (error as Error).toString(); } return response; diff --git a/models/baseModels/Account/types.ts b/models/baseModels/Account/types.ts index 9d8b5d49..477d3007 100644 --- a/models/baseModels/Account/types.ts +++ b/models/baseModels/Account/types.ts @@ -26,6 +26,7 @@ export type AccountRootType = | 'Income' | 'Expense'; + export interface COARootAccount { rootType: AccountRootType; [key: string]: COAChildAccount | AccountRootType; diff --git a/models/index.ts b/models/index.ts index 139ae4a4..09549533 100644 --- a/models/index.ts +++ b/models/index.ts @@ -17,7 +17,7 @@ import { SetupWizard } from './baseModels/SetupWizard/SetupWizard'; import { Tax } from './baseModels/Tax/Tax'; import { TaxSummary } from './baseModels/TaxSummary/TaxSummary'; -export default { +export const models = { Account, AccountingLedgerEntry, AccountingSettings, diff --git a/reports/AccountsReceivablePayable/AccountsReceivablePayable.js b/reports/AccountsReceivablePayable/AccountsReceivablePayable.js index 11658ac0..614c0c84 100644 --- a/reports/AccountsReceivablePayable/AccountsReceivablePayable.js +++ b/reports/AccountsReceivablePayable/AccountsReceivablePayable.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import fyo from 'fyo'; export default class AccountsReceivablePayable { async run(reportType, { date }) { @@ -65,7 +65,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) { // helpers async function getVouchers() { - return await frappe.db.getAll({ + return await fyo.db.getAll({ doctype: referenceType, fields: ['name', 'date'], filters: { @@ -140,7 +140,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) { const partyType = reportType === 'Receivable' ? 'customer' : 'supplier'; const partyList = ( - await frappe.db.getAll({ + await fyo.db.getAll({ doctype: 'Party', filters: { [partyType]: 1, @@ -148,7 +148,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) { }) ).map((d) => d.name); - return await frappe.db.getAll({ + return await fyo.db.getAll({ doctype: 'AccountingLedgerEntry', fields: [ 'name', diff --git a/reports/BalanceSheet/BalanceSheet.js b/reports/BalanceSheet/BalanceSheet.ts similarity index 66% rename from reports/BalanceSheet/BalanceSheet.js rename to reports/BalanceSheet/BalanceSheet.ts index 9daf6430..02431843 100644 --- a/reports/BalanceSheet/BalanceSheet.js +++ b/reports/BalanceSheet/BalanceSheet.ts @@ -1,9 +1,13 @@ +import { Fyo } from 'fyo'; import { unique } from 'fyo/utils'; -import { getData } from '../FinancialStatements/FinancialStatements'; +import { FinancialStatements } from 'reports/FinancialStatements/financialStatements'; +import { FinancialStatementOptions } from 'reports/types'; class BalanceSheet { - async run({ fromDate, toDate, periodicity }) { - let asset = await getData({ + 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, @@ -12,7 +16,7 @@ class BalanceSheet { accumulateValues: true, }); - let liability = await getData({ + const liability = await fs.getData({ rootType: 'Liability', balanceMustBe: 'Credit', fromDate, @@ -21,7 +25,7 @@ class BalanceSheet { accumulateValues: true, }); - let equity = await getData({ + const equity = await fs.getData({ rootType: 'Equity', balanceMustBe: 'Credit', fromDate, diff --git a/reports/BalanceSheet/viewConfig.js b/reports/BalanceSheet/viewConfig.js index a0a6b669..3d573002 100644 --- a/reports/BalanceSheet/viewConfig.js +++ b/reports/BalanceSheet/viewConfig.js @@ -1,5 +1,6 @@ -import frappe, { t } from 'fyo'; +import { t } from 'fyo'; import getCommonExportActions from '../commonExporter'; +import { fyo } from 'src/initFyo'; const periodicityMap = { Monthly: t`Monthly`, @@ -19,7 +20,7 @@ export default { label: t`To Date`, required: 1, default: async () => { - return (await frappe.getSingle('AccountingSettings')).fiscalYearEnd; + return (await fyo.doc.getSingle('AccountingSettings')).fiscalYearEnd; }, }, { diff --git a/reports/BankReconciliation/BankReconciliation.js b/reports/BankReconciliation/BankReconciliation.js index 7e8bf17f..4540f16b 100644 --- a/reports/BankReconciliation/BankReconciliation.js +++ b/reports/BankReconciliation/BankReconciliation.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import { fyo } from 'src/initFyo' class BankReconciliation { async run(params) { @@ -15,7 +15,7 @@ class BankReconciliation { filters.paymentMethod = ['in', ['Cheque', 'Transfer']]; - let data = await frappe.db.getAll({ + let data = await fyo.db.getAll({ doctype: 'Payment', fields: [ 'date', @@ -31,7 +31,7 @@ class BankReconciliation { }); for (var i = 0; i < data.length; i++) { - let ledger = await frappe.db.getAll({ + let ledger = await fyo.db.getAll({ doctype: 'AccountingLedgerEntry', fields: ['date', 'referenceType', 'referenceName', 'debit', 'credit'], filters: { diff --git a/reports/BankReconciliation/BankReconciliationImport.js b/reports/BankReconciliation/BankReconciliationImport.js index 4af6c2b1..b407f713 100644 --- a/reports/BankReconciliation/BankReconciliationImport.js +++ b/reports/BankReconciliation/BankReconciliationImport.js @@ -1,5 +1,5 @@ import csv2json from 'csvjson-csv2json'; -import frappe from 'fyo'; +import { fyo } from 'src/initFyo'; import ReconciliationValidation from '../../src/components/ReconciliationValidation'; export const fileImportHandler = (file, report) => { @@ -42,7 +42,7 @@ export const findMatchingReferences = async (json, report) => { const references = json.map((row) => { return row[referenceField]; }); - const payments = await frappe.db.getAll({ + const payments = await fyo.db.getAll({ doctype: 'Payment', fields: ['*'], filters: { @@ -60,11 +60,11 @@ export const findMatchingReferences = async (json, report) => { }); const normalizedEntries = entries.map((entry) => { return { - 'Posting Date': frappe.format(entry.date, 'Date'), + 'Posting Date': fyo.format(entry.date, 'Date'), 'Payment Entry': entry.name, 'Ref/Cheq. ID': entry[referenceField], 'Cr/Dr': - frappe.parseNumber(entry[debitField]) > 0 + fyo.parseNumber(entry[debitField]) > 0 ? entry[debitField] + ' Dr.' : entry[creditField] + ' Cr.', 'Clearance Date': entry[clearanceDateField], diff --git a/reports/Cashflow/Cashflow.js b/reports/Cashflow/Cashflow.js deleted file mode 100644 index 0fed2e6d..00000000 --- a/reports/Cashflow/Cashflow.js +++ /dev/null @@ -1,40 +0,0 @@ -import frappe from 'fyo'; -import { DateTime } from 'luxon'; -import { - getFiscalYear, - getPeriodList, -} from '../FinancialStatements/FinancialStatements'; - -class Cashflow { - async run({ fromDate, toDate, periodicity }) { - const res = await frappe.db.getCashflow(fromDate, toDate); - let fiscalYear = await getFiscalYear(); - let periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear); - - let data = periodList.map((periodKey) => { - let monthYear = this.getMonthYear(periodKey, 'MMM yyyy'); - let cashflowForPeriod = res.find((d) => d['month-year'] === monthYear); - if (cashflowForPeriod) { - cashflowForPeriod.periodKey = periodKey; - return cashflowForPeriod; - } - return { - inflow: 0, - outflow: 0, - periodKey, - 'month-year': monthYear, - }; - }); - - return { - data, - periodList, - }; - } - - getMonthYear(periodKey, format) { - return DateTime.fromFormat(periodKey, format).toFormat('MM-yyyy'); - } -} - -export default Cashflow; diff --git a/reports/Cashflow/Cashflow.ts b/reports/Cashflow/Cashflow.ts new file mode 100644 index 00000000..568b044a --- /dev/null +++ b/reports/Cashflow/Cashflow.ts @@ -0,0 +1,52 @@ +import { Fyo } from 'fyo'; +import { DateTime } from 'luxon'; +import { + getFiscalYear, + getPeriodList, +} from 'reports/FinancialStatements/financialStatements'; +import { FinancialStatementOptions } from 'reports/types'; + +class Cashflow { + fyo: Fyo; + constructor(fyo: Fyo) { + this.fyo = fyo; + } + async run(options: FinancialStatementOptions) { + const { fromDate, toDate, periodicity } = options; + const res = await this.fyo.db.getCashflow(fromDate, toDate); + const fiscalYear = await getFiscalYear(this.fyo); + const periodList = getPeriodList( + fromDate, + toDate, + periodicity!, + fiscalYear + ); + + const data = periodList.map((periodKey) => { + const monthYear = this.getMonthYear(periodKey, 'MMM yyyy'); + const cashflowForPeriod = res.find((d) => d['month-year'] === monthYear); + + if (cashflowForPeriod) { + return { ...cashflowForPeriod, periodKey }; + } + + return { + inflow: 0, + outflow: 0, + periodKey, + 'month-year': monthYear, + }; + }); + + return { + data, + periodList, + }; + } + + getMonthYear(periodKey: string, format: string) { + return DateTime.fromFormat(periodKey, format).toFormat('MM-yyyy'); + } +} + +export default Cashflow; diff --git a/reports/FinancialStatements/FinancialStatements.js b/reports/FinancialStatements/FinancialStatements.js deleted file mode 100644 index 2e382a63..00000000 --- a/reports/FinancialStatements/FinancialStatements.js +++ /dev/null @@ -1,334 +0,0 @@ -import frappe from 'fyo'; -import { DateTime } from 'luxon'; -import { convertPesaValuesToFloat } from '../../src/utils'; - -export async function getData({ - rootType, - balanceMustBe = 'Debit', - fromDate, - toDate, - periodicity = 'Monthly', - accumulateValues = false, -}) { - let accounts = await getAccounts(rootType); - let fiscalYear = await getFiscalYear(); - let ledgerEntries = await getLedgerEntries(fromDate, toDate, accounts); - let periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear); - - for (let account of accounts) { - const entries = ledgerEntries.filter( - (entry) => entry.account === account.name - ); - - for (let entry of entries) { - let periodKey = getPeriodKey(entry.date, periodicity, fiscalYear); - - if (!account[periodKey]) { - account[periodKey] = frappe.pesa(0.0); - } - - const multiplier = balanceMustBe === 'Debit' ? 1 : -1; - const value = entry.debit.sub(entry.credit).mul(multiplier); - account[periodKey] = value.add(account[periodKey]); - } - } - - if (accumulateValues) { - periodList.forEach((periodKey, i) => { - if (i === 0) return; - const previousPeriodKey = periodList[i - 1]; - - for (let account of accounts) { - if (!account[periodKey]) { - account[periodKey] = frappe.pesa(0.0); - } - - account[periodKey] = account[periodKey].add( - account[previousPeriodKey] ?? 0 - ); - } - }); - } - - // calculate totalRow - let totalRow = { - account: `Total ${rootType} (${balanceMustBe})`, - }; - - periodList.forEach((periodKey) => { - if (!totalRow[periodKey]) { - totalRow[periodKey] = frappe.pesa(0.0); - } - - for (let account of accounts) { - totalRow[periodKey] = totalRow[periodKey].add(account[periodKey] ?? 0.0); - } - }); - - convertPesaValuesToFloat(totalRow); - accounts.forEach(convertPesaValuesToFloat); - - return { accounts, totalRow, periodList }; -} - -export async function getTrialBalance({ rootType, fromDate, toDate }) { - let accounts = await getAccounts(rootType); - let ledgerEntries = await getLedgerEntries(null, toDate, accounts); - - for (let account of accounts) { - const accountEntries = ledgerEntries.filter( - (entry) => entry.account === account.name - ); - // opening - const beforePeriodEntries = accountEntries.filter( - (entry) => entry.date < fromDate - ); - account.opening = beforePeriodEntries.reduce( - (acc, entry) => acc.add(entry.debit).sub(entry.credit), - frappe.pesa(0) - ); - - if (account.opening.gte(0)) { - account.openingDebit = account.opening; - account.openingCredit = frappe.pesa(0); - } else { - account.openingCredit = account.opening.neg(); - account.openingDebit = frappe.pesa(0); - } - - // debit / credit - const periodEntries = accountEntries.filter( - (entry) => entry.date >= fromDate && entry.date < toDate - ); - account.debit = periodEntries.reduce( - (acc, entry) => acc.add(entry.debit), - frappe.pesa(0) - ); - account.credit = periodEntries.reduce( - (acc, entry) => acc.add(entry.credit), - frappe.pesa(0) - ); - - // closing - account.closing = account.opening.add(account.debit).sub(account.credit); - - if (account.closing.gte(0)) { - account.closingDebit = account.closing; - account.closingCredit = frappe.pesa(0); - } else { - account.closingCredit = account.closing.neg(); - account.closingDebit = frappe.pesa(0); - } - - if (account.debit.neq(0) || account.credit.neq(0)) { - setParentEntry(account, account.parentAccount); - } - } - - function setParentEntry(leafAccount, parentName) { - for (let acc of accounts) { - if (acc.name === parentName) { - acc.debit = acc.debit.add(leafAccount.debit); - acc.credit = acc.credit.add(leafAccount.credit); - acc.closing = acc.opening.add(acc.debit).sub(acc.credit); - if (acc.closing.gte(0)) { - acc.closingDebit = acc.closing; - } else { - acc.closingCredit = acc.closing.neg(); - } - if (acc.parentAccount) { - setParentEntry(leafAccount, acc.parentAccount); - } else { - return; - } - } - } - } - - accounts.forEach(convertPesaValuesToFloat); - return accounts; -} - -export function getPeriodList(fromDate, toDate, periodicity, fiscalYear) { - if (!fromDate) { - fromDate = fiscalYear.start; - } - - let monthsToAdd = { - Monthly: 1, - Quarterly: 3, - 'Half Yearly': 6, - Yearly: 12, - }[periodicity]; - - let startDate = DateTime.fromISO(fromDate).startOf('month'); - let endDate = DateTime.fromISO(toDate).endOf('month'); - let curDate = startDate; - let out = []; - - while (curDate <= endDate) { - out.push(getPeriodKey(curDate, periodicity, fiscalYear)); - curDate = curDate.plus({ months: monthsToAdd }); - } - - return out; -} - -function getPeriodKey(date, periodicity, fiscalYear) { - let key; - let { start, end, quarters, isSplit } = fiscalYear; - let dateObj = DateTime.fromISO(date); - let { month, quarter, year } = dateObj; - let fisacalStart = DateTime.fromISO(start); - let fisacalEnd = DateTime.fromISO(end); - - let getKey = { - Monthly: () => `${dateObj.monthShort} ${year}`, - Quarterly: () => { - const key = - month < fisacalStart.month - ? `${year - 1} - ${year}` - : `${year} - ${year + 1}`; - let strYear = isSplit ? key : `${year}`; - return { - 1: `Q1 ${strYear}`, - 2: `Q2 ${strYear}`, - 3: `Q3 ${strYear}`, - 4: `Q4 ${strYear}`, - }[quarters[month - 1]]; - }, - 'Half Yearly': () => { - const key = - month < fisacalStart.month - ? `${year - 1} - ${year}` - : `${year} - ${year + 1}`; - let strYear = isSplit ? key : `${year}`; - return { - 1: `1st Half ${strYear}`, - 2: `1st Half ${strYear}`, - 3: `2nd Half ${strYear}`, - 4: `2nd Half ${strYear}`, - }[quarters[month - 1]]; - }, - Yearly: () => { - const key = - month < fisacalStart.month - ? `${year - 1} - ${year}` - : `${year} - ${year + 1}`; - let strYear = isSplit ? key : `${year}`; - return `FY ${strYear}`; - }, - }[periodicity]; - - return getKey(); -} - -function setIndentLevel(accounts, parentAccount, level) { - if (!parentAccount) { - // root - parentAccount = null; - level = 0; - } - - accounts.forEach((account) => { - if ( - account.parentAccount === parentAccount && - account.indent === undefined - ) { - account.indent = level; - setIndentLevel(accounts, account.name, level + 1); - } - }); - - return accounts; -} - -function sortAccounts(accounts) { - let out = []; - let pushed = {}; - - pushToOut(null); - - function pushToOut(parentAccount) { - accounts.forEach((account) => { - if (account.parentAccount === parentAccount && !pushed[account.name]) { - out.push(account); - pushed[account.name] = 1; - - pushToOut(account.name); - } - }); - } - - return out; -} - -async function getLedgerEntries(fromDate, toDate, accounts) { - const dateFilter = () => { - const before = ['<=', toDate]; - const after = ['>=', fromDate]; - if (fromDate) { - return [...after, ...before]; - } - return before; - }; - - const ledgerEntries = await frappe.db.getAll({ - doctype: 'AccountingLedgerEntry', - fields: ['account', 'debit', 'credit', 'date'], - filters: { - account: ['in', accounts.map((d) => d.name)], - date: dateFilter(), - }, - }); - - return ledgerEntries; -} - -async function getAccounts(rootType) { - let accounts = await frappe.db.getAll({ - doctype: 'Account', - fields: ['name', 'parentAccount', 'isGroup'], - filters: { - rootType, - }, - }); - - accounts = setIndentLevel(accounts); - accounts = sortAccounts(accounts); - - accounts.forEach((account) => { - account.account = account.name; - }); - - return accounts; -} - -export async function getFiscalYear() { - let { fiscalYearStart, fiscalYearEnd } = await frappe.getSingle( - 'AccountingSettings' - ); - - //right now quaters received from luxon lib is fixed to Jan as starting quarter - //moving the financial quarters, according to of start of fiscal year month - let quarters = [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]; - let start = DateTime.fromISO(fiscalYearStart); - quarters.unshift(...quarters.splice(13 - start.month, 11)); - - //check if fiscal year ends in next year - let end = DateTime.fromISO(fiscalYearEnd); - let isFiscalSplit = start.year - end.year; - - return { - start: fiscalYearStart, - end: fiscalYearEnd, - quarters: quarters, - isSplit: isFiscalSplit, - }; -} - -export default { - getData, - getTrialBalance, - getPeriodList, -}; diff --git a/reports/FinancialStatements/financialStatements.ts b/reports/FinancialStatements/financialStatements.ts new file mode 100644 index 00000000..24185928 --- /dev/null +++ b/reports/FinancialStatements/financialStatements.ts @@ -0,0 +1,426 @@ +import { Fyo } from 'fyo'; +import { DocValueMap } from 'fyo/core/types'; +import { DateTime } from 'luxon'; +import { AccountRootType } from 'models/baseModels/Account/types'; +import Money from 'pesa/dist/types/src/money'; +import { + BalanceType, + FinancialStatementOptions, + Periodicity, +} from 'reports/types'; +import { convertPesaValuesToFloat } from 'src/utils'; + +interface FiscalYear { + start: string; + end: string; + quarters: number[]; + isSplit: number; +} + +interface AccountInfo extends DocValueMap { + name: string; + parentAccount: string; + isGroup: boolean; + account?: string; + indent?: number; +} + +interface LedgerInfo extends DocValueMap { + account: string; + debit: Money; + credit: Money; + date: string; +} + +export class FinancialStatements { + fyo: Fyo; + constructor(fyo: Fyo) { + this.fyo = fyo; + } + + async getData(options: FinancialStatementOptions) { + const rootType = options.rootType; + const balanceMustBe = options.balanceMustBe ?? 'Debit'; + const fromDate = options.fromDate; + const toDate = options.toDate; + const periodicity = options.periodicity ?? 'Monthly'; + const accumulateValues = options.accumulateValues ?? false; + + const accounts = await this.getAccounts(rootType); + const fiscalYear = await getFiscalYear(this.fyo); + const ledgerEntries = await this.getLedgerEntries( + fromDate, + toDate, + accounts + ); + const periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear); + this.setPeriodAmounts( + accounts, + ledgerEntries, + periodicity, + fiscalYear, + balanceMustBe + ); + + if (accumulateValues) { + this.accumulateValues(accounts, periodList); + } + + const totalRow = this.getTotalRow( + rootType, + balanceMustBe, + periodList, + accounts + ); + accounts.forEach(convertPesaValuesToFloat); + + return { accounts, totalRow, periodList }; + } + + setPeriodAmounts( + accounts: AccountInfo[], + ledgerEntries: LedgerInfo[], + periodicity: Periodicity, + fiscalYear: FiscalYear, + balanceMustBe: BalanceType + ) { + for (const account of accounts) { + const entries = ledgerEntries.filter( + (entry) => entry.account === account.name + ); + + for (const entry of entries) { + const periodKey = getPeriodKey(entry.date, periodicity, fiscalYear); + + if (account[periodKey] === undefined) { + account[periodKey] = this.fyo.pesa(0.0); + } + + const multiplier = balanceMustBe === 'Debit' ? 1 : -1; + const value = entry.debit.sub(entry.credit).mul(multiplier); + account[periodKey] = value.add(account[periodKey] as Money); + } + } + } + + getTotalRow( + rootType: AccountRootType, + balanceMustBe: BalanceType, + periodList: string[], + accounts: AccountInfo[] + ) { + const totalRow: DocValueMap = { + account: `Total ${rootType} (${balanceMustBe})`, + }; + + periodList.forEach((periodKey) => { + if (totalRow[periodKey] === undefined) { + totalRow[periodKey] = this.fyo.pesa(0.0); + } + + for (const account of accounts) { + totalRow[periodKey] = (totalRow[periodKey] as Money).add( + (account[periodKey] as Money) ?? 0.0 + ); + } + }); + + convertPesaValuesToFloat(totalRow); + return totalRow; + } + + async accumulateValues(accounts: AccountInfo[], periodList: string[]) { + periodList.forEach((periodKey, i) => { + if (i === 0) { + return; + } + + const previousPeriodKey = periodList[i - 1]; + + for (const account of accounts) { + if (!account[periodKey]) { + account[periodKey] = this.fyo.pesa(0.0); + } + + account[periodKey] = (account[periodKey] as Money).add( + (account[previousPeriodKey] as Money | undefined) ?? 0 + ); + } + }); + } + + async getAccounts(rootType: AccountRootType) { + let accounts = (await this.fyo.db.getAll('Account', { + fields: ['name', 'parentAccount', 'isGroup'], + filters: { + rootType, + }, + })) as AccountInfo[]; + + accounts = setIndentLevel(accounts); + accounts = sortAccounts(accounts); + + accounts.forEach((account) => { + account.account = account.name; + }); + + return accounts; + } + + async getLedgerEntries( + fromDate: string | null, + toDate: string, + accounts: AccountInfo[] + ) { + const accountFilter = ['in', accounts.map((d) => d.name)]; + let dateFilter: string[] = ['<=', toDate]; + if (fromDate) { + dateFilter = ['>=', fromDate, '<=', toDate]; + } + + const ledgerEntries = (await this.fyo.db.getAll('AccountingLedgerEntry', { + fields: ['account', 'debit', 'credit', 'date'], + filters: { + account: accountFilter, + date: dateFilter, + }, + })) as LedgerInfo[]; + + return ledgerEntries; + } + + async getTrialBalance(options: FinancialStatementOptions) { + const { rootType, fromDate, toDate } = options; + const accounts = await this.getAccounts(rootType); + const ledgerEntries = await this.getLedgerEntries(null, toDate, accounts); + + for (const account of accounts) { + const accountEntries = ledgerEntries.filter( + (entry) => entry.account === account.name + ); + // opening + const beforePeriodEntries = accountEntries.filter( + (entry) => entry.date < fromDate + ); + + account.opening = beforePeriodEntries.reduce( + (acc, entry) => acc.add(entry.debit).sub(entry.credit), + this.fyo.pesa(0) + ); + + if (account.opening.gte(0)) { + account.openingDebit = account.opening; + account.openingCredit = this.fyo.pesa(0); + } else { + account.openingCredit = account.opening.neg(); + account.openingDebit = this.fyo.pesa(0); + } + + // debit / credit + const periodEntries = accountEntries.filter( + (entry) => entry.date >= fromDate && entry.date < toDate + ); + account.debit = periodEntries.reduce( + (acc, entry) => acc.add(entry.debit), + this.fyo.pesa(0) + ); + account.credit = periodEntries.reduce( + (acc, entry) => acc.add(entry.credit), + this.fyo.pesa(0) + ); + + // closing + account.closing = account.opening.add(account.debit).sub(account.credit); + + if (account.closing.gte(0)) { + account.closingDebit = account.closing; + account.closingCredit = this.fyo.pesa(0); + } else { + account.closingCredit = account.closing.neg(); + account.closingDebit = this.fyo.pesa(0); + } + + if (account.debit.neq(0) || account.credit.neq(0)) { + setParentEntry(account, account.parentAccount); + } + } + + function setParentEntry(leafAccount: AccountInfo, parentName: string) { + for (const acc of accounts) { + if (acc.name === parentName) { + acc.debit = (acc.debit as Money).add(leafAccount.debit as Money); + acc.credit = (acc.credit as Money).add(leafAccount.credit as Money); + acc.closing = (acc.opening as Money).add(acc.debit).sub(acc.credit); + + if (acc.closing.gte(0)) { + acc.closingDebit = acc.closing; + } else { + acc.closingCredit = acc.closing.neg(); + } + + if (acc.parentAccount) { + setParentEntry(leafAccount, acc.parentAccount); + } else { + return; + } + } + } + } + + accounts.forEach(convertPesaValuesToFloat); + return accounts; + } +} + +function setIndentLevel( + accounts: AccountInfo[], + parentAccount?: string | null, + level?: number +): AccountInfo[] { + if (parentAccount === undefined) { + parentAccount = null; + level = 0; + } + + accounts.forEach((account) => { + if ( + account.parentAccount === parentAccount && + account.indent === undefined + ) { + account.indent = level; + setIndentLevel(accounts, account.name, (level ?? 0) + 1); + } + }); + + return accounts; +} + +function sortAccounts(accounts: AccountInfo[]) { + const out: AccountInfo[] = []; + const pushed: Record = {}; + + pushToOut(null); + + function pushToOut(parentAccount: string | null) { + accounts.forEach((account) => { + if (pushed[account.name] && account.parentAccount !== parentAccount) { + return; + } + + out.push(account); + pushed[account.name] = true; + + pushToOut(account.name); + }); + } + + return out; +} + +export function getPeriodList( + fromDate: string, + toDate: string, + periodicity: Periodicity, + fiscalYear: FiscalYear +) { + if (!fromDate) { + fromDate = fiscalYear.start; + } + + const monthsToAdd = { + Monthly: 1, + Quarterly: 3, + 'Half Yearly': 6, + Yearly: 12, + }[periodicity]; + + const startDate = DateTime.fromISO(fromDate).startOf('month'); + const endDate = DateTime.fromISO(toDate).endOf('month'); + let curDate = startDate; + const periodKeyList: string[] = []; + + while (curDate <= endDate) { + const periodKey = getPeriodKey(curDate, periodicity, fiscalYear); + periodKeyList.push(periodKey); + curDate = curDate.plus({ months: monthsToAdd }); + } + + return periodKeyList; +} + +function getPeriodKey( + dateObj: DateTime | string, + periodicity: Periodicity, + fiscalYear: FiscalYear +) { + if (typeof dateObj === 'string') { + dateObj = DateTime.fromISO(dateObj); + } + + const { start, quarters, isSplit } = fiscalYear; + const { month, year } = dateObj; + const fisacalStart = DateTime.fromISO(start); + + if (periodicity === 'Monthly') { + return `${dateObj.monthShort} ${year}`; + } + + if (periodicity === 'Quarterly') { + const key = + month < fisacalStart.month + ? `${year - 1} - ${year}` + : `${year} - ${year + 1}`; + const strYear = isSplit ? key : `${year}`; + return { + 1: `Q1 ${strYear}`, + 2: `Q2 ${strYear}`, + 3: `Q3 ${strYear}`, + 4: `Q4 ${strYear}`, + }[quarters[month - 1]] as string; + } + + if (periodicity === 'Half Yearly') { + const key = + month < fisacalStart.month + ? `${year - 1} - ${year}` + : `${year} - ${year + 1}`; + const strYear = isSplit ? key : `${year}`; + return { + 1: `1st Half ${strYear}`, + 2: `1st Half ${strYear}`, + 3: `2nd Half ${strYear}`, + 4: `2nd Half ${strYear}`, + }[quarters[month - 1]] as string; + } + + const key = + month < fisacalStart.month + ? `${year - 1} - ${year}` + : `${year} - ${year + 1}`; + const strYear = isSplit ? key : `${year}`; + return `FY ${strYear}`; +} + +export async function getFiscalYear(fyo: Fyo): Promise { + const accountingSettings = await fyo.doc.getSingle('AccountingSettings'); + + const fiscalYearStart = accountingSettings.fiscalYearStart as string; + const fiscalYearEnd = accountingSettings.fiscalYearEnd as string; + + //right now quaters received from luxon lib is fixed to Jan as starting quarter + //moving the financial quarters, according to of start of fiscal year month + const quarters = [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]; + const start = DateTime.fromISO(fiscalYearStart); + quarters.unshift(...quarters.splice(13 - start.month, 11)); + + //check if fiscal year ends in next year + const end = DateTime.fromISO(fiscalYearEnd); + const isFiscalSplit = start.year - end.year; + + return { + start: fiscalYearStart, + end: fiscalYearEnd, + quarters: quarters, + isSplit: isFiscalSplit, + }; +} diff --git a/reports/GeneralLedger/GeneralLedger.js b/reports/GeneralLedger/GeneralLedger.js index a1cdbbec..35cee083 100644 --- a/reports/GeneralLedger/GeneralLedger.js +++ b/reports/GeneralLedger/GeneralLedger.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import { fyo } from 'src/initFyo'; class GeneralLedger { async run(params) { @@ -15,7 +15,7 @@ class GeneralLedger { } let data = ( - await frappe.db.getAll({ + await fyo.db.getAll({ doctype: 'AccountingLedgerEntry', fields: [ 'date', @@ -40,6 +40,7 @@ class GeneralLedger { return this.appendOpeningEntry(data); } + appendOpeningEntry(data) { let glEntries = []; let balance = 0, diff --git a/reports/GoodsAndServiceTax/BaseGSTR.js b/reports/GoodsAndServiceTax/BaseGSTR.js index 0577584e..b0a11072 100644 --- a/reports/GoodsAndServiceTax/BaseGSTR.js +++ b/reports/GoodsAndServiceTax/BaseGSTR.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import { fyo } from 'src/initFyo'; import { stateCodeMap } from '../../accounting/gst'; import { convertPesaValuesToFloat } from '../../src/utils'; @@ -7,7 +7,7 @@ class BaseGSTR { if (['GSTR-1', 'GSTR-2'].includes(gstrType)) { const place = filters.place; delete filters.place; - let entries = await frappe.db.getAll({ + let entries = await fyo.db.getAll({ doctype: gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice', filters, }); @@ -39,21 +39,18 @@ class BaseGSTR { } async getRow(ledgerEntry) { - ledgerEntry = await frappe.doc.getDoc( - ledgerEntry.doctype, - ledgerEntry.name - ); + ledgerEntry = await fyo.doc.getDoc(ledgerEntry.doctype, ledgerEntry.name); const row = {}; - const { gstin } = frappe.AccountingSettings; + const { gstin } = fyo.AccountingSettings; - let party = await frappe.doc.getDoc( + let party = await fyo.doc.getDoc( 'Party', ledgerEntry.customer || ledgerEntry.supplier ); if (party.address) { - let addressDetails = await frappe.doc.getDoc('Address', party.address); + let addressDetails = await fyo.doc.getDoc('Address', party.address); row.place = addressDetails.pos || ''; } diff --git a/reports/ProfitAndLoss/ProfitAndLoss.js b/reports/ProfitAndLoss/ProfitAndLoss.js deleted file mode 100644 index 06999f4b..00000000 --- a/reports/ProfitAndLoss/ProfitAndLoss.js +++ /dev/null @@ -1,83 +0,0 @@ -import { unique } from 'fyo/utils'; -import { getData } from '../FinancialStatements/FinancialStatements'; - -class ProfitAndLoss { - async run({ fromDate, toDate, periodicity }) { - let income = await getData({ - rootType: 'Income', - balanceMustBe: 'Credit', - fromDate, - toDate, - periodicity, - }); - - let expense = await getData({ - rootType: 'Expense', - balanceMustBe: 'Debit', - fromDate, - toDate, - periodicity, - }); - - let incomeTotalRow = income.totalRow; - incomeTotalRow.account = { - template: `${income.totalRow.account}`, - }; - - let expenseTotalRow = expense.totalRow; - expenseTotalRow.account = { - template: `${expense.totalRow.account}`, - }; - - let rows = [ - ...income.accounts, - incomeTotalRow, - { - account: { - template: ' ', - }, - isGroup: 1, - }, - ...expense.accounts, - expenseTotalRow, - { - account: { - template: ' ', - }, - isGroup: 1, - }, - ]; - - rows = rows.map((row) => { - if (row.indent === 0) { - row.account = { - template: `${row.account}`, - }; - } - return row; - }); - - const columns = unique([...income.periodList, ...expense.periodList]); - - let profitRow = { - account: 'Total Profit', - }; - - for (let column of columns) { - profitRow[column] = - (income.totalRow[column] || 0.0) - (expense.totalRow[column] || 0.0); - - rows.forEach((row) => { - if (!row.isGroup) { - row[column] = row[column] || 0.0; - } - }); - } - - rows.push(profitRow); - - return { rows, columns }; - } -} - -export default ProfitAndLoss; diff --git a/reports/ProfitAndLoss/ProfitAndLoss.ts b/reports/ProfitAndLoss/ProfitAndLoss.ts new file mode 100644 index 00000000..c7509276 --- /dev/null +++ b/reports/ProfitAndLoss/ProfitAndLoss.ts @@ -0,0 +1,107 @@ +import { Fyo } from 'fyo'; +import { unique } from 'fyo/utils'; +import { FinancialStatements } from 'reports/FinancialStatements/financialStatements'; +import { FinancialStatementOptions } from 'reports/types'; + +interface Row { + indent?: number; + account: string | { template: string }; + isGroup?: boolean; + [key: string]: unknown; +} + +export default class ProfitAndLoss { + fyo: Fyo; + constructor(fyo: Fyo) { + this.fyo = 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, + }); + + const expense = await fs.getData({ + rootType: 'Expense', + balanceMustBe: 'Debit', + fromDate, + toDate, + periodicity, + }); + + const incomeAccount = income.totalRow.account as string; + const incomeTotalRow = { + ...income.totalRow, + account: { + template: `${incomeAccount}`, + }, + }; + + const expenseAccount = expense.totalRow.account as string; + const expenseTotalRow = { + ...expense.totalRow, + account: { + template: `${expenseAccount}`, + }, + }; + + let rows = [ + ...income.accounts, + incomeTotalRow, + { + account: { + template: ' ', + }, + isGroup: true, + }, + ...expense.accounts, + expenseTotalRow, + { + account: { + template: ' ', + }, + isGroup: true, + }, + ] as Row[]; + + rows = rows.map((row) => { + if (row.indent === 0) { + row.account = { + template: `${row.account}`, + }; + } + 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; + } + }); + } + + rows.push(profitRow); + + return { rows, columns }; + } +} diff --git a/reports/ProfitAndLoss/viewConfig.js b/reports/ProfitAndLoss/viewConfig.js index 654aeb47..962c33f9 100644 --- a/reports/ProfitAndLoss/viewConfig.js +++ b/reports/ProfitAndLoss/viewConfig.js @@ -1,4 +1,5 @@ -import frappe, { t } from 'fyo'; +import { t } from 'fyo'; +import { fyo } from 'src/initFyo'; import getCommonExportActions from '../commonExporter'; const title = t`Profit and Loss`; @@ -22,7 +23,7 @@ export default { label: t`From Date`, required: 1, default: async () => { - return (await frappe.getSingle('AccountingSettings')).fiscalYearStart; + return (await fyo.getSingle('AccountingSettings')).fiscalYearStart; }, }, { @@ -33,7 +34,7 @@ export default { label: t`To Date`, required: 1, default: async () => { - return (await frappe.getSingle('AccountingSettings')).fiscalYearEnd; + return (await fyo.getSingle('AccountingSettings')).fiscalYearEnd; }, }, { diff --git a/reports/PurchaseRegister/PurchaseRegister.js b/reports/PurchaseRegister/PurchaseRegister.js index 8d1c9f1a..8210d8cc 100644 --- a/reports/PurchaseRegister/PurchaseRegister.js +++ b/reports/PurchaseRegister/PurchaseRegister.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import { fyo } from 'src/initFyo'; class PurchaseRegister { async run({ fromDate, toDate, supplier }) { @@ -18,7 +18,7 @@ class PurchaseRegister { } filters.submitted = 1; - const bills = await frappe.db.getAll({ + const bills = await fyo.db.getAll({ doctype: 'PurchaseInvoice', fields: ['name', 'date', 'supplier', 'account', 'netTotal', 'grandTotal'], filters, @@ -28,7 +28,7 @@ class PurchaseRegister { const billNames = bills.map((d) => d.name); - const taxes = await frappe.db.getAll({ + const taxes = await fyo.db.getAll({ doctype: 'TaxSummary', fields: ['parent', 'amount'], filters: { diff --git a/reports/SalesRegister/SalesRegister.js b/reports/SalesRegister/SalesRegister.js index 18b3d203..d0d23bef 100644 --- a/reports/SalesRegister/SalesRegister.js +++ b/reports/SalesRegister/SalesRegister.js @@ -1,4 +1,4 @@ -import frappe from 'fyo'; +import { fyo } from 'src/initFyo'; class SalesRegister { async run({ fromDate, toDate, customer }) { @@ -17,7 +17,7 @@ class SalesRegister { filters.date = ['<=', toDate]; } - const invoices = await frappe.db.getAll({ + const invoices = await fyo.db.getAll({ doctype: 'SalesInvoice', fields: ['name', 'date', 'customer', 'account', 'netTotal', 'grandTotal'], filters: filters, @@ -27,7 +27,7 @@ class SalesRegister { const invoiceNames = invoices.map((d) => d.name); - const taxes = await frappe.db.getAll({ + const taxes = await fyo.db.getAll({ doctype: 'TaxSummary', fields: ['parent', 'amount'], filters: { diff --git a/reports/TrialBalance/TrialBalance.js b/reports/TrialBalance/TrialBalance.js index a841a77a..f9f6dce5 100644 --- a/reports/TrialBalance/TrialBalance.js +++ b/reports/TrialBalance/TrialBalance.js @@ -1,4 +1,4 @@ -import { getTrialBalance } from '../FinancialStatements/FinancialStatements'; +import { getTrialBalance } from '../helpers/financialStatements'; export default class TrialBalance { async run({ fromDate, toDate }) { diff --git a/reports/TrialBalance/viewConfig.js b/reports/TrialBalance/viewConfig.js index 3ab25e2e..7502d1bb 100644 --- a/reports/TrialBalance/viewConfig.js +++ b/reports/TrialBalance/viewConfig.js @@ -1,4 +1,5 @@ -import frappe, { t } from 'fyo'; +import { t } from 'fyo'; +import { fyo } from 'src/initFyo'; import getCommonExportActions from '../commonExporter'; const title = t`Trial Balance`; @@ -16,7 +17,7 @@ export default { placeholder: t`From Date`, required: 1, default: async () => { - return (await frappe.getSingle('AccountingSettings')).fiscalYearStart; + return (await fyo.getSingle('AccountingSettings')).fiscalYearStart; }, }, { @@ -27,7 +28,7 @@ export default { label: t`To Date`, required: 1, default: async () => { - return (await frappe.getSingle('AccountingSettings')).fiscalYearEnd; + return (await fyo.getSingle('AccountingSettings')).fiscalYearEnd; }, }, ], diff --git a/reports/commonExporter.ts b/reports/commonExporter.ts new file mode 100644 index 00000000..2a29f18f --- /dev/null +++ b/reports/commonExporter.ts @@ -0,0 +1,161 @@ +import { DocValue, DocValueMap } from 'fyo/core/types'; +import { Verb } from 'fyo/telemetry/types'; +import { Field } from 'schemas/types'; +import { fyo } from 'src/initFyo'; +import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls'; + +interface JSONExport { + columns: { fieldname: string; label: string }[]; + rows: Record[]; + filters: Record; + timestamp: string; + reportName: string; + softwareName: string; + softwareVersion: string; +} + +type GetReportData = () => { + rows: DocValueMap[]; + columns: Field[]; + filters: Record; +}; + +type TemplateObject = { template: string }; + +function templateToInnerText(innerHTML: string): string { + const temp = document.createElement('template'); + temp.innerHTML = innerHTML.trim(); + // @ts-ignore + return temp.content.firstChild!.innerText; +} + +function deObjectify(value: TemplateObject | DocValue) { + if (typeof value !== 'object') return value; + if (value === null) return ''; + + const innerHTML = (value as TemplateObject).template; + if (!innerHTML) return ''; + return templateToInnerText(innerHTML); +} + +function csvFormat(value: TemplateObject | DocValue): string { + if (typeof value === 'string') { + return `"${value}"`; + } else if (value === null) { + return ''; + } else if (typeof value === 'object') { + const innerHTML = (value as TemplateObject).template; + + if (!innerHTML) return ''; + return csvFormat(deObjectify(value as TemplateObject)); + } + + return String(value); +} + +export async function exportCsv( + rows: DocValueMap[], + columns: Field[], + filePath: string +) { + const fieldnames = columns.map(({ fieldname }) => fieldname); + const labels = columns.map(({ label }) => csvFormat(label)); + const csvRows = [ + labels.join(','), + ...rows.map((row) => + fieldnames.map((f) => csvFormat(row[f] as DocValue)).join(',') + ), + ]; + + saveExportData(csvRows.join('\n'), filePath); +} + +async function exportJson( + rows: DocValueMap[], + columns: Field[], + filePath: string, + filters: Record, + reportName: string +) { + const exportObject: JSONExport = { + columns: [], + rows: [], + filters: {}, + timestamp: '', + reportName: '', + softwareName: '', + softwareVersion: '', + }; + const fieldnames = columns.map(({ fieldname }) => fieldname); + + exportObject.columns = columns.map(({ fieldname, label }) => ({ + fieldname, + label, + })); + + exportObject.rows = rows.map((row) => + fieldnames.reduce((acc, f) => { + const value = row[f]; + if (value === undefined) { + acc[f] = ''; + } else { + acc[f] = deObjectify(value as DocValue | TemplateObject); + } + + return acc; + }, {} as Record) + ); + + exportObject.filters = Object.keys(filters) + .filter((name) => filters[name] !== null && filters[name] !== undefined) + .reduce((acc, name) => { + acc[name] = filters[name]; + return acc; + }, {} as Record); + + exportObject.timestamp = new Date().toISOString(); + exportObject.reportName = reportName; + exportObject.softwareName = 'Frappe Books'; + exportObject.softwareVersion = fyo.store.appVersion; + + await saveExportData(JSON.stringify(exportObject), filePath); +} + +async function exportReport( + extention: string, + reportName: string, + getReportData: GetReportData +) { + const { rows, columns, filters } = getReportData(); + + const { filePath, canceled } = await getSavePath(reportName, extention); + if (canceled || !filePath) return; + + switch (extention) { + case 'csv': + await exportCsv(rows, columns, filePath); + break; + case 'json': + await exportJson(rows, columns, filePath, filters, reportName); + break; + default: + return; + } + + fyo.telemetry.log(Verb.Exported, reportName, { extention }); +} + +export default function getCommonExportActions(reportName: string) { + return ['csv', 'json'].map((ext) => ({ + group: fyo.t`Export`, + label: ext.toUpperCase(), + type: 'primary', + action: async (getReportData: GetReportData) => + await exportReport(ext, reportName, getReportData), + })); +} + +export async function saveExportData(data: string, filePath: string) { + await saveData(data, filePath); + showExportInFolder(fyo.t`Export Successful`, filePath); +} diff --git a/reports/index.js b/reports/index.js index 22b16db7..2f6be6fe 100644 --- a/reports/index.js +++ b/reports/index.js @@ -1,3 +1,4 @@ +/* import AccountsReceivablePayable from './AccountsReceivablePayable/AccountsReceivablePayable'; import BalanceSheet from './BalanceSheet/BalanceSheet'; import BankReconciliation from './BankReconciliation/BankReconciliation'; @@ -33,3 +34,7 @@ export function getReportData(method, filters) { const ReportClass = reports[method]; return new ReportClass().run(filters); } +*/ +export function getReportData(method, filters) { + return { rows: [], columns: [] }; +} diff --git a/reports/types.ts b/reports/types.ts new file mode 100644 index 00000000..830c73ef --- /dev/null +++ b/reports/types.ts @@ -0,0 +1,23 @@ +import { AccountRootType } from 'models/baseModels/Account/types'; + +export type ExportExtension = 'csv' | 'json'; + +export interface ReportData { + rows: unknown[]; + columns: unknown[]; +} + +export abstract class Report { + abstract run(filters: Record): ReportData; +} + +export type BalanceType = 'Credit' | 'Debit'; +export type Periodicity = 'Monthly' | 'Quarterly' | 'Half Yearly' | 'Yearly'; +export interface FinancialStatementOptions { + rootType: AccountRootType; + fromDate: string; + toDate: string; + balanceMustBe?: BalanceType; + periodicity?: Periodicity; + accumulateValues?: boolean; +} diff --git a/reports/view.js b/reports/view.js index d73df6e1..dc2a1c3a 100644 --- a/reports/view.js +++ b/reports/view.js @@ -1,19 +1,21 @@ -import BalanceSheetViewConfig from './BalanceSheet/viewConfig'; -import GeneralLedgerViewConfig from './GeneralLedger/viewConfig'; -import GoodsAndServiceTaxGSTR1View from './GoodsAndServiceTax/GSTR1View'; -import GoodsAndServiceTaxGSTR2View from './GoodsAndServiceTax/GSTR2View'; -import ProfitAndLossViewConfig from './ProfitAndLoss/viewConfig'; -import PurchaseRegisterViewConfig from './PurchaseRegister/viewConfig'; -import SalesRegisterViewConfig from './SalesRegister/viewConfig'; -import TrialBalanceViewConfig from './TrialBalance/viewConfig'; +// import BalanceSheetViewConfig from './BalanceSheet/viewConfig'; +// import GeneralLedgerViewConfig from './GeneralLedger/viewConfig'; +// import GoodsAndServiceTaxGSTR1View from './GoodsAndServiceTax/GSTR1View'; +// import GoodsAndServiceTaxGSTR2View from './GoodsAndServiceTax/GSTR2View'; +// import ProfitAndLossViewConfig from './ProfitAndLoss/viewConfig'; +// import PurchaseRegisterViewConfig from './PurchaseRegister/viewConfig'; +// import SalesRegisterViewConfig from './SalesRegister/viewConfig'; +// import TrialBalanceViewConfig from './TrialBalance/viewConfig'; -export default { - 'general-ledger': GeneralLedgerViewConfig, - 'sales-register': SalesRegisterViewConfig, - 'purchase-register': PurchaseRegisterViewConfig, - 'balance-sheet': BalanceSheetViewConfig, - 'profit-and-loss': ProfitAndLossViewConfig, - 'trial-balance': TrialBalanceViewConfig, - 'gstr-1': GoodsAndServiceTaxGSTR1View, - 'gstr-2': GoodsAndServiceTaxGSTR2View, -}; +// export default { +// 'general-ledger': GeneralLedgerViewConfig, +// 'sales-register': SalesRegisterViewConfig, +// 'purchase-register': PurchaseRegisterViewConfig, +// 'balance-sheet': BalanceSheetViewConfig, +// 'profit-and-loss': ProfitAndLossViewConfig, +// 'trial-balance': TrialBalanceViewConfig, +// 'gstr-1': GoodsAndServiceTaxGSTR1View, +// 'gstr-2': GoodsAndServiceTaxGSTR2View, +// }; + +export default {}; diff --git a/src/App.vue b/src/App.vue index b324f3b0..424e9198 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,17 +3,17 @@ id="app" class="h-screen flex flex-col font-sans overflow-hidden antialiased" > - + -
- + --> diff --git a/src/components/FilterDropdown.vue b/src/components/FilterDropdown.vue index fb0a7f40..d9a3e9de 100644 --- a/src/components/FilterDropdown.vue +++ b/src/components/FilterDropdown.vue @@ -116,8 +116,8 @@ diff --git a/src/components/ReconciliationValidation.vue b/src/components/ReconciliationValidation.vue index a07c31e1..b0b5454c 100644 --- a/src/components/ReconciliationValidation.vue +++ b/src/components/ReconciliationValidation.vue @@ -36,6 +36,7 @@ diff --git a/src/components/SalesInvoice/Templates/Base.vue b/src/components/SalesInvoice/Templates/Base.vue index 48d519f4..2558c910 100644 --- a/src/components/SalesInvoice/Templates/Base.vue +++ b/src/components/SalesInvoice/Templates/Base.vue @@ -1,5 +1,5 @@ diff --git a/src/pages/ListView/ListView.vue b/src/pages/ListView/ListView.vue index c7a76353..95d9c581 100644 --- a/src/pages/ListView/ListView.vue +++ b/src/pages/ListView/ListView.vue @@ -30,11 +30,11 @@