From e79e32721c847e7c2bd9e777371cf7d5f4f59b27 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 24 Feb 2015 12:02:29 +0100 Subject: [PATCH] [ADD] examples of journal entries * interaction is terrible, select is ugly but radio buttons take way too much vertical space, no matter whether they're on the left or the right * may be possible to unify operations lists between chart of accounts and entries? Not currently selected set, just the list of possible ops --- _static/chart-of-accounts.js | 2 +- _static/entries.js | 172 ++++++++++++++++++++++++ _static/fiscalyear.js | 247 ----------------------------------- conf.py | 2 +- index.rst | 2 +- 5 files changed, 175 insertions(+), 250 deletions(-) create mode 100644 _static/entries.js delete mode 100644 _static/fiscalyear.js diff --git a/_static/chart-of-accounts.js b/_static/chart-of-accounts.js index 6cb0e4bf2..1703bbd70 100644 --- a/_static/chart-of-accounts.js +++ b/_static/chart-of-accounts.js @@ -27,7 +27,7 @@ { key: toKey(label), style: {display: 'block'}, - className: (operations === state.get('active') && 'highlight-op') + className: (operations === state.get('active') ? 'highlight-op' : void 0) }, React.DOM.input({ type: 'checkbox', diff --git a/_static/entries.js b/_static/entries.js new file mode 100644 index 000000000..b2ced339a --- /dev/null +++ b/_static/entries.js @@ -0,0 +1,172 @@ +(function () { + 'use strict'; + + var data = createAtom(); + data.addWatch('chart', function (k, m, prev, next) { + React.render( + React.createElement(Controls, {entry: next}), + document.getElementById('entries-control')); + React.render( + React.createElement(FormatEntry, {entry: next}), + document.querySelector('.journal-entries')); + }); + document.addEventListener('DOMContentLoaded', function () { + var entries_node = document.getElementById('journal-entries'), + controls = document.createElement('div'); + controls.setAttribute('id', 'entries-control'); + entries_node.insertBefore(controls, entries_node.lastElementChild); + + data.reset(entries.first()); + }); + + var Controls = React.createClass({ + render: function () { + var _this = this; + return React.DOM.div( + null, + "Example journal entries: ", + React.DOM.select( + { + value: entries.indexOf(this.props.entry), + onChange: function (e) { + data.reset(entries.get(e.target.value)); + } + }, + entries.map(function (entry, index) { + return React.DOM.option( + {key: index, value: index}, + entry.get('title') + ); + }).toArray()), + this.props.entry && React.DOM.p(null, this.props.entry.get('help')) + ); + } + }); + var FormatEntry = React.createClass({ + render: function () { + return React.DOM.table( + {className: 'table'}, + React.DOM.thead( + null, + React.DOM.tr( + null, + React.DOM.th(), + React.DOM.th({width: '25%', className: 'text-right'}, "Debit"), + React.DOM.th({width: '25%', className: 'text-right'}, "Credit") + ) + ), + React.DOM.tbody( + null, + this.render_rows() + ) + ); + }, + render_rows: function () { + if (!this.props.entry) { return; } + return this.props.entry.get('operations').map(this.render_row).toArray(); + }, + render_row: function (entry, index) { + if (!entry) { + return React.DOM.tr( + {key: 'spacer-' + index}, + React.DOM.td({colSpan: 3}, "\u00A0") + ); + } + return React.DOM.tr( + {key: index}, + React.DOM.td(null, entry.get('account')), + React.DOM.td({className: 'text-right'}, entry.get('debit')), + React.DOM.td({className: 'text-right'}, entry.get('credit')) + ); + } + }); + + // TODO: help explaining what the operation is about? + // TODO: link to relevant Odoo operation? + var entries = Immutable.fromJS([ + { + title: "Company Founding", + operations: [ + {account: 'Cash', debit: 10000}, + {account: 'Common Stock', credit: 10000} + ] + }, { + title: "Buy work tooling", + operations: [ + {account: 'Tooling', debit: 3000}, + {account: 'Cash', credit: 3000} + ] + }, { + title: "Buy work tooling (invoiced)", + operations: [ + {account: 'Tooling', debit: 3000}, + {account: 'Accounts Payable', credit: 3000} + ] + }, { + title: "Pay supplier invoice", + operations: [ + {account: 'Accounts Payable', debit: 3000}, + {account: 'Cash', credit: 3000} + ] + }, { + title: "Sale paid immediately", + operations: [ + {account: 'Cash', debit: 100}, + {account: 'Sales', credit: 100} + ] + }, { + title: "Delayed payment (trade credit)", + operations: [ + {account: 'Accounts Receivable', debit: 1000}, + {account: 'Sales', credit: 1000} + ] + }, { + title: "Customer invoice paid", + operations: [ + {account: 'Cash', debit: 1000}, + {account: 'Accounts Receivable', credit: 1000} + ] + }, { + title: "Customer invoice, 10% early payment rebate", + operations: [ + {account: 'Cash', debit: 900}, + {account: 'Sales Discount', debit: 100}, + {account: 'Accounts Receivable', credit: 1000} + ] + }, { + title: "Sale with tax", + operations: [ + {account: 'Cash', debit: 109}, + {account: 'Sales', credit: 100}, + {account: 'Taxes Payable', credit: 9} + ] + }, { + title: "Fiscal year cloture — positive earnings and 50% dividends", + operations: [ + {account: 'Revenue', debit: 5000}, + {account: 'Income Summary', credit: 5000}, + null, + {account: 'Income Summary', debit: 4000}, + {account: 'Expenses', credit: 4000}, + null, + {account: 'Income Summary', debit: 1000}, + {account: 'Retained Earnings', credit: 1000}, + null, + {account: 'Retained Earnings', debit: 500}, + {account: 'Dividend Payable', credit: 500} + ] + }, { + title: "Fiscal year cloture — negative earnings and dividend irrelevant", + operations: [ + {account: 'Revenue', debit: 5000}, + {account: 'Income Summary', credit: 5000}, + null, + {account: 'Income Summary', debit: 6000}, + {account: 'Expenses', credit: 6000}, + null, + {account: 'Retained Earnings', debit: 1000}, + {account: 'Income Summary', credit: 1000} + ] + } + ]); +}()); diff --git a/_static/fiscalyear.js b/_static/fiscalyear.js deleted file mode 100644 index 22171c8c3..000000000 --- a/_static/fiscalyear.js +++ /dev/null @@ -1,247 +0,0 @@ -(function () { - 'use strict'; - - function item(it) { - if (it.credit && it.debit) { - throw new Error("A journal item can't have both credit and debit, got " + JSON.stringify(it)); - } - return React.DOM.tr( - {key: it.label.toLowerCase().split(' ').concat( - ['debit', it.debit, 'credit', it.credit] - ).join('-') - }, - React.DOM.td(null, (it.credit ? '\u2001' : '') + it.label), - React.DOM.td(null, it.debit), - React.DOM.td(null, it.credit) - ); - } - function spacer(key) { - return React.DOM.tr({key: 'spacer-' + key}, React.DOM.td({colSpan: 3}, "\u00A0")); - } - - var ClotureTable = React.createClass({ - getInitialState: function () { - return { - revenues: { - cash: 800, - receivable: 200, - }, - expenses: { - cash: 100, - payable: 500, - }, - // don't ignore/break invalid values - dividends: "0.5", - }; - }, - - income: function () { - return ( - (this.state.revenues.cash + this.state.revenues.receivable) - - - (this.state.expenses.cash + this.state.expenses.payable) - ); - }, - - render: function () { - return React.DOM.div( - null, - this.controls(), - React.DOM.table( - {className: 'table'}, - this.makeHeader(), - React.DOM.tbody( - null, - this.revenues(this.state.revenues), - spacer('table-1'), - this.expenses(this.state.expenses), - spacer('table-2'), - this.closure({ - dividends: this.state.dividends, - income: this.income(), - }) - ) - ) - ); - }, - makeHeader: function () { - return React.DOM.thead( - null, - React.DOM.tr( - null, - React.DOM.th(), - React.DOM.th(null, "Debit"), - React.DOM.th(null, "Credit") - ) - ); - }, - - controls: function () { - var _this = this; - return [ - React.DOM.fieldset( - {key: 'income'}, - React.DOM.legend(null, "Income"), - React.DOM.label( - null, "Cash ", - React.DOM.input({ - type: 'number', - step: 1, - value: this.state.revenues.cash, - onChange: function (e) { - var val = e.target.value; - _this.setState({ - revenues: { - cash: val ? parseInt(val, 10) : 0, - receivable: _this.state.revenues.receivable - } - }); - } - }) - ), - ' ', - React.DOM.label( - null, " Accounts Receivable ", - React.DOM.input({ - type: 'number', - step: 1, - value: this.state.revenues.receivable, - onChange: function (e) { - var val = e.target.value; - _this.setState({ - revenues: { - cash: _this.state.revenues.cash, - receivable: val ? parseInt(val, 10) : 0, - } - }) - } - }) - ) - ), - React.DOM.fieldset( - {key: 'expenses'}, - React.DOM.legend(null, "Expenses"), - React.DOM.label( - null, "Cash ", - React.DOM.input({ - type: 'number', - step: 1, - value: this.state.expenses.cash, - onChange: function (e) { - var val = e.target.value; - _this.setState({ - expenses: { - cash: val ? parseInt(val, 10): 0, - payable: _this.state.expenses.payable - } - }) - } - }) - ), - ' ', - React.DOM.label( - null, " Accounts Payable ", - React.DOM.input({ - type: 'number', - step: 1, - value: this.state.expenses.payable, - onChange: function (e) { - var val = e.target.value; - _this.setState({ - expenses: { - cash: _this.state.expenses.cash, - payable: val ? parseInt(val, 10) : 0 - } - }) - } - }) - ) - ), - React.DOM.fieldset( - {key: 'dividends'}, - React.DOM.legend(null, "Dividends"), - React.DOM.label( - null, - "Ratio (from retained earnings) ", - React.DOM.input({ - type: 'range', - min: 0, - max: 1, - step: 0.01, - value: this.state.dividends, - style: { display: 'inline-block' }, - onChange: function (e) { - _this.setState({dividends: e.target.value}); - } - }) - ) - ) - ]; - }, - // components must return a single root which isn't practical here - closure: function (props) { - var result, income = Math.abs(props.income), dividends = 0; - if (props.income > 0) { - // credit retained earnings from income, then credit dividends - // from retained - var dividends = parseInt(income * Math.max(0, Math.min(1, parseFloat(props.dividends)))); - result = [ - item({label: "Income Summary", debit: income}), - item({label: "Retained Earnings", credit: income}), - ]; - } else { - // debit retained earnings, no dividends - result = [ - item({label: "Retained Earnings", debit: income}), - item({label: "Income Summary", credit: income}), - ]; - } - if (dividends) { - result = result.concat([ - spacer('closure'), - item({label: "Retained Earnings", debit: dividends}), - item({label: "Dividends Payable", credit: dividends}) - ]); - } - return result; - }, - revenues: function (props) { - var total = props.cash + props.receivable; - return [ - item({label: "Cash", debit: props.cash}), - item({label: "Accounts Receivable", debit: props.receivable}), - item({label: "Revenue", credit: total}), - React.DOM.tr({key: 'revenue-notes'}, React.DOM.td( - {colSpan: 3}, - "\u2001\u2001Consolidation of revenues")), - spacer('revenues'), - item({label: "Revenue", debit: total}), - item({label: "Income Summary", credit: total}) - ]; - }, - expenses: function (props) { - var total = props.cash + props.payable; - return [ - item({label: "Expenses", debit: total}), - item({label: "Cash", credit: props.cash}), - item({label: "Accounts Payable", credit: props.payable}), - React.DOM.tr( - {key: 'expenses-note'}, React.DOM.td( - {colSpan: 3}, - "\u2001\u2001Consolidation of expenses" - ) - ), - spacer('expenses'), - item({label: "Income Summary", debit: total}), - item({label: "Expenses", credit: total}) - ]; - } - }); - - document.addEventListener('DOMContentLoaded', function () { - React.render( - React.createElement(ClotureTable), - document.querySelector('.fiscal-year-closing')); - }); - -})(); diff --git a/conf.py b/conf.py index 6f68c6c91..7973e8516 100644 --- a/conf.py +++ b/conf.py @@ -272,4 +272,4 @@ def setup(app): app.add_javascript('react.js') app.add_javascript('accounts.js') app.add_javascript('chart-of-accounts.js') - app.add_javascript('fiscalyear.js') + app.add_javascript('entries.js') diff --git a/index.rst b/index.rst index 0a3aa6662..5715c1dc3 100644 --- a/index.rst +++ b/index.rst @@ -123,7 +123,7 @@ context. Common journals are: .. h:div:: force-right journal-entries - .. todo:: insert examples of accounting entries for various transactions + examples of accounting entries for various transactions Reconciliation ==============