From 82e012901120850225e43a073424d521fa7daa1d Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 16 Apr 2018 18:33:54 +0530 Subject: [PATCH 1/5] Refactor electron client code --- client/index.js | 4 +- electron/client.js | 130 ++++++++++++++++++++----------------------- electron/settings.js | 25 +++++++++ server/index.js | 6 +- 4 files changed, 92 insertions(+), 73 deletions(-) create mode 100644 electron/settings.js diff --git a/client/index.js b/client/index.js index c28218fd..4f7f486a 100644 --- a/client/index.js +++ b/client/index.js @@ -6,9 +6,6 @@ module.exports = { // require modules frappe.registerModels(require('../models'), 'client'); - frappe.registerView('List', 'ToDo', require('frappejs/models/doctype/ToDo/ToDoList.js')); - frappe.registerView('Form', 'FilterSelector', require('frappejs/models/doctype/FilterSelector/FilterSelectorForm.js')); - frappe.registerView('List', 'Customer', require('../models/doctype/Party/CustomerList.js')); frappe.router.add('report/general-ledger', async (params) => { @@ -26,6 +23,7 @@ module.exports = { frappe.desk.menu.addItem('Address', "#list/Address"); frappe.desk.menu.addItem('Contact', "#list/Contact"); frappe.desk.menu.addItem('Settings', () => frappe.desk.showFormModal('SystemSettings')); + frappe.desk.menu.addItem('General Ledger', '#report/general-ledger'); frappe.router.default = '#tree/Account'; diff --git a/electron/client.js b/electron/client.js index 927fd9e5..8134f172 100644 --- a/electron/client.js +++ b/electron/client.js @@ -1,10 +1,11 @@ const frappe = require('frappejs'); const path = require('path'); const electron = require('frappejs/client/electron'); -const { writeFile } = require('frappejs/server/utils'); const appClient = require('../client'); const SetupWizard = require('../setup'); const { getPDFForElectron } = require('frappejs/server/pdf'); +const { getSettings, saveSettings } = require('./settings'); +const { postStart } = require('../server'); const fs = require('fs'); @@ -13,79 +14,70 @@ require.extensions['.html'] = function (module, filename) { }; (async () => { - const configFilePath = path.join(require('os').homedir(), '.config', 'frappe-accounting', 'settings.json'); + let electronSettings = getSettings(); + let firstRun = false, setupWizardValues = null; - let settings, dbPath; - try { - settings = require(configFilePath); - } catch(e) { - settings = {} + if (!electronSettings.dbPath) { + const values = await runSetupWizard(); + const dbPath = path.join(values.file[0].path, frappe.slug(values.companyName) + '.db'); + const config = { + directory: path.dirname(dbPath), + dbPath: dbPath + }; + await saveSettings(config); + + firstRun = true; + electronSettings = config; + setupWizardValues = values; } - frappe.electronConfig = settings; + await electron.start({ + dbPath: electronSettings.dbPath, + models: require('../models') + }); + + // await postStart(); + + if (firstRun) { + await saveSetupWizardValues(values); + await bootstrapChartOfAccounts(); + } frappe.getPDF = getPDFForElectron; + frappe.electronSettings = electronSettings; - if (settings.dbPath) { - dbPath = settings.dbPath; - electron.start({ - dbPath, - models: require('../models') - }).then(() => { - - frappe.syncDoc(require('../fixtures/invoicePrint')); - appClient.start(); - }); - } else { - const setup = new SetupWizard(); - window.setup = setup; - const values = await setup.start(); - const { - companyName, - file, - country, - name, - email, - abbreviation, - bankName - } = values; - - dbPath = path.join(file[0].path, companyName + '.db'); - - electron.start({ - dbPath, - models: require('../models') - }).then(async () => { - const config = { - directory: path.dirname(dbPath), - dbPath: dbPath - }; - - await writeFile(configFilePath, JSON.stringify(config)); - - frappe.electronConfig = config; - - const doc = await frappe.getDoc('AccountingSettings'); - - await doc.set('companyName', companyName); - await doc.set('country', country); - await doc.set('fullname', name); - await doc.set('email', email); - await doc.set('bankName', bankName); - - await doc.update(); - - // bootstrap Chart of Accounts - const importCOA = require('../models/doctype/account/importCOA'); - const chart = require('../fixtures/standardCOA'); - await importCOA(chart); - - - frappe.syncDoc(require('../fixtures/invoicePrint')); - appClient.start(); - }) - } - - + appClient.start(); })(); +async function runSetupWizard() { + const setup = new SetupWizard(); + const values = await setup.start(); + return values; +} + +async function saveSetupWizardValues(values) { + const { + companyName, + country, + name, + email, + abbreviation, + bankName + } = values; + + const doc = await frappe.getDoc('AccountingSettings'); + + await doc.set('companyName', companyName); + await doc.set('country', country); + await doc.set('fullname', name); + await doc.set('email', email); + await doc.set('bankName', bankName); + + await doc.update(); +} + +async function bootstrapChartOfAccounts() { + const importCOA = require('../models/doctype/account/importCOA'); + const chart = require('../fixtures/standardCOA'); + await importCOA(chart); +} diff --git a/electron/settings.js b/electron/settings.js new file mode 100644 index 00000000..91e82f70 --- /dev/null +++ b/electron/settings.js @@ -0,0 +1,25 @@ +const os = require('os'); +const { writeFile } = require('frappejs/server/utils'); + +const homedir = os.homedir(); +const configFilePath = path.join(homedir, '.config', 'frappe-accounting', 'settings.json'); + +function getSettings() { + let settings; + try { + settings = require(configFilePath); + } catch (e) { + settings = {} + } + + return settings; +} + +async function saveSettings(settings) { + await writeFile(configFilePath, JSON.stringify(settings)); +} + +module.exports = { + getSettings, + saveSettings +} diff --git a/server/index.js b/server/index.js index ee5c7516..267e9391 100644 --- a/server/index.js +++ b/server/index.js @@ -7,11 +7,15 @@ module.exports = { async start() { await server.start({ backend: 'sqlite', - connectionParams: {dbPath: 'test.db'}, + connectionParams: { dbPath: 'test.db' }, staticPath: './www', models: require('../models') }) + await this.postStart(); + }, + + async postStart() { // set server-side modules frappe.models.Invoice.documentClass = require('../models/doctype/Invoice/InvoiceServer.js'); frappe.models.Payment.documentClass = require('../models/doctype/Payment/PaymentServer.js'); From 8a67e6bf9ac01ba3d74da849330822f06f1e53c3 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 18 Apr 2018 12:17:00 +0530 Subject: [PATCH 2/5] Financial Statements init --- accounting/utils.js | 6 +-- electron/client.js | 7 ++-- electron/index.html | 2 +- electron/index.js | 5 +++ electron/settings.js | 1 + main.js | 2 +- models/doctype/Account/Account.js | 1 + .../AccountingLedgerEntry.js | 2 +- models/doctype/Invoice/InvoiceServer.js | 2 +- models/doctype/Payment/PaymentServer.js | 2 +- package.json | 2 +- reports/financialStatements.js | 41 +++++++++++++++++++ server/index.js | 4 ++ 13 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 electron/index.js create mode 100644 reports/financialStatements.js diff --git a/accounting/utils.js b/accounting/utils.js index 162bcb02..f14f7c3d 100644 --- a/accounting/utils.js +++ b/accounting/utils.js @@ -6,10 +6,8 @@ module.exports = { return { route: ['report', 'general-ledger'], params: { - filters: { - referenceType: form.doc.doctype, - referenceName: form.doc.name - } + referenceType: form.doc.doctype, + referenceName: form.doc.name } }; } diff --git a/electron/client.js b/electron/client.js index 8134f172..301f684b 100644 --- a/electron/client.js +++ b/electron/client.js @@ -6,6 +6,7 @@ const SetupWizard = require('../setup'); const { getPDFForElectron } = require('frappejs/server/pdf'); const { getSettings, saveSettings } = require('./settings'); const { postStart } = require('../server'); +const { slug } = require('frappejs/utils'); const fs = require('fs'); @@ -19,7 +20,7 @@ require.extensions['.html'] = function (module, filename) { if (!electronSettings.dbPath) { const values = await runSetupWizard(); - const dbPath = path.join(values.file[0].path, frappe.slug(values.companyName) + '.db'); + const dbPath = path.join(values.file[0].path, slug(values.companyName) + '.db'); const config = { directory: path.dirname(dbPath), dbPath: dbPath @@ -36,10 +37,10 @@ require.extensions['.html'] = function (module, filename) { models: require('../models') }); - // await postStart(); + await postStart(); if (firstRun) { - await saveSetupWizardValues(values); + await saveSetupWizardValues(setupWizardValues); await bootstrapChartOfAccounts(); } diff --git a/electron/index.html b/electron/index.html index a3e9514c..4c81b30d 100644 --- a/electron/index.html +++ b/electron/index.html @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/electron/index.js b/electron/index.js new file mode 100644 index 00000000..e1426ee3 --- /dev/null +++ b/electron/index.js @@ -0,0 +1,5 @@ +global.rootRequire = function(name) { + return require(process.cwd() + '/' + name); +} + +require('./client'); diff --git a/electron/settings.js b/electron/settings.js index 91e82f70..4dcf0fb2 100644 --- a/electron/settings.js +++ b/electron/settings.js @@ -1,4 +1,5 @@ const os = require('os'); +const path = require('path'); const { writeFile } = require('frappejs/server/utils'); const homedir = os.homedir(); diff --git a/main.js b/main.js index 34531b43..88df761d 100644 --- a/main.js +++ b/main.js @@ -17,7 +17,7 @@ function createWindow () { // and load the index.html of the app. mainWindow.loadURL(url.format({ - pathname: path.join(__dirname, 'electron/index.html'), + pathname: path.join(__dirname, 'electron', 'index.html'), protocol: 'file:', slashes: true })) diff --git a/models/doctype/Account/Account.js b/models/doctype/Account/Account.js index 5bd6ef11..908f6e4f 100644 --- a/models/doctype/Account/Account.js +++ b/models/doctype/Account/Account.js @@ -3,6 +3,7 @@ module.exports = { "doctype": "DocType", "documentClass": require("./AccountDocument.js"), "isSingle": 0, + "isTree": 1, "keywordFields": [ "name", "rootType", diff --git a/models/doctype/AccountingLedgerEntry/AccountingLedgerEntry.js b/models/doctype/AccountingLedgerEntry/AccountingLedgerEntry.js index 5a108cdf..ff2e18f5 100644 --- a/models/doctype/AccountingLedgerEntry/AccountingLedgerEntry.js +++ b/models/doctype/AccountingLedgerEntry/AccountingLedgerEntry.js @@ -49,7 +49,7 @@ module.exports = { fieldname: "againstAccount", label: "Against Account", fieldtype: "Text", - required: 1 + required: 0 }, { fieldname: "referenceType", diff --git a/models/doctype/Invoice/InvoiceServer.js b/models/doctype/Invoice/InvoiceServer.js index 8c8484e6..70ef9e1a 100644 --- a/models/doctype/Invoice/InvoiceServer.js +++ b/models/doctype/Invoice/InvoiceServer.js @@ -1,6 +1,6 @@ const Invoice = require('./InvoiceDocument'); const frappe = require('frappejs'); -const LedgerPosting = require.main.require('./accounting/ledgerPosting'); +const LedgerPosting = rootRequire('accounting/ledgerPosting'); module.exports = class InvoiceServer extends Invoice { getPosting() { diff --git a/models/doctype/Payment/PaymentServer.js b/models/doctype/Payment/PaymentServer.js index e09ef24b..ef6f2e9f 100644 --- a/models/doctype/Payment/PaymentServer.js +++ b/models/doctype/Payment/PaymentServer.js @@ -1,6 +1,6 @@ const BaseDocument = require('frappejs/model/document'); const frappe = require('frappejs'); -const LedgerPosting = require.main.require('./accounting/ledgerPosting'); +const LedgerPosting = rootRequire('accounting/ledgerPosting'); module.exports = class PaymentServer extends BaseDocument { getPosting() { diff --git a/package.json b/package.json index 36b1c272..a7161b3b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "mocha tests", "start": "nodemon server.js", "watch": "rollup -c --watch", - "electron": "electron main.js", + "electron": "EIO_WS_ENGINE=ws electron main.js", "electron-pack": "electron-packager . --overwrite", "postinstall": "electron-builder install-app-deps" }, diff --git a/reports/financialStatements.js b/reports/financialStatements.js new file mode 100644 index 00000000..54aafff7 --- /dev/null +++ b/reports/financialStatements.js @@ -0,0 +1,41 @@ +const frappe = require('frappejs'); + +async function getData(rootType) { + const accounts = await getAccounts(rootType); + if (!accounts || accounts.length === 0) return; + + const ledgerEntries = await frappe.db.getAll({ + doctype: 'AccountingLedgerEntry', + fields: ['account', 'debit', 'credit'], + filters: { + account: ['in', accounts] + } + }); + + let data = {}; + + for (let entry of ledgerEntries) { + if (!data[entry.account]) { + data[entry.account] = 0.0; + } + + data[entry.account] += entry.debit - entry.credit; + } + + return data; +} + +async function getAccounts(rootType) { + return (await frappe.db.getAll({ + doctype: 'Account', + fields: ['name'], + filters: { + rootType + } + })) + .map(d => d.name); +} + +module.exports = { + getData +} \ No newline at end of file diff --git a/server/index.js b/server/index.js index 267e9391..006124ad 100644 --- a/server/index.js +++ b/server/index.js @@ -1,3 +1,7 @@ +global.rootRequire = function(name) { + return require(process.cwd() + '/' + name); +} + const server = require('frappejs/server'); const frappe = require('frappejs'); const GeneralLedger = require('../reports/generalLedger/GeneralLedger'); From 1c1f72281eef48d190e6700d479c84faa47fd775 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 18 Apr 2018 14:32:05 +0530 Subject: [PATCH 3/5] wip --- models/doctype/JournalEntry/JournalEntry.js | 2 +- .../JournalEntry/JournalEntryServer.js | 10 +++---- models/doctype/Quotation/Quotation.js | 10 ++++--- .../QuotationSettings/QuotationSettings.js | 15 ++++++---- reports/ProfitAndLoss/ProfitAndLoss.js | 30 +++++++++++++++++++ reports/financialStatements.js | 6 ++-- reports/generalLedger/GeneralLedger.js | 10 +++---- server/index.js | 2 +- 8 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 reports/ProfitAndLoss/ProfitAndLoss.js diff --git a/models/doctype/JournalEntry/JournalEntry.js b/models/doctype/JournalEntry/JournalEntry.js index a6ff5931..25d9eb9e 100644 --- a/models/doctype/JournalEntry/JournalEntry.js +++ b/models/doctype/JournalEntry/JournalEntry.js @@ -70,7 +70,7 @@ module.exports = { // section 2 { fields: ["accounts"]}, // section 3 - { + { columns: [ { fields: [ "referenceNumber"] }, { fields: [ "referenceDate"] } diff --git a/models/doctype/JournalEntry/JournalEntryServer.js b/models/doctype/JournalEntry/JournalEntryServer.js index 8680dfdd..d9d6c468 100644 --- a/models/doctype/JournalEntry/JournalEntryServer.js +++ b/models/doctype/JournalEntry/JournalEntryServer.js @@ -1,10 +1,10 @@ -const BaseDocument = require('frappejs/model/document'); +const JournalEntry = require('./JournalEntry'); const frappe = require('frappejs'); -const LedgerPosting = require.main.require('./accounting/ledgerPosting'); +const LedgerPosting = rootRequire('accounting/ledgerPosting'); -module.exports = class PaymentServer extends BaseDocument { +module.exports = class JournalEntryServer extends BaseDocument { /** - + getPosting() { let entries = new LedgerPosting({reference: this, party: this.party}); entries.debit(this.paymentAccount, this.amount); @@ -24,6 +24,6 @@ module.exports = class PaymentServer extends BaseDocument { async afterRevert() { await this.getPosting().postReverse(); } - + **/ } \ No newline at end of file diff --git a/models/doctype/Quotation/Quotation.js b/models/doctype/Quotation/Quotation.js index 5ddb0a85..572fc295 100644 --- a/models/doctype/Quotation/Quotation.js +++ b/models/doctype/Quotation/Quotation.js @@ -1,8 +1,10 @@ +const deepmerge = require('deepmerge'); const Invoice = require('../Invoice/Invoice'); -const Quotation = Invoice; -Quotation.name = "Quotation"; -Quotation.label = "Quotation"; -Quotation.settings = "QuotationSettings"; +const Quotation = deepmerge(Invoice, { + name: "Quotation", + label: "Quotation", + settings: "QuotationSettings" +}); module.exports = Quotation; diff --git a/models/doctype/QuotationSettings/QuotationSettings.js b/models/doctype/QuotationSettings/QuotationSettings.js index fd423aa3..dbc26797 100644 --- a/models/doctype/QuotationSettings/QuotationSettings.js +++ b/models/doctype/QuotationSettings/QuotationSettings.js @@ -1,8 +1,11 @@ +const deepmerge = require('deepmerge'); const InvoiceSettings = require('../InvoiceSettings/InvoiceSettings'); -const QuotationSettings = InvoiceSettings; -QuotationSettings.name = "QuotationSettings"; -QuotationSettings.label = "Quotation Settings"; -QuotationSettings.fields.find((field)=>{ - if (field.fieldname == "numberSeries") field.default = "QTN"; -}); +const QuotationSettings = deepmerge(InvoiceSettings, { + "name": "QuotationSettings", + "label": "Quotation Settings", + "fields": { + "default": "INV" + } +}) + module.exports = QuotationSettings; \ No newline at end of file diff --git a/reports/ProfitAndLoss/ProfitAndLoss.js b/reports/ProfitAndLoss/ProfitAndLoss.js new file mode 100644 index 00000000..58505c76 --- /dev/null +++ b/reports/ProfitAndLoss/ProfitAndLoss.js @@ -0,0 +1,30 @@ +const frappe = require('frappejs'); +const { getData } = require('../financialStatements'); + +class ProfitAndLoss { + async run(params) { + const filters = {}; + if (params.account) filters.account = params.account; + if (params.party) filters.party = params.party; + if (params.referenceType) filters.referenceType = params.referenceType; + if (params.referenceName) filters.referenceName = params.referenceName; + if (params.fromDate) filters.date = ['>=', params.fromDate]; + if (params.toDate) filters.date = ['<=', params.toDate]; + + let income = await getData({ + rootType: 'Income', + balanceMustBe: 'Credit' + }); + + let expense = await getData({ + rootType: 'Expense', + balanceMustBe: 'Credit' + }); + + return data; + } +} + +module.exports = function execute(params) { + return new ProfitAndLoss().run(params); +} diff --git a/reports/financialStatements.js b/reports/financialStatements.js index 54aafff7..439ccdb1 100644 --- a/reports/financialStatements.js +++ b/reports/financialStatements.js @@ -1,8 +1,8 @@ const frappe = require('frappejs'); -async function getData(rootType) { +async function getData({ rootType, balanceMustBe }) { const accounts = await getAccounts(rootType); - if (!accounts || accounts.length === 0) return; + if (!accounts || accounts.length === 0) return []; const ledgerEntries = await frappe.db.getAll({ doctype: 'AccountingLedgerEntry', @@ -38,4 +38,4 @@ async function getAccounts(rootType) { module.exports = { getData -} \ No newline at end of file +} diff --git a/reports/generalLedger/GeneralLedger.js b/reports/generalLedger/GeneralLedger.js index e5f49760..81ef77cd 100644 --- a/reports/generalLedger/GeneralLedger.js +++ b/reports/generalLedger/GeneralLedger.js @@ -1,9 +1,5 @@ const frappe = require('frappejs'); -module.exports = function execute(params) { - return new GeneralLedger().run(params); -} - class GeneralLedger { async run(params) { const filters = {}; @@ -22,4 +18,8 @@ class GeneralLedger { return data; } -} \ No newline at end of file +} + +module.exports = function execute(params) { + return new GeneralLedger().run(params); +} diff --git a/server/index.js b/server/index.js index 578165cb..3a615120 100644 --- a/server/index.js +++ b/server/index.js @@ -23,7 +23,7 @@ module.exports = { // set server-side modules frappe.models.Invoice.documentClass = require('../models/doctype/Invoice/InvoiceServer.js'); frappe.models.Payment.documentClass = require('../models/doctype/Payment/PaymentServer.js'); - frappe.models.JournalEntry.documentClass = require('../models/doctype/JournalEntry/JournalEntryServer.js'); + // frappe.models.JournalEntry.documentClass = require('../models/doctype/JournalEntry/JournalEntryServer.js'); frappe.metaCache = {}; From 3da7b7169aa09d8b6ca2239429285d3d86944f82 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 19 Apr 2018 20:01:35 +0530 Subject: [PATCH 4/5] Profit and Loss Statement first cut --- client/index.js | 9 +-- models/doctype/Invoice/Invoice.js | 8 +- reports/ProfitAndLoss/ProfitAndLoss.js | 21 +++-- reports/ProfitAndLoss/ProfitAndLossView.js | 93 ++++++++++++++++++++++ reports/financialStatements.js | 60 ++++++++++++-- reports/generalLedger/GeneralLedgerView.js | 2 + reports/index.js | 42 ++++++++++ server/index.js | 7 +- yarn.lock | 10 +++ 9 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 reports/ProfitAndLoss/ProfitAndLossView.js create mode 100644 reports/index.js diff --git a/client/index.js b/client/index.js index fbab3a15..1358683b 100644 --- a/client/index.js +++ b/client/index.js @@ -1,5 +1,5 @@ -const GeneralLedgerView = require('../reports/generalLedger/GeneralLedgerView'); const frappe = require('frappejs'); +const { registerReportRoutes } = require('../reports'); module.exports = { start() { @@ -8,12 +8,7 @@ module.exports = { frappe.registerView('List', 'Customer', require('../models/doctype/Party/CustomerList.js')); - frappe.router.add('report/general-ledger', async (params) => { - if (!frappe.views.generalLedger) { - frappe.views.generalLedger = new GeneralLedgerView(); - } - await frappe.views.generalLedger.show(params); - }) + registerReportRoutes(); frappe.desk.menu.addItem('ToDo', '#list/ToDo'); frappe.desk.menu.addItem('Chart of Accounts', '#tree/Account'); diff --git a/models/doctype/Invoice/Invoice.js b/models/doctype/Invoice/Invoice.js index 04201f8e..12c56e27 100644 --- a/models/doctype/Invoice/Invoice.js +++ b/models/doctype/Invoice/Invoice.js @@ -31,7 +31,13 @@ module.exports = { "fieldname": "account", "label": "Account", "fieldtype": "Link", - "target": "Account" + "target": "Account", + getFilters: (query, control) => { + return { + keywords: ["like", query], + isGroup: 0 + } + } }, { "fieldname": "items", diff --git a/reports/ProfitAndLoss/ProfitAndLoss.js b/reports/ProfitAndLoss/ProfitAndLoss.js index 58505c76..4223e465 100644 --- a/reports/ProfitAndLoss/ProfitAndLoss.js +++ b/reports/ProfitAndLoss/ProfitAndLoss.js @@ -2,26 +2,25 @@ const frappe = require('frappejs'); const { getData } = require('../financialStatements'); class ProfitAndLoss { - async run(params) { - const filters = {}; - if (params.account) filters.account = params.account; - if (params.party) filters.party = params.party; - if (params.referenceType) filters.referenceType = params.referenceType; - if (params.referenceName) filters.referenceName = params.referenceName; - if (params.fromDate) filters.date = ['>=', params.fromDate]; - if (params.toDate) filters.date = ['<=', params.toDate]; + async run({ fromDate, toDate, periodicity }) { let income = await getData({ rootType: 'Income', - balanceMustBe: 'Credit' + balanceMustBe: 'Credit', + fromDate, + toDate, + periodicity }); let expense = await getData({ rootType: 'Expense', - balanceMustBe: 'Credit' + balanceMustBe: 'Debit', + fromDate, + toDate, + periodicity }); - return data; + return { income, expense }; } } diff --git a/reports/ProfitAndLoss/ProfitAndLossView.js b/reports/ProfitAndLoss/ProfitAndLossView.js new file mode 100644 index 00000000..07526c92 --- /dev/null +++ b/reports/ProfitAndLoss/ProfitAndLossView.js @@ -0,0 +1,93 @@ +const ReportPage = require('frappejs/client/desk/reportpage'); +const frappe = require('frappejs'); +const { unique } = require('frappejs/utils'); + +module.exports = class ProfitAndLossView extends ReportPage { + constructor() { + super({ + title: frappe._('Profit and Loss'), + filterFields: [ + {fieldtype: 'Date', label: 'From Date', required: 1}, + {fieldtype: 'Date', label: 'To Date', required: 1}, + {fieldtype: 'Select', options: ['Monthly', 'Quarterly', 'Half Yearly', 'Yearly'], + label: 'Periodicity', fieldname: 'periodicity'} + ] + }); + + this.method = 'profit-and-loss'; + this.datatableOptions = { + treeView: true + } + } + + getRowsForDataTable(data) { + const { expense, income } = data; + const rows = []; + + rows.push({ + account: 'Income', + indent: 0 + }); + for (let account of Object.keys(income)) { + const row = { + account, + indent: 1 + }; + for (let periodKey of Object.keys(income[account] || {})) { + row[periodKey] = income[account][periodKey]; + } + rows.push(row); + } + + rows.push({ + account: 'Expense', + indent: 0 + }); + for (let account of Object.keys(expense)) { + const row = { + account, + indent: 1 + }; + for (let periodKey of Object.keys(expense[account] || {})) { + row[periodKey] = expense[account][periodKey]; + } + rows.push(row); + } + + return rows; + } + + getColumns(data) { + debugger + const columns = [ + { label: 'Account', fieldtype: 'Data' } + ]; + + if (data) { + const { income, expense } = data; + let currencyColumns = []; + + for (let account of Object.keys(income)) { + const periods = Object.keys(income[account] || {}); + currencyColumns.push(...periods); + } + + for (let account of Object.keys(expense)) { + const periods = Object.keys(expense[account] || {}); + currencyColumns.push(...periods); + } + + currencyColumns = unique(currencyColumns); + + const columnDefs = currencyColumns.map(name => ({ + label: name, + fieldname: name, + fieldtype: 'Currency' + })); + + columns.push(...columnDefs); + } + + return columns; + } +} diff --git a/reports/financialStatements.js b/reports/financialStatements.js index 439ccdb1..ee30a7a5 100644 --- a/reports/financialStatements.js +++ b/reports/financialStatements.js @@ -1,30 +1,80 @@ const frappe = require('frappejs'); +const { DateTime } = require('luxon'); + +async function getData({ + rootType, + balanceMustBe = 'Debit', + fromDate, + toDate, + periodicity = 'Monthly' + }) { -async function getData({ rootType, balanceMustBe }) { const accounts = await getAccounts(rootType); if (!accounts || accounts.length === 0) return []; const ledgerEntries = await frappe.db.getAll({ doctype: 'AccountingLedgerEntry', - fields: ['account', 'debit', 'credit'], + fields: ['account', 'debit', 'credit', 'date'], filters: { - account: ['in', accounts] + account: ['in', accounts], + date: ['>=', fromDate, '<=', toDate] } }); let data = {}; for (let entry of ledgerEntries) { + let periodKey = getPeriodKey(entry.date, periodicity); + if (!data[entry.account]) { - data[entry.account] = 0.0; + data[entry.account] = {}; } - data[entry.account] += entry.debit - entry.credit; + if (!data[entry.account][periodKey]) { + data[entry.account][periodKey] = 0.0; + } + + const multiplier = balanceMustBe === 'Debit' ? 1 : -1; + data[entry.account][periodKey] += (entry.debit - entry.credit) * multiplier; } return data; } +function getPeriodKey(date, periodicity) { + let key; + let dateObj = DateTime.fromISO(date); + let year = dateObj.year; + let quarter = dateObj.quarter; + let month = dateObj.month; + + let getKey = { + 'Monthly': () => `${dateObj.monthShort} ${year}`, + 'Quarterly': () => { + return { + 1: `Jan ${year} - Mar ${year}`, + 2: `Apr ${year} - Jun ${year}`, + 3: `Jun ${year} - Sep ${year}`, + 4: `Oct ${year} - Dec ${year}` + }[quarter] + }, + 'Half Yearly': () => { + return { + 1: `Apr ${year} - Sep ${year}`, + 2: `Oct ${year} - Mar ${year}` + }[[2, 3].includes(quarter) ? 1 : 2] + }, + 'Yearly': () => { + if (month > 3) { + return `${year} - ${year + 1}` + } + return `${year - 1} - ${year}` + } + }[periodicity]; + + return getKey(); +} + async function getAccounts(rootType) { return (await frappe.db.getAll({ doctype: 'Account', diff --git a/reports/generalLedger/GeneralLedgerView.js b/reports/generalLedger/GeneralLedgerView.js index a76faee0..73608f68 100644 --- a/reports/generalLedger/GeneralLedgerView.js +++ b/reports/generalLedger/GeneralLedgerView.js @@ -24,6 +24,8 @@ module.exports = class GeneralLedgerView extends ReportPage { return [ {label: 'Date', fieldtype: 'Date'}, {label: 'Account', fieldtype: 'Link'}, + {label: 'Reference Type', fieldtype: 'Data'}, + {label: 'Reference Name', fieldtype: 'Data'}, {label: 'Party', fieldtype: 'Link'}, {label: 'Description', fieldtype: 'Data'}, {label: 'Debit', fieldtype: 'Currency'}, diff --git a/reports/index.js b/reports/index.js new file mode 100644 index 00000000..6a1469b6 --- /dev/null +++ b/reports/index.js @@ -0,0 +1,42 @@ +const frappe = require('frappejs'); + +const GeneralLedger = require('./GeneralLedger/GeneralLedger'); +const GeneralLedgerView = require('../reports/generalLedger/GeneralLedgerView'); + +const ProfitAndLoss = require('./ProfitAndLoss/ProfitAndLoss'); +const ProfitAndLossView = require('./ProfitAndLoss/ProfitAndLossView'); + +// called on server side +function registerReportMethods() { + frappe.registerMethod({ + method: 'general-ledger', + handler: args => GeneralLedger(args) + }); + + frappe.registerMethod({ + method: 'profit-and-loss', + handler: args => ProfitAndLoss(args) + }); +} + +// called on client side +function registerReportRoutes() { + frappe.router.add('report/general-ledger', async (params) => { + if (!frappe.views.GeneralLedger) { + frappe.views.GeneralLedger = new GeneralLedgerView(); + } + await frappe.views.GeneralLedger.show(params); + }); + + frappe.router.add('report/profit-and-loss', async (params) => { + if (!frappe.views.ProfitAndLoss) { + frappe.views.ProfitAndLoss = new ProfitAndLossView(); + } + await frappe.views.ProfitAndLoss.show(params); + }); +} + +module.exports = { + registerReportMethods, + registerReportRoutes +} diff --git a/server/index.js b/server/index.js index 3a615120..dbe3e8df 100644 --- a/server/index.js +++ b/server/index.js @@ -4,8 +4,8 @@ global.rootRequire = function(name) { const server = require('frappejs/server'); const frappe = require('frappejs'); -const GeneralLedger = require('../reports/generalLedger/GeneralLedger'); const naming = require('frappejs/model/naming'); +const { registerReportMethods } = require('../reports'); module.exports = { async start() { @@ -35,9 +35,6 @@ module.exports = { await naming.createNumberSeries('JV-', 'JournalEntrySettings'); await naming.createNumberSeries('QTN-', 'QuotationSettings'); - frappe.registerMethod({ - method: 'general-ledger', - handler: args => GeneralLedger(args) - }); + registerReportMethods(); } } diff --git a/yarn.lock b/yarn.lock index c0a1ce4b..1c5a3bac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1260,6 +1260,10 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +deepmerge@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -2053,12 +2057,14 @@ frappejs@../frappejs: clusterize.js "^0.18.0" codemirror "^5.35.0" commander "^2.13.0" + deepmerge "^2.1.0" eslint "^4.19.1" express "^4.16.2" flatpickr "^4.3.2" frappe-datatable frappe/datatable frappejs "../frappejs" jquery "^3.3.1" + luxon "^1.0.0" mkdirp "^0.5.1" mocha "^4.1.0" moment "^2.20.1" @@ -3151,6 +3157,10 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +luxon@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.0.0.tgz#ec1cba8cf53be14d2375c2f17e3468eb195c20bb" + macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" From 180825243f794d2dca84e75c66b3532a73488a80 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 24 Apr 2018 13:28:57 +0530 Subject: [PATCH 5/5] Reports - Common FinancialStatementsView - Add BalanceSheet - Add Fiscal Year in Accounting Settings --- client/index.js | 2 + electron/client.js | 6 +- .../AccountingSettings/AccountingSettings.js | 16 +- package.json | 1 + reports/BalanceSheet/BalanceSheet.js | 53 + reports/BalanceSheet/BalanceSheetView.js | 16 + .../FinancialStatements.js | 214 + .../FinancialStatementsView.js | 41 + reports/ProfitAndLoss/ProfitAndLoss.js | 22 +- reports/ProfitAndLoss/ProfitAndLossView.js | 84 +- reports/financialStatements.js | 91 - reports/generalLedger/GeneralLedgerView.js | 6 +- reports/index.js | 15 + setup/config.js | 18 +- www/dist/css/style.css | 317 +- www/dist/js/bundle.js | 8635 +++++++++++++++-- yarn.lock | 7 +- 17 files changed, 8175 insertions(+), 1369 deletions(-) create mode 100644 reports/BalanceSheet/BalanceSheet.js create mode 100644 reports/BalanceSheet/BalanceSheetView.js create mode 100644 reports/FinancialStatements/FinancialStatements.js create mode 100644 reports/FinancialStatements/FinancialStatementsView.js delete mode 100644 reports/financialStatements.js diff --git a/client/index.js b/client/index.js index 1358683b..7307ae03 100644 --- a/client/index.js +++ b/client/index.js @@ -21,6 +21,8 @@ module.exports = { frappe.desk.menu.addItem('Contact', "#list/Contact"); frappe.desk.menu.addItem('Settings', () => frappe.desk.showFormModal('SystemSettings')); frappe.desk.menu.addItem('General Ledger', '#report/general-ledger'); + frappe.desk.menu.addItem('Profit And Loss', '#report/profit-and-loss'); + frappe.desk.menu.addItem('Balance Sheet', '#report/balance-sheet'); frappe.router.default = '#tree/Account'; diff --git a/electron/client.js b/electron/client.js index 301f684b..ee11a4c0 100644 --- a/electron/client.js +++ b/electron/client.js @@ -63,7 +63,9 @@ async function saveSetupWizardValues(values) { name, email, abbreviation, - bankName + bankName, + fiscalYearStart, + fiscalYearEnd } = values; const doc = await frappe.getDoc('AccountingSettings'); @@ -73,6 +75,8 @@ async function saveSetupWizardValues(values) { await doc.set('fullname', name); await doc.set('email', email); await doc.set('bankName', bankName); + await doc.set('fiscalYearStart', fiscalYearStart); + await doc.set('fiscalYearEnd', fiscalYearEnd); await doc.update(); } diff --git a/models/doctype/AccountingSettings/AccountingSettings.js b/models/doctype/AccountingSettings/AccountingSettings.js index a5a3b1a7..7e5515b5 100644 --- a/models/doctype/AccountingSettings/AccountingSettings.js +++ b/models/doctype/AccountingSettings/AccountingSettings.js @@ -50,7 +50,21 @@ module.exports = { "label": "Bank Name", "fieldtype": "Data", "required": 1 - } + }, + + { + "fieldname": "fiscalYearStart", + "label": "Fiscal Year Start Date", + "fieldtype": "Date", + "required": 1 + }, + + { + "fieldname": "fiscalYearEnd", + "label": "Fiscal Year End Date", + "fieldtype": "Date", + "required": 1 + }, ] } \ No newline at end of file diff --git a/package.json b/package.json index a7161b3b..ac777789 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "postinstall": "electron-builder install-app-deps" }, "dependencies": { + "frappe-datatable": "../datatable", "frappejs": "../frappejs" }, "devDependencies": { diff --git a/reports/BalanceSheet/BalanceSheet.js b/reports/BalanceSheet/BalanceSheet.js new file mode 100644 index 00000000..4f274665 --- /dev/null +++ b/reports/BalanceSheet/BalanceSheet.js @@ -0,0 +1,53 @@ +const frappe = require('frappejs'); +const { unique } = require('frappejs/utils'); +const { getData } = require('../FinancialStatements/FinancialStatements'); + +class BalanceSheet { + async run({ fromDate, toDate, periodicity }) { + + let asset = await getData({ + rootType: 'Asset', + balanceMustBe: 'Debit', + fromDate, + toDate, + periodicity, + accumulateValues: true + }); + + let liability = await getData({ + rootType: 'Liability', + balanceMustBe: 'Credit', + fromDate, + toDate, + periodicity, + accumulateValues: true + }); + + let equity = await getData({ + rootType: 'Equity', + balanceMustBe: 'Credit', + fromDate, + toDate, + periodicity, + accumulateValues: true + }); + + const rows = [ + ...asset.accounts, asset.totalRow, [], + ...liability.accounts, liability.totalRow, [], + ...equity.accounts, equity.totalRow, [] + ]; + + const columns = unique([ + ...asset.periodList, + ...liability.periodList, + ...equity.periodList + ]); + + return { rows, columns }; + } +} + +module.exports = function execute(params) { + return new BalanceSheet().run(params); +} diff --git a/reports/BalanceSheet/BalanceSheetView.js b/reports/BalanceSheet/BalanceSheetView.js new file mode 100644 index 00000000..4346a2cb --- /dev/null +++ b/reports/BalanceSheet/BalanceSheetView.js @@ -0,0 +1,16 @@ +const frappe = require('frappejs'); +const FinancialStatementsView = require('../FinancialStatements/FinancialStatementsView'); + +module.exports = class BalanceSheetView extends FinancialStatementsView { + constructor() { + super({ + title: frappe._('Balance Sheet'), + method: 'balance-sheet', + filterFields: [ + {fieldtype: 'Date', label: 'To Date', required: 1}, + {fieldtype: 'Select', options: ['Monthly', 'Quarterly', 'Half Yearly', 'Yearly'], + label: 'Periodicity', fieldname: 'periodicity', default: 'Monthly'} + ] + }); + } +} diff --git a/reports/FinancialStatements/FinancialStatements.js b/reports/FinancialStatements/FinancialStatements.js new file mode 100644 index 00000000..ccc6e36b --- /dev/null +++ b/reports/FinancialStatements/FinancialStatements.js @@ -0,0 +1,214 @@ +const frappe = require('frappejs'); +const { DateTime } = require('luxon'); +const { unique } = require('frappejs/utils'); + +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); + + if (!account[periodKey]) { + account[periodKey] = 0.0; + } + + const multiplier = balanceMustBe === 'Debit' ? 1 : -1; + const value = (entry.debit - entry.credit) * multiplier + account[periodKey] += value; + } + } + + 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] = 0.0; + } + account[periodKey] += account[previousPeriodKey] || 0.0; + } + }); + } + + // calculate totalRow + let totalRow = { + account: `Total ${rootType} (${balanceMustBe})` + }; + + periodList.forEach((periodKey) => { + if (!totalRow[periodKey]) { + totalRow[periodKey] = 0.0; + } + + for (let account of accounts) { + totalRow[periodKey] += account[periodKey] || 0.0; + } + }); + + return { accounts, totalRow, periodList }; +} + +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)); + curDate = curDate.plus({ months: monthsToAdd }); + } + + return out; +} + +function getPeriodKey(date, periodicity) { + let key; + let dateObj = DateTime.fromISO(date); + let year = dateObj.year; + let quarter = dateObj.quarter; + let month = dateObj.month; + + let getKey = { + 'Monthly': () => `${dateObj.monthShort} ${year}`, + 'Quarterly': () => { + return { + 1: `Jan ${year} - Mar ${year}`, + 2: `Apr ${year} - Jun ${year}`, + 3: `Jun ${year} - Sep ${year}`, + 4: `Oct ${year} - Dec ${year}` + }[quarter] + }, + 'Half Yearly': () => { + return { + 1: `Apr ${year} - Sep ${year}`, + 2: `Oct ${year} - Mar ${year}` + }[[2, 3].includes(quarter) ? 1 : 2] + }, + 'Yearly': () => { + if (month > 3) { + return `${year} - ${year + 1}` + } + return `${year - 1} - ${year}` + } + }[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'], + filters: { + rootType + } + }); + + accounts = setIndentLevel(accounts); + accounts = sortAccounts(accounts); + + accounts.forEach(account => { + account.account = account.name; + }); + + return accounts; +} + +async function getFiscalYear() { + let { fiscalYearStart, fiscalYearEnd } = await frappe.getSingle('AccountingSettings'); + return { + start: fiscalYearStart, + end: fiscalYearEnd + }; +} + +module.exports = { + getData +} diff --git a/reports/FinancialStatements/FinancialStatementsView.js b/reports/FinancialStatements/FinancialStatementsView.js new file mode 100644 index 00000000..43238985 --- /dev/null +++ b/reports/FinancialStatements/FinancialStatementsView.js @@ -0,0 +1,41 @@ +const ReportPage = require('frappejs/client/desk/reportpage'); +const frappe = require('frappejs'); +const { unique } = require('frappejs/utils'); + +module.exports = class FinancialStatementsView extends ReportPage { + constructor(opts) { + super({ + title: opts.title, + filterFields: opts.filterFields + }); + + this.method = opts.method; + this.datatableOptions = { + treeView: true, + layout: 'fixed' + } + } + + getRowsForDataTable(data) { + return data.rows || []; + } + + getColumns(data) { + const columns = [ + { label: 'Account', fieldtype: 'Data', fieldname: 'account', width: 340 } + ]; + + if (data && data.columns) { + const currencyColumns = data.columns; + const columnDefs = currencyColumns.map(name => ({ + label: name, + fieldname: name, + fieldtype: 'Currency' + })); + + columns.push(...columnDefs); + } + + return columns; + } +} diff --git a/reports/ProfitAndLoss/ProfitAndLoss.js b/reports/ProfitAndLoss/ProfitAndLoss.js index 4223e465..63c47c85 100644 --- a/reports/ProfitAndLoss/ProfitAndLoss.js +++ b/reports/ProfitAndLoss/ProfitAndLoss.js @@ -1,5 +1,6 @@ const frappe = require('frappejs'); -const { getData } = require('../financialStatements'); +const { unique } = require('frappejs/utils'); +const { getData } = require('../FinancialStatements/FinancialStatements'); class ProfitAndLoss { async run({ fromDate, toDate, periodicity }) { @@ -20,7 +21,24 @@ class ProfitAndLoss { periodicity }); - return { income, expense }; + const rows = [ + ...income.accounts, income.totalRow, [], + ...expense.accounts, expense.totalRow, [] + ]; + + 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.push(profitRow); + + return { rows, columns }; } } diff --git a/reports/ProfitAndLoss/ProfitAndLossView.js b/reports/ProfitAndLoss/ProfitAndLossView.js index 07526c92..dd95a37c 100644 --- a/reports/ProfitAndLoss/ProfitAndLossView.js +++ b/reports/ProfitAndLoss/ProfitAndLossView.js @@ -1,93 +1,17 @@ -const ReportPage = require('frappejs/client/desk/reportpage'); const frappe = require('frappejs'); -const { unique } = require('frappejs/utils'); +const FinancialStatementsView = require('../FinancialStatements/FinancialStatementsView'); -module.exports = class ProfitAndLossView extends ReportPage { +module.exports = class ProfitAndLossView extends FinancialStatementsView { constructor() { super({ title: frappe._('Profit and Loss'), + method: 'profit-and-loss', filterFields: [ {fieldtype: 'Date', label: 'From Date', required: 1}, {fieldtype: 'Date', label: 'To Date', required: 1}, {fieldtype: 'Select', options: ['Monthly', 'Quarterly', 'Half Yearly', 'Yearly'], - label: 'Periodicity', fieldname: 'periodicity'} + label: 'Periodicity', fieldname: 'periodicity', default: 'Monthly'} ] }); - - this.method = 'profit-and-loss'; - this.datatableOptions = { - treeView: true - } - } - - getRowsForDataTable(data) { - const { expense, income } = data; - const rows = []; - - rows.push({ - account: 'Income', - indent: 0 - }); - for (let account of Object.keys(income)) { - const row = { - account, - indent: 1 - }; - for (let periodKey of Object.keys(income[account] || {})) { - row[periodKey] = income[account][periodKey]; - } - rows.push(row); - } - - rows.push({ - account: 'Expense', - indent: 0 - }); - for (let account of Object.keys(expense)) { - const row = { - account, - indent: 1 - }; - for (let periodKey of Object.keys(expense[account] || {})) { - row[periodKey] = expense[account][periodKey]; - } - rows.push(row); - } - - return rows; - } - - getColumns(data) { - debugger - const columns = [ - { label: 'Account', fieldtype: 'Data' } - ]; - - if (data) { - const { income, expense } = data; - let currencyColumns = []; - - for (let account of Object.keys(income)) { - const periods = Object.keys(income[account] || {}); - currencyColumns.push(...periods); - } - - for (let account of Object.keys(expense)) { - const periods = Object.keys(expense[account] || {}); - currencyColumns.push(...periods); - } - - currencyColumns = unique(currencyColumns); - - const columnDefs = currencyColumns.map(name => ({ - label: name, - fieldname: name, - fieldtype: 'Currency' - })); - - columns.push(...columnDefs); - } - - return columns; } } diff --git a/reports/financialStatements.js b/reports/financialStatements.js deleted file mode 100644 index ee30a7a5..00000000 --- a/reports/financialStatements.js +++ /dev/null @@ -1,91 +0,0 @@ -const frappe = require('frappejs'); -const { DateTime } = require('luxon'); - -async function getData({ - rootType, - balanceMustBe = 'Debit', - fromDate, - toDate, - periodicity = 'Monthly' - }) { - - const accounts = await getAccounts(rootType); - if (!accounts || accounts.length === 0) return []; - - const ledgerEntries = await frappe.db.getAll({ - doctype: 'AccountingLedgerEntry', - fields: ['account', 'debit', 'credit', 'date'], - filters: { - account: ['in', accounts], - date: ['>=', fromDate, '<=', toDate] - } - }); - - let data = {}; - - for (let entry of ledgerEntries) { - let periodKey = getPeriodKey(entry.date, periodicity); - - if (!data[entry.account]) { - data[entry.account] = {}; - } - - if (!data[entry.account][periodKey]) { - data[entry.account][periodKey] = 0.0; - } - - const multiplier = balanceMustBe === 'Debit' ? 1 : -1; - data[entry.account][periodKey] += (entry.debit - entry.credit) * multiplier; - } - - return data; -} - -function getPeriodKey(date, periodicity) { - let key; - let dateObj = DateTime.fromISO(date); - let year = dateObj.year; - let quarter = dateObj.quarter; - let month = dateObj.month; - - let getKey = { - 'Monthly': () => `${dateObj.monthShort} ${year}`, - 'Quarterly': () => { - return { - 1: `Jan ${year} - Mar ${year}`, - 2: `Apr ${year} - Jun ${year}`, - 3: `Jun ${year} - Sep ${year}`, - 4: `Oct ${year} - Dec ${year}` - }[quarter] - }, - 'Half Yearly': () => { - return { - 1: `Apr ${year} - Sep ${year}`, - 2: `Oct ${year} - Mar ${year}` - }[[2, 3].includes(quarter) ? 1 : 2] - }, - 'Yearly': () => { - if (month > 3) { - return `${year} - ${year + 1}` - } - return `${year - 1} - ${year}` - } - }[periodicity]; - - return getKey(); -} - -async function getAccounts(rootType) { - return (await frappe.db.getAll({ - doctype: 'Account', - fields: ['name'], - filters: { - rootType - } - })) - .map(d => d.name); -} - -module.exports = { - getData -} diff --git a/reports/generalLedger/GeneralLedgerView.js b/reports/generalLedger/GeneralLedgerView.js index 73608f68..3d6564b7 100644 --- a/reports/generalLedger/GeneralLedgerView.js +++ b/reports/generalLedger/GeneralLedgerView.js @@ -24,13 +24,13 @@ module.exports = class GeneralLedgerView extends ReportPage { return [ {label: 'Date', fieldtype: 'Date'}, {label: 'Account', fieldtype: 'Link'}, + {label: 'Debit', fieldtype: 'Currency'}, + {label: 'Credit', fieldtype: 'Currency'}, + {label: 'Balance', fieldtype: 'Currency'}, {label: 'Reference Type', fieldtype: 'Data'}, {label: 'Reference Name', fieldtype: 'Data'}, {label: 'Party', fieldtype: 'Link'}, {label: 'Description', fieldtype: 'Data'}, - {label: 'Debit', fieldtype: 'Currency'}, - {label: 'Credit', fieldtype: 'Currency'}, - {label: 'Balance', fieldtype: 'Currency'} ] } } diff --git a/reports/index.js b/reports/index.js index 6a1469b6..bdd43396 100644 --- a/reports/index.js +++ b/reports/index.js @@ -6,6 +6,9 @@ const GeneralLedgerView = require('../reports/generalLedger/GeneralLedgerView'); const ProfitAndLoss = require('./ProfitAndLoss/ProfitAndLoss'); const ProfitAndLossView = require('./ProfitAndLoss/ProfitAndLossView'); +const BalanceSheet = require('./BalanceSheet/BalanceSheet'); +const BalanceSheetView = require('./BalanceSheet/BalanceSheetView'); + // called on server side function registerReportMethods() { frappe.registerMethod({ @@ -17,6 +20,11 @@ function registerReportMethods() { method: 'profit-and-loss', handler: args => ProfitAndLoss(args) }); + + frappe.registerMethod({ + method: 'balance-sheet', + handler: args => BalanceSheet(args) + }); } // called on client side @@ -34,6 +42,13 @@ function registerReportRoutes() { } await frappe.views.ProfitAndLoss.show(params); }); + + frappe.router.add('report/balance-sheet', async (params) => { + if (!frappe.views.BalanceSheet) { + frappe.views.BalanceSheet = new BalanceSheetView(); + } + await frappe.views.BalanceSheet.show(params); + }); } module.exports = { diff --git a/setup/config.js b/setup/config.js index 6edb5448..5f4079db 100644 --- a/setup/config.js +++ b/setup/config.js @@ -46,7 +46,21 @@ module.exports = { "label": "Bank Name", "fieldtype": "Data", "required": 1 - } + }, + + { + "fieldname": "fiscalYearStart", + "label": "Fiscal Year Start Date", + "fieldtype": "Date", + "required": 1 + }, + + { + "fieldname": "fiscalYearEnd", + "label": "Fiscal Year End Date", + "fieldtype": "Date", + "required": 1 + }, ], layout: [ @@ -67,7 +81,7 @@ module.exports = { { title: 'Add your Company', - fields: ['companyName', 'bankName'] + fields: ['companyName', 'bankName', 'fiscalYearStart', 'fiscalYearEnd'] } ] } diff --git a/www/dist/css/style.css b/www/dist/css/style.css index 40a1c6a6..025b1ee9 100644 --- a/www/dist/css/style.css +++ b/www/dist/css/style.css @@ -7130,40 +7130,160 @@ div.CodeMirror-dragcursors { /* Help users use markselection to safely style text background */ span.CodeMirror-selectedtext { background: none; } -/* This file is processed by postcss */ -/* variables */ -.data-table { - /* styling */ - position: relative; - overflow: auto; } -/* resets */ -.data-table *, .data-table *::after, .data-table *::before { +.datatable *, .datatable *::after, .datatable *::before { -webkit-box-sizing: border-box; box-sizing: border-box; } -.data-table button, .data-table input { +.datatable { + position: relative; + overflow: auto; } +.dt-header { + border-collapse: collapse; + border-bottom: 1px solid #d1d8dd; + position: absolute; + top: 0; + left: 0; + background-color: #fff; } +.dt-body { + border-collapse: collapse; } +.dt-scrollable { + max-height: 40vw; + overflow: auto; + border-bottom: 1px solid #d1d8dd; } +.dt-scrollable--highlight-all { + background-color: #fffce7; } +.dt-scrollable__no-data { + text-align: center; + padding: 16px; + padding: 1rem; + border-left: 1px solid #d1d8dd; + border-right: 1px solid #d1d8dd; } +.dt-row--highlight { + background-color: #fffce7; } +.dt-row--unhighlight { + background-color: #fff; } +.dt-row--hide { + display: none; } +.dt-cell { + border: 1px solid #d1d8dd; + position: relative; + outline: none; + padding: 0; } +.dt-cell__content { + padding: 8px; + padding: 0.5rem; + border: 2px solid transparent; + height: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } +.dt-cell__edit { + display: none; + padding: 8px; + padding: 0.5rem; + background-color: #fff; + border: 2px solid #ffa00a; + z-index: 1; + height: 100%; } +.dt-cell__resize-handle { + opacity: 0; + position: absolute; + right: -3px; + top: 0; + width: 5px; + height: 100%; + cursor: col-resize; + z-index: 1; } +.dt-cell--editing .dt-cell__content { + display: none; } +.dt-cell--editing .dt-cell__edit { + display: block; } +.dt-cell--focus .dt-cell__content { + border-color: #5292f7; } +.dt-cell--highlight { + background-color: #f5f7fa; } +.dt-cell--dragging { + background-color: #f5f7fa; } +.dt-cell--header .dt-cell__content { + padding-right: 16px; + padding-right: 1rem; + font-weight: bold; } +.dt-cell--header:hover .dt-dropdown__toggle { + opacity: 1; } +.dt-cell--tree-close .dt-tree-node__toggle:before { + content: '►'; } +.dt-dropdown { + position: absolute; + right: 10px; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: top; + text-align: left; + font-weight: normal; + cursor: pointer; } +.dt-dropdown__toggle { + opacity: 0; } +.dt-dropdown__list { + display: none; + position: absolute; + min-width: 128px; + min-width: 8rem; + top: 100%; + right: 0; + z-index: 1; + background-color: #fff; + border-radius: 3px; + padding: 8px 0; + padding: 0.5rem 0; + -webkit-box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); } +.dt-dropdown__list-item { + padding: 8px 16px; + padding: 0.5rem 1rem; } +.dt-dropdown__list-item:hover { + background-color: #f5f7fa; } +.dt-dropdown--active .dt-dropdown__list { + display: block; } +.dt-tree-node { + display: inline-block; + position: relative; } +.dt-tree-node__toggle { + display: inline-block; + position: absolute; + font-size: 10px; + padding: 0 4px; + cursor: pointer; } +.dt-tree-node__toggle:before { + content: '▼'; } +.dt-toast { + position: absolute; + bottom: 16px; + bottom: 1rem; + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); } +.dt-toast__message { + display: inline-block; + background-color: rgba(0, 0, 0, 0.8); + color: #dfe2e5; + border-radius: 3px; + padding: 8px 16px; + padding: 0.5rem 1rem; } +.dt-input { + outline: none; + width: 100%; + border: none; overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; margin: 0; padding: 0; } -.data-table .input-style { - outline: none; - width: 100%; - border: none; } -.data-table *, .data-table *:focus { - outline: none; - border-radius: 0px; - -webkit-box-shadow: none; - box-shadow: none; } -.data-table table { - border-collapse: collapse; } -.data-table table td { - padding: 0; - border: 1px solid #d1d8dd; } -.data-table thead td { - border-bottom-width: 1px; } -.data-table .freeze-container { +.dt-freeze { display: -webkit-box; display: -ms-flexbox; display: flex; @@ -7180,150 +7300,15 @@ span.CodeMirror-selectedtext { background-color: #f5f7fa; opacity: 0.5; font-size: 2em; } -.data-table .freeze-container span { +.dt-freeze__message { position: absolute; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); } -.data-table .hide { - display: none; } -.data-table .toast-message { - position: absolute; - bottom: 16px; - bottom: 1rem; - left: 50%; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); } -.data-table .toast-message span { - display: inline-block; - background-color: rgba(0, 0, 0, 0.8); - color: #dfe2e5; - border-radius: 3px; - padding: 8px 16px; - padding: 0.5rem 1rem; } -.body-scrollable { - max-height: 500px; - overflow: auto; - border-bottom: 1px solid #d1d8dd; } -.body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) { - background-color: #f5f7fa; } -.body-scrollable .no-data td { - text-align: center; - padding: 8px; - padding: 0.5rem; } -.data-table-header { - position: absolute; - top: 0; - left: 0; - background-color: white; - font-weight: bold; } -.data-table-header .content span:not(.column-resizer) { - cursor: pointer; } -.data-table-header .column-resizer { - display: none; - position: absolute; - right: 0; - top: 0; - width: 4px; - width: 0.25rem; - height: 100%; - background-color: #5292f7; - cursor: col-resize; } -.data-table-header .data-table-dropdown { - position: absolute; - right: 10px; - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - vertical-align: top; - text-align: left; } -.data-table-header .data-table-dropdown.is-active .data-table-dropdown-list { - display: block; } -.data-table-header .data-table-dropdown.is-active .data-table-dropdown-toggle { - display: block; } -.data-table-header .data-table-dropdown-toggle { - display: none; - background-color: transparent; - border: none; } -.data-table-header .data-table-dropdown-list { - display: none; - font-weight: normal; - position: absolute; - min-width: 128px; - min-width: 8rem; - top: 100%; - right: 0; - z-index: 1; - background-color: white; - border-radius: 3px; - -webkit-box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); - box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); - padding-bottom: 8px; - padding-bottom: 0.5rem; - padding-top: 8px; - padding-top: 0.5rem; } -.data-table-header .data-table-dropdown-list > div { - padding: 8px 16px; - padding: 0.5rem 1rem; } -.data-table-header .data-table-dropdown-list > div:hover { - background-color: #f5f7fa; } -.data-table-header .data-table-cell.remove-column { - background-color: #FD8B8B; - -webkit-transition: 300ms background-color ease-in-out; - transition: 300ms background-color ease-in-out; } -.data-table-header .data-table-cell.sortable-chosen { - background-color: #f5f7fa; } -.data-table-cell { - position: relative; } -.data-table-cell .content { - padding: 8px; - padding: 0.5rem; - border: 2px solid transparent; } -.data-table-cell .content.ellipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; } -.data-table-cell .edit-cell { - display: none; - padding: 8px; - padding: 0.5rem; - background: #fff; - z-index: 1; - height: 100%; } -.data-table-cell.selected .content { - border: 2px solid #5292f7; } -.data-table-cell.editing .content { - display: none; } -.data-table-cell.editing .edit-cell { - border: 2px solid #5292f7; - display: block; } -.data-table-cell.highlight { - background-color: #f5f7fa; } -.data-table-cell:hover .column-resizer { - display: inline-block; } -.data-table-cell:hover .data-table-dropdown-toggle { - display: block; } -.data-table-cell .tree-node { - display: inline-block; - position: relative; } -.data-table-cell .toggle { - display: inline-block; - position: absolute; - padding: 0 4px; - cursor: pointer; } -.data-table-cell .toggle:before { - content: '▼'; } -.data-table-cell.tree-close .toggle:before { - content: '►'; } -.data-table-row.row-highlight { - background-color: #f5f7fa; } -.noselect { - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; } -body.data-table-resize { +.dt-paste-target { + position: fixed; + left: -999em; } +body.dt-resize { cursor: col-resize; } .indicator, .indicator-right { background: none; diff --git a/www/dist/js/bundle.js b/www/dist/js/bundle.js index 27abea32..94350bc1 100644 --- a/www/dist/js/bundle.js +++ b/www/dist/js/bundle.js @@ -69,6 +69,21 @@ var utils = { }); }, + /** + * Returns array from 0 to n - 1 + * @param {Number} n + */ + range(n) { + return Array.from(Array(4)).map((d, i) => i) + }, + + unique(list, key = it => it) { + var seen = {}; + return list.filter(item => { + var k = key(item); + return seen.hasOwnProperty(k) ? false : (seen[k] = true); + }); + } }; const number_formats = { @@ -4575,7 +4590,7 @@ if (typeof undefined === 'function' && undefined.amd) { } }).call(commonjsGlobal); - +//# sourceMappingURL=showdown.js.map }); var moment = createCommonjsModule(function (module, exports) { @@ -9913,6 +9928,14 @@ var model = { { fieldname: 'parentfield', fieldtype: 'Data', required: 1 } + ], + treeFields: [ + { + fieldname: 'lft', fieldtype: 'Int', required: 1 + }, + { + fieldname: 'rgt', fieldtype: 'Int', required: 1 + } ] }; @@ -10026,6 +10049,15 @@ var meta = class BaseMeta extends document$1 { } } + if (this.isTree) { + // tree fields + for (let field of model.treeFields) { + if (frappejs.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) { + _add(field); + } + } + } + // doctype fields for (let field of this.fields) { let include = frappejs.db.typeMap[field.fieldtype]; @@ -23075,7 +23107,7 @@ Popper.placements = placements; Popper.Defaults = Defaults; - +//# sourceMappingURL=popper.js.map var popper = Object.freeze({ @@ -26974,7 +27006,7 @@ exports.Tooltip = Tooltip; Object.defineProperty(exports, '__esModule', { value: true }); }))); - +//# sourceMappingURL=bootstrap.js.map }); unwrapExports(bootstrap); @@ -28192,7 +28224,6 @@ object-assign @license MIT */ -/* eslint-disable no-unused-vars */ var getOwnPropertySymbols = Object.getOwnPropertySymbols; var hasOwnProperty = Object.prototype.hasOwnProperty; var propIsEnumerable = Object.prototype.propertyIsEnumerable; @@ -28345,7 +28376,7 @@ var treeNode$1 = Object.freeze({ default: treeNode }); -var require$$0$1 = ( treeNode$1 && treeNode ) || treeNode$1; +var require$$0$2 = ( treeNode$1 && treeNode ) || treeNode$1; const iconSet = { open: octicons["triangle-down"].toSVG({ width: "12", height: "12", "class": "tree-icon-open" }), @@ -28358,7 +28389,7 @@ class TreeNode extends baseComponent { } get templateHTML() { - return require$$0$1; + return require$$0$2; } constructor() { @@ -28443,11 +28474,11 @@ var tree = Object.freeze({ default: index }); -var require$$0$2 = ( tree && index ) || tree; +var require$$0$3 = ( tree && index ) || tree; class Tree extends baseComponent { get templateHTML() { - return require$$0$2; + return require$$0$3; } constructor() { @@ -28457,8 +28488,6 @@ class Tree extends baseComponent { window.customElements.define('f-tree', Tree); -// const keyboard = require('frappejs/client/ui/keyboard'); - var tree$3 = class BaseTree extends list { init() { @@ -28561,6 +28590,8 @@ var tree$3 = class BaseTree extends list { if (action === 'edit') { this.edit(treeNode.props.doc.name); + } else if (action === 'addChild') { + this.addChildNode(treeNode.props.doc.name); } }); @@ -28571,6 +28602,15 @@ var tree$3 = class BaseTree extends list { frappejs.desk.showFormModal(this.doctype, name); } + async addChildNode(name) { + const newDoc = await frappejs.getNewDoc(this.doctype); + const formModal = await frappejs.desk.showFormModal(this.doctype, newDoc.name); + const parentField = this.treeSettings.parentField; + if (formModal.form.doc.meta.hasField(parentField)) { + formModal.form.doc.set(parentField, name); + } + } + async getData(node) { let fields = this.getFields(); let filters = {}; @@ -28606,8 +28646,8 @@ var tree$3 = class BaseTree extends list { getActionButtonsHTML() { return [ - { id: 'edit', label: frappejs._('Edit') } - // { id: 'addChild', label: frappe._('Add Child') }, + { id: 'edit', label: frappejs._('Edit') }, + { id: 'addChild', label: frappejs._('Add Child') }, // { id: 'delete', label: frappe._('Delete') }, ].map(button => { return `