From c1482b0d2f182dfc88911c0a827f1b88471292bf Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 11 Apr 2022 11:34:55 +0530 Subject: [PATCH] incr: convert knex calls to dbHandler calls - add callBespoke --- accounting/gst.js | 14 +++--- backend/database/bespoke.ts | 67 +++++++++++++++++++++++++ backend/database/manager.ts | 17 ++++++- backend/database/types.ts | 3 ++ frappe/core/dbHandler.ts | 68 ++++++++++++++++++++++++-- main/registerIpcMainActionListeners.ts | 14 ++++++ models/doctype/Item/Item.js | 8 +-- models/doctype/Party/PartyServer.js | 12 ++--- models/doctype/Tax/RegionalEntries.js | 3 +- reports/Cashflow/Cashflow.js | 26 ++-------- src/demux/db.ts | 16 ++++++ src/pages/Dashboard/Expenses.vue | 25 ++-------- src/pages/Dashboard/UnpaidInvoices.vue | 16 ++---- src/pages/GetStarted.vue | 30 ++++-------- src/pages/SetupWizard/setupCompany.js | 14 ++++-- src/utils.js | 22 ++++++--- utils/db/types.ts | 2 + utils/messages.ts | 1 + 18 files changed, 252 insertions(+), 106 deletions(-) create mode 100644 backend/database/bespoke.ts diff --git a/accounting/gst.js b/accounting/gst.js index 718d71c6..2bb19bbf 100644 --- a/accounting/gst.js +++ b/accounting/gst.js @@ -145,9 +145,10 @@ async function generateB2bData(rows) { itms: [], }; - let items = await frappe.db - .knex('SalesInvoiceItem') - .where('parent', invRecord.inum); + const items = await frappe.db.getAllRaw('SalesInvoiceItem', { + fields: ['*'], + filters: { parent: invRecord.inum }, + }); items.forEach((item) => { const itemRecord = { @@ -205,9 +206,10 @@ async function generateB2clData(invoices) { itms: [], }; - let items = await frappe.db - .knex('SalesInvoiceItem') - .where('parent', invRecord.inum); + const items = await frappe.db.getAllRaw('SalesInvoiceItem', { + fields: ['*'], + filters: { parent: invRecord.inum }, + }); items.forEach((item) => { const itemRecord = { diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts new file mode 100644 index 00000000..f6b18181 --- /dev/null +++ b/backend/database/bespoke.ts @@ -0,0 +1,67 @@ +import DatabaseCore from './core'; +import { BespokeFunction } from './types'; + +export class BespokeQueries { + [key: string]: BespokeFunction; + + static async getTopExpenses( + db: DatabaseCore, + fromDate: string, + toDate: string + ) { + const expenseAccounts = db + .knex!.select('name') + .from('Account') + .where('rootType', 'Expense'); + + const topExpenses = await db + .knex!.select({ + total: db.knex!.raw('sum(cast(?? as real)) - sum(cast(?? as real))', [ + 'debit', + 'credit', + ]), + }) + .select('account') + .from('AccountingLedgerEntry') + .where('account', 'in', expenseAccounts) + .whereBetween('date', [fromDate, toDate]) + .groupBy('account') + .orderBy('total', 'desc') + .limit(5); + return topExpenses; + } + + static async getTotalOutstanding( + db: DatabaseCore, + schemaName: string, + fromDate: string, + toDate: string + ) { + return await db.knex!(schemaName) + .sum({ total: 'baseGrandTotal' }) + .sum({ outstanding: 'outstandingAmount' }) + .where('submitted', true) + .whereBetween('date', [fromDate, toDate]) + .first(); + } + + static async getCashflow(db: DatabaseCore, fromDate: string, toDate: string) { + const cashAndBankAccounts = db.knex!('Account') + .select('name') + .where('accountType', 'in', ['Cash', 'Bank']) + .andWhere('isGroup', false); + const dateAsMonthYear = db.knex!.raw('strftime("%m-%Y", ??)', 'date'); + return await db.knex!('AccountingLedgerEntry') + .where('reverted', false) + .sum({ + inflow: 'debit', + outflow: 'credit', + }) + .select({ + 'month-year': dateAsMonthYear, + }) + .where('account', 'in', cashAndBankAccounts) + .whereBetween('date', [fromDate, toDate]) + .groupBy(dateAsMonthYear); + } +} diff --git a/backend/database/manager.ts b/backend/database/manager.ts index 53d52e32..edf9d5c3 100644 --- a/backend/database/manager.ts +++ b/backend/database/manager.ts @@ -3,9 +3,10 @@ import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types'; import { getSchemas } from '../../schemas'; import { databaseMethodSet } from '../helpers'; import patches from '../patches'; +import { BespokeQueries } from './bespoke'; import DatabaseCore from './core'; import { runPatches } from './runPatch'; -import { Patch } from './types'; +import { BespokeFunction, Patch } from './types'; export class DatabaseManager extends DatabaseDemuxBase { db?: DatabaseCore; @@ -52,6 +53,20 @@ export class DatabaseManager extends DatabaseDemuxBase { return response; } + async callBespoke(method: string, ...args: unknown[]): Promise { + if (!this.#isInitialized) { + return; + } + + if (!BespokeQueries.hasOwnProperty(method)) { + return; + } + + // @ts-ignore + const queryFunction: BespokeFunction = BespokeQueries[method]; + return await queryFunction(this.db!, ...args); + } + async #migrate(): Promise { if (!this.#isInitialized) { return; diff --git a/backend/database/types.ts b/backend/database/types.ts index ff521c53..6dcf0ab8 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -1,4 +1,5 @@ import { Field, RawValue } from '../../schemas/types'; +import DatabaseCore from './core'; import { DatabaseManager } from './manager'; export interface GetQueryBuilderOptions { @@ -43,3 +44,5 @@ export interface SqliteTableInfo { notnull: number; // 0 | 1 dflt_value: string | null; } + +export type BespokeFunction = (db:DatabaseCore, ...args: unknown[]) => Promise \ No newline at end of file diff --git a/frappe/core/dbHandler.ts b/frappe/core/dbHandler.ts index cfd115af..c8c45f6d 100644 --- a/frappe/core/dbHandler.ts +++ b/frappe/core/dbHandler.ts @@ -11,6 +11,10 @@ import { SingleValue, } from './types'; +type TopExpenses = { account: string; total: number }[]; +type TotalOutstanding = { total: number; outstanding: number }; +type Cashflow = { inflow: number; outflow: number; 'month-year': string }[]; + export class DatabaseHandler extends DatabaseBase { #frappe: Frappe; #demux: DatabaseDemuxBase; @@ -74,13 +78,34 @@ export class DatabaseHandler extends DatabaseBase { schemaName: string, options: GetAllOptions = {} ): Promise { - const rawValueMap = (await this.#demux.call( + const rawValueMap = await this.#getAll(schemaName, options); + return this.#toDocValueMap(schemaName, rawValueMap) as DocValueMap[]; + } + + async getAllRaw( + schemaName: string, + options: GetAllOptions = {} + ): Promise { + return await this.#getAll(schemaName, options); + } + + async count( + schemaName: string, + options: GetAllOptions = {} + ): Promise { + const rawValueMap = await this.#getAll(schemaName, options); + return rawValueMap.length; + } + + async #getAll( + schemaName: string, + options: GetAllOptions = {} + ): Promise { + return (await this.#demux.call( 'getAll', schemaName, options )) as RawValueMap[]; - - return this.#toDocValueMap(schemaName, rawValueMap) as DocValueMap[]; } async getSingleValues( @@ -123,6 +148,43 @@ export class DatabaseHandler extends DatabaseBase { return (await this.#demux.call('exists', schemaName, name)) as boolean; } + /** + * Bespoke function + * + * These are functions to run custom queries that are too complex for + * DatabaseCore and require use of knex or raw queries. + * + * The query logic for these is in backend/database/bespoke.ts + */ + async getTopExpenses(fromDate: string, toDate: string): Promise { + return (await this.#demux.callBespoke( + 'getTopExpenses', + fromDate, + toDate + )) as TopExpenses; + } + + async getTotalOutstanding( + schemaName: string, + fromDate: string, + toDate: string + ): Promise { + return (await this.#demux.callBespoke( + 'getTotalOutstanding', + schemaName, + fromDate, + toDate + )) as TotalOutstanding; + } + + async getCashflow(fromDate: string, toDate: string): Promise { + return (await this.#demux.callBespoke( + 'getCashflow', + fromDate, + toDate + )) as Cashflow; + } + #toDocValueMap( schemaName: string, rawValueMap: RawValueMap | RawValueMap[] diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts index 03b62b96..0ff30367 100644 --- a/main/registerIpcMainActionListeners.ts +++ b/main/registerIpcMainActionListeners.ts @@ -172,6 +172,20 @@ export default function registerIpcMainActionListeners(main: Main) { } ); + ipcMain.handle( + IPC_ACTIONS.DB_BESPOKE, + async (_, method: string, ...args: unknown[]) => { + const response: DatabaseResponse = { error: '', data: undefined }; + try { + response.data = await databaseManager.callBespoke(method, ...args); + } catch (error) { + response.error = error.toString(); + } + + return response; + } + ); + ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async (_) => { const response: DatabaseResponse = { error: '', data: undefined }; response.data = await databaseManager.getSchemaMap(); diff --git a/models/doctype/Item/Item.js b/models/doctype/Item/Item.js index 0ef9fdf6..7833e2cf 100644 --- a/models/doctype/Item/Item.js +++ b/models/doctype/Item/Item.js @@ -97,9 +97,11 @@ export default { }, formulaDependsOn: ['itemType'], async formula() { - const cogs = await frappe.db - .knex('Account') - .where({ accountType: 'Cost of Goods Sold' }); + const cogs = await frappe.db.getAllRaw('Account', { + filters: { + accountType: 'Cost of Goods Sold', + }, + }); if (cogs.length === 0) { return ''; } else { diff --git a/models/doctype/Party/PartyServer.js b/models/doctype/Party/PartyServer.js index 56868140..02b54a24 100644 --- a/models/doctype/Party/PartyServer.js +++ b/models/doctype/Party/PartyServer.js @@ -19,13 +19,13 @@ export default class PartyServer extends Document { async updateOutstandingAmount() { let isCustomer = this.customer; let doctype = isCustomer ? 'SalesInvoice' : 'PurchaseInvoice'; - let partyField = isCustomer ? 'customer' : 'supplier'; - const outstandingAmounts = await frappe.db.knex - .select('outstandingAmount') - .from(doctype) - .where('submitted', 1) - .andWhere(partyField, this.name); + const outstandingAmounts = ( + await frappe.db.getAllRaw(doctype, { + fields: ['outstandingAmount', 'party'], + filters: { submitted: true }, + }) + ).filter(({ party }) => party === this.name); const totalOutstanding = outstandingAmounts .map(({ outstandingAmount }) => frappe.pesa(outstandingAmount)) diff --git a/models/doctype/Tax/RegionalEntries.js b/models/doctype/Tax/RegionalEntries.js index 9e0be5cf..7b33cb8d 100644 --- a/models/doctype/Tax/RegionalEntries.js +++ b/models/doctype/Tax/RegionalEntries.js @@ -15,8 +15,7 @@ export default async function generateTaxes(country) { const name = `${type}-${percent}`; // Not cross checking cause hardcoded values. - await frappe.db.knex('Tax').where({ name }).del(); - await frappe.db.knex('TaxDetail').where({ parent: name }).del(); + await frappe.db.delete('Tax', name); const details = getTaxDetails(type, percent); await newTax.set({ name, details }); diff --git a/reports/Cashflow/Cashflow.js b/reports/Cashflow/Cashflow.js index 10b3f294..3c0bd08f 100644 --- a/reports/Cashflow/Cashflow.js +++ b/reports/Cashflow/Cashflow.js @@ -1,29 +1,13 @@ import frappe from 'frappe'; import { DateTime } from 'luxon'; -import { getPeriodList, getFiscalYear } from '../FinancialStatements/FinancialStatements'; +import { + getFiscalYear, + getPeriodList, +} from '../FinancialStatements/FinancialStatements'; class Cashflow { async run({ fromDate, toDate, periodicity }) { - let cashAndBankAccounts = frappe.db - .knex('Account') - .select('name') - .where('accountType', 'in', ['Cash', 'Bank']) - .andWhere('isGroup', 0); - let dateAsMonthYear = frappe.db.knex.raw('strftime("%m-%Y", ??)', 'date'); - let res = await frappe.db - .knex('AccountingLedgerEntry') - .where('reverted', 0) - .sum({ - inflow: 'debit', - outflow: 'credit', - }) - .select({ - 'month-year': dateAsMonthYear, - }) - .where('account', 'in', cashAndBankAccounts) - .whereBetween('date', [fromDate, toDate]) - .groupBy(dateAsMonthYear); - + const res = await frappe.db.getCashflow(fromDate, toDate); let fiscalYear = await getFiscalYear(); let periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear); diff --git a/src/demux/db.ts b/src/demux/db.ts index 41abe1b8..9cdc8885 100644 --- a/src/demux/db.ts +++ b/src/demux/db.ts @@ -78,4 +78,20 @@ export class DatabaseDemux extends DatabaseDemuxBase { return response.data; } + + async callBespoke(method: string, ...args: unknown[]): Promise { + let response: DatabaseResponse; + if (this.#isElectron) { + response = await ipcRenderer.invoke(IPC_ACTIONS.DB_BESPOKE, method, ...args); + } else { + // TODO: API Call + response = { error: '', data: undefined }; + } + + if (response.error) { + throw new Error(response.error); + } + + return response.data; + } } diff --git a/src/pages/Dashboard/Expenses.vue b/src/pages/Dashboard/Expenses.vue index 3678051b..a71533ab 100644 --- a/src/pages/Dashboard/Expenses.vue +++ b/src/pages/Dashboard/Expenses.vue @@ -21,7 +21,9 @@
{{ d.account }}
-

{{ frappe.format(d.total, 'Currency') }}

+

+ {{ frappe.format(d.total, 'Currency') }} +

0) { toUpdate.itemCreated = 1; } } if (!frappe.GetStarted.invoiceCreated) { - let { count } = ( - await frappe.db.knex('SalesInvoice').count('name as count') - )[0]; + const count = await frappe.db.count('SalesInvoice'); if (count > 0) { toUpdate.invoiceCreated = 1; } } if (!frappe.GetStarted.customerCreated) { - let { count } = ( - await frappe.db - .knex('Party') - .where('customer', 1) - .count('name as count') - )[0]; + const count = frappe.db.count('Party', { + filters: { role: 'Customer' }, + }); if (count > 0) { toUpdate.customerCreated = 1; } } if (!frappe.GetStarted.billCreated) { - let { count } = ( - await frappe.db.knex('PurchaseInvoice').count('name as count') - )[0]; + const count = await frappe.db.count('SalesInvoice'); if (count > 0) { toUpdate.billCreated = 1; } } if (!frappe.GetStarted.supplierCreated) { - let { count } = ( - await frappe.db - .knex('Party') - .where('supplier', 1) - .count('name as count') - )[0]; + const count = frappe.db.count('Party', { + filters: { role: 'Supplier' }, + }); if (count > 0) { toUpdate.supplierCreated = 1; } diff --git a/src/pages/SetupWizard/setupCompany.js b/src/pages/SetupWizard/setupCompany.js index 75d235e6..136615b2 100644 --- a/src/pages/SetupWizard/setupCompany.js +++ b/src/pages/SetupWizard/setupCompany.js @@ -136,7 +136,10 @@ export async function checkIfExactRecordAbsent(docObject) { const { doctype, name } = docObject; const newDocObject = Object.assign({}, docObject); delete newDocObject.doctype; - const rows = await frappe.db.knex(doctype).where({ name }); + const rows = await frappe.db.getAllRaw(doctype, { + fields: ['*'], + filters: { name }, + }); if (rows.length === 0) { return true; @@ -150,7 +153,7 @@ export async function checkIfExactRecordAbsent(docObject) { }); if (!matchList.every(Boolean)) { - await frappe.db.knex(doctype).where({ name }).del(); + await frappe.db.delete(doctype, name); return true; } @@ -168,9 +171,10 @@ async function checkAndCreateDoc(docObject) { } async function getBankAccountParentName(country) { - const parentBankAccount = await frappe.db - .knex('Account') - .where({ isGroup: true, accountType: 'Bank' }); + const parentBankAccount = await frappe.db.getAllRaw('Account', { + fields: ['*'], + filters: { isGroup: true, accountType: 'Bank' }, + }); if (parentBankAccount.length === 0) { // This should not happen if the fixtures are correct. diff --git a/src/utils.js b/src/utils.js index 28c3648f..3f8377c8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -65,12 +65,22 @@ export function deleteDocWithPrompt(doc) { export async function cancelDocWithPrompt(doc) { let description = t`This action is permanent`; if (['SalesInvoice', 'PurchaseInvoice'].includes(doc.doctype)) { - const query = await frappe.db - .knex('PaymentFor') - .join('Payment', 'PaymentFor.parent', 'Payment.name') - .select('parent') - .where('cancelled', 0) - .where('referenceName', doc.name); + const payments = ( + await frappe.db.getAll('Payment', { + fields: ['name'], + filters: { cancelled: false }, + }) + ).map(({ name }) => name); + + const query = ( + await frappe.db.getAll('PaymentFor', { + fields: ['parent'], + filters: { + referenceName: doc.name, + }, + }) + ).filter(({ parent }) => payments.includes(parent)); + const paymentList = [...new Set(query.map(({ parent }) => parent))]; if (paymentList.length === 1) { diff --git a/utils/db/types.ts b/utils/db/types.ts index 56403d34..4937aade 100644 --- a/utils/db/types.ts +++ b/utils/db/types.ts @@ -80,4 +80,6 @@ export abstract class DatabaseDemuxBase { abstract connectToDatabase(dbPath: string, countryCode?: string): Promise abstract call(method: DatabaseMethod, ...args: unknown[]): Promise + + abstract callBespoke(method: string, ...args: unknown[]): Promise } diff --git a/utils/messages.ts b/utils/messages.ts index d0fbdb55..8ce2e316 100644 --- a/utils/messages.ts +++ b/utils/messages.ts @@ -32,6 +32,7 @@ export enum IPC_ACTIONS { DB_CREATE = 'db-create', DB_CONNECT = 'db-connect', DB_CALL = 'db-call', + DB_BESPOKE = 'db-bespoke', DB_SCHEMA = 'db-schema', }