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 `