diff --git a/_static/accounting.css b/_static/accounting.css index 6b8155e7c..234db077b 100644 --- a/_static/accounting.css +++ b/_static/accounting.css @@ -77,3 +77,10 @@ li > p { .accounts-table dt span:last-child { font-style: normal; } + +.highlight-op { + background-color: #dce6f8; +} +.chart-of-accounts .highlight-op { + background-color: #030035; +} diff --git a/_static/chart-of-accounts.js b/_static/chart-of-accounts.js index bc272e882..2798cf1b6 100644 --- a/_static/chart-of-accounts.js +++ b/_static/chart-of-accounts.js @@ -18,296 +18,127 @@ return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-'); } - var isFulfilled = (function () { - var enabledTransactions = Immutable.Seq(); - // memoize enabled ops so they don't have to be recomputed all the time - data.addWatch('enableds', function (k, m, prev, next) { - enabledTransactions = next.filter(function (v, k) { - return v.get('enabled'); - }).keySeq(); - }); - return function isFulfilled(deps) { - var d = Immutable.Set(deps); - return d.isEmpty() || d.subtract(enabledTransactions).isEmpty(); - } - })(); - function isEnabled(transaction) { var item = data.deref().get(transaction); - return item.get('enabled') && isFulfilled(item.get('depends')); + return item.get('enabled'); } var Controls = React.createClass({ - getInitialState: function () { - return { folded: true }; - }, - toggle: function () { - this.setState({folded: !this.state.folded}); - }, render: function () { - return React.DOM.div( - null, - React.DOM.h4( - { onClick: this.toggle, style: { cursor: 'pointer' } }, - this.state.folded ? "\u25B8 " : "\u25BE ", - "Operations"), - this.state.folded ? undefined : this.props.p.map(function (v, k) { - return React.DOM.label( - {key: k, style: {display: 'block' } }, - React.DOM.input({ - type: 'checkbox', - disabled: !isFulfilled(v.get('depends')), - checked: v.get('enabled'), - onChange: function () { - data.swap(function (d) { - return d.updateIn( - [k, 'enabled'], - function (check) { return !check; }); + var _this = this; + return React.DOM.div(null, operations.map(function (op) { + var label = op.get('label'), operations = op.get('operations'); + return React.DOM.label( + { + key: toKey(label), + style: {display: 'block'}, + className: (operations === _this.props.p.last() && 'highlight-op') + }, + React.DOM.input({ + type: 'checkbox', + checked: _this.props.p.contains(operations), + onChange: function (e) { + if (e.target.checked) { + data.swap(function (ops) { + return ops.add(operations); + }); + } else { + data.swap(function (ops) { + return ops.remove(operations); }); } - }), - " ", - v.get('label') - ); - }, this).toArray() - ); + } + }), + " ", + label + ); + }).toArray()); } }); - var Journal = React.createClass({ - render: function () { - return React.DOM.div( - null, - React.createElement(Controls, this.props), - React.DOM.table( - {className: 'table'}, - React.DOM.thead( - null, - React.DOM.tr( - null, - React.DOM.th(), - React.DOM.th({width: '20%'}, "Debit"), - React.DOM.th({width: '20%'}, "Credit") - ) - ), - React.DOM.tbody( - null, - this.props.p - .filter(function (v, k) { return isEnabled(k); }) - .valueSeq() - .flatMap(function (tx) { - if (tx.get('operations').isEmpty()) { - return []; - } - var label = tx.get('label'); - var k = toKey(label); - return tx.get('operations').toSeq().map(function (op) { - var credit = op.get('credit'), debit = op.get('debit'); - return React.DOM.tr( - {key: toKey(label, op.get('account'))}, - React.DOM.td( - null, - credit ? '\u2001' : '', - accounts[op.get('account')].label - ), - React.DOM.td(null, debit && debit(this.props.p)), - React.DOM.td(null, credit && credit(this.props.p)) - ); - }, this).concat( - React.DOM.tr( - {key: k + '-label'}, - React.DOM.td( - {colSpan: 3, style: {textAlign: 'center'}}, - label)), - React.DOM.tr( - {key: k + '-spacer'}, - React.DOM.td({colSpan: 3}, "\u00A0")) - ); - }, this) - .toArray() - ) - ) - ); - } - }); - - data.addWatch('journals', function (k, m, prev, next) { - React.render( - React.createElement(Journal, {p: next}), - document.querySelector('.journals') - ); - }); - var Chart = React.createClass({ render: function () { + var lastop = Immutable.Map( + (this.props.p.last() || Immutable.List()).map(function (op) { + return [op.get('account'), op.has('credit') ? 'credit' : 'debit']; + }) + ); return React.DOM.div( null, - React.createElement(Controls, {p: this.props.p}), - DOM.table( + React.DOM.table( {className: 'table'}, - DOM.tr( + React.DOM.tr( null, - DOM.th(), - DOM.th({className: 'text-right'}, "Debit"), - DOM.th({className: 'text-right'}, "Credit"), - DOM.th({className: 'text-right'}, "Balance")), + React.DOM.th(), + React.DOM.th({className: 'text-right'}, "Debit"), + React.DOM.th({className: 'text-right'}, "Credit"), + React.DOM.th({className: 'text-right'}, "Balance")), this.accounts().map(function (data) { - return DOM.tr( - {key: data.code}, - DOM.th(null, - data.level ? '\u2001 ' : '', - data.code, ' ', data.label), - DOM.td({className: 'text-right'}, data.debit), - DOM.td({className: 'text-right'}, data.credit), - DOM.td({className: 'text-right'}, data.debit - data.credit) + var highlight = lastop.get(data.get('code')); + return React.DOM.tr( + {key: data.get('code')}, + React.DOM.th(null, + data.get('level') ? '\u2001 ' : '', + data.get('code'), ' ', data.get('label')), + React.DOM.td({className: React.addons.classSet({ + 'text-right': true, + 'highlight-op': highlight === 'debit' + })}, data.get('debit')), + React.DOM.td({className: React.addons.classSet({ + 'text-right': true, + 'highlight-op': highlight === 'credit' + })}, data.get('credit')), + React.DOM.td({className: 'text-right'}, + data.get('debit') - data.get('credit')) ); - }) + }).toArray() ) ); }, accounts: function() { var _this = this; - var out = []; - var zero = function () { return 0; } + var zero = function () { return 0; }; var data = this.props.p; - // for each operated-on account, apply all operations and save the - // resulting (debit, credit) state - var chart = data - .filter(function (v, k) { return isEnabled(k); }) - .valueSeq() - .flatMap(function (v) { return v.get('operations'); }) - .reduce(function (acc, op) { - // update operation's account debit and credit by adding - // operation's debit and credit to them, initialize to 0 - // if not set yet - return acc - .updateIn([op.get('account'), 'debit'], 0, function (d) { - return d + op.get('debit', zero)(data); - }) - .updateIn([op.get('account'), 'credit'], 0, function (c) { - return c + op.get('credit', zero)(data); - }); - }, Immutable.Map()); - categories.forEach(function (cat) { - var current = { level: 0, label: cat.label, code: cat.code, credit: 0, debit: 0 }; - var values = accs(cat).map(function (acc) { - // If no operation has been performed on an account, 0 debit or credit - var it = chart.get(acc.code, Immutable.Map({credit: 0, debit: 0})); - var debit = it.get('debit') || 0; - var credit = it.get('credit') || 0; - current.debit += debit; - current.credit += credit; - return { - level: 1, - code: acc.code, - label: acc.label, - debit: debit, - credit: credit - } - }); - values.unshift(current); - out.push.apply(out, values); + + var totals = data.flatten(true).reduce(function (acc, op) { + return acc + .updateIn([op.get('account'), 'debit'], function (d) { + return (d || 0) + op.get('debit', zero)(data); + }) + .updateIn([op.get('account'), 'credit'], function (c) { + return (c || 0) + op.get('credit', zero)(data); + }); + }, Immutable.Map()); + + return accounts.map(function (account) { + // for each account, add sum + return account.merge( + account.get('accounts').map(function (code) { + return totals.get(code, NULL); + }).reduce(function (acc, it) { + return acc.mergeWith(function (a, b) { return a + b; }, it, NULL); + }) + ); }); - return out; } }); data.addWatch('chart', function (k, m, prev, next) { + React.render( + React.createElement(Controls, {p: next}), + document.getElementById('chart-controls')); React.render( React.createElement(Chart, {p: next}), document.querySelector('.chart-of-accounts')); }); document.addEventListener('DOMContentLoaded', function () { - var sale = 100, - tax = sale * 0.09, - total = sale + tax, - refund = sale * 0.1, - purchase = 80; - data.reset(Immutable.fromJS({ - customer_invoice: { - enabled: false, - label: "Customer Invoice ($100 + 9% tax)", - depends: [], - operations: [ - {account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: function () { return total; }}, - {account: REVENUE.SALES.code, credit: function () { return sale; }}, - {account: LIABILITIES.TAXES_PAYABLE.code, credit: function () { return tax; }} - ] - }, - // TODO: dependencies? - customer_refund: { - enabled: false, - label: "Customer Refund 10%", - depends: ['customer_invoice'], - operations: [ - {account: REVENUE.SALES.code, debit: function () { return refund; }}, - {account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: function () { return refund; }} - ] - }, - customer_payment: { - enabled: false, - label: "Customer Payment", - depends: ['customer_invoice'], - operations: [ - // TODO: depends of refund - {account: ASSETS.CASH.code, debit: function (ops) { - return ops.getIn(['customer_refund', 'enabled']) - ? total - refund - : total; - }}, - {account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: function (ops) { - return ops.getIn(['customer_refund', 'enabled']) - ? total - refund - : total; - }} - ] - }, - supplier_invoice: { - enabled: false, - label: "Supplier Invoice", - depends: [], - operations: [ - {account: EXPENSES.PURCHASES.code, debit: function () { return purchase; }}, - {account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: function () { return purchase; }}, - ] - }, - supplier_invoice_paid: { - enabled: false, - label: "Supplier Invoice Paid", - depends: ['supplier_invoice'], - operations: [ - {account: LIABILITIES.ACCOUNTS_PAYABLE.code, debit: function () { return purchase; }}, - {account: ASSETS.CASH.code, credit: function () { return purchase; }} - ] - }, - inventory_reception: { - enabled: false, - label: "Inventory Reception", - depends: ['supplier_invoice'], - // TODO: ??? - operations: [] - }, - customer_delivery: { - enabled: false, - label: "Customer Delivery", - depends: ['customer_invoice', 'inventory_reception'], - // TODO: ??? - operations: [], - }, - taxes: { - enabled: false, - label: "Pay Taxes Due", - depends: [], - // TODO: no taxes due if no customer invoice? - operations: [ - {account: LIABILITIES.TAXES_PAYABLE.code, debit: function () { return tax; }}, - {account: ASSETS.CASH.code, credit: function () { return tax; }} - ] - }, - })); + var chart = document.getElementById('chart-of-accounts'), + controls = document.createElement('div'); + controls.setAttribute('id', 'chart-controls'); + chart.insertBefore(controls, chart.lastElementChild); + data.reset(Immutable.OrderedSet()); }); - var DOM = React.DOM; - + var NULL = Immutable.Map({debit: 0, credit: 0}); var ASSETS = { code: 1, label: "Assets", @@ -330,26 +161,73 @@ label: "Expenses", PURCHASES: { code: 50100, label: "Purchases" } }; - var categories = [ASSETS, LIABILITIES, REVENUE, EXPENSES]; - var accounts = (function () { - var acs = {}; - categories.forEach(function (cat) { - acs[cat.code] = cat; - accs(cat).forEach(function (acc) { - acs[acc.code] = acc; - }); - }); - return acs; - })(); + var categories = Immutable.fromJS([ASSETS, LIABILITIES, REVENUE, EXPENSES]); + var accounts = categories.toSeq().flatMap(function (cat) { + return Immutable.Seq.of(cat.set('level', 0)).concat(cat.filter(function (v, k) { + return k.toUpperCase() === k; + }).toIndexedSeq().map(function (acc) { return acc.set('level', 1) })); + }).map(function (account) { // add accounts: Seq to each account + return account.set( + 'accounts', + Immutable.Seq.of(account.get('code')).concat( + account.toIndexedSeq().map(function (val) { + return Immutable.Map.isMap(val) && val.get('code'); + }).filter(function (val) { return !!val; }) + ) + ); + }); - - function accs(category) { - var out = []; - for(var k in category) { - if (k.toUpperCase() === k) { - out.push(category[k]); - } - } - return out; + var sale = 100, + tax = sale * 0.09, + total = sale + tax, + refund = sale * 0.1, + purchase = 80; + var operations = Immutable.fromJS([{ + label: "Customer Invoice ($100 + 9% tax)", + operations: [ + {account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: function () { return total; }}, + {account: REVENUE.SALES.code, credit: function () { return sale; }}, + {account: LIABILITIES.TAXES_PAYABLE.code, credit: function () { return tax; }} + ] + }, { + label: "Customer Refund 10%", + operations: [ + {account: REVENUE.SALES.code, debit: function () { return refund; }}, + {account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: function () { return refund; }} + ] + }, { + label: "Customer Payment", + operations: [ + // TODO: depends of refund + {account: ASSETS.CASH.code, debit: function (ops) { + return ops.contains(operations.getIn(['customer_refund', 'operations'])) + ? total - refund + : total; + }}, + {account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: function (ops) { + return ops.contains(operations.getIn(['customer_refund', 'operations'])) + ? total - refund + : total; + }} + ] + }, { + label: "Supplier Invoice", + operations: [ + {account: EXPENSES.PURCHASES.code, debit: function () { return purchase; }}, + {account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: function () { return purchase; }} + ] + }, { + label: "Supplier Invoice Paid", + operations: [ + {account: LIABILITIES.ACCOUNTS_PAYABLE.code, debit: function () { return purchase; }}, + {account: ASSETS.CASH.code, credit: function () { return purchase; }} + ] + }, { + label: "Pay Taxes Due", + operations: [ + {account: LIABILITIES.TAXES_PAYABLE.code, debit: function () { return tax; }}, + {account: ASSETS.CASH.code, credit: function () { return tax; }} + ] } + ]); })(); diff --git a/index.rst b/index.rst index 8c9193ae7..ab0865ffc 100644 --- a/index.rst +++ b/index.rst @@ -69,6 +69,28 @@ them being consumed for the company to "work". What is owned has been financed through debts to reimburse or acquired assets (profits, capical). +Chart of Accounts +================= + +The **chart of accounts** lists all the accounts used by the company, whether +they are balance sheet accounts (assets and liabilities) or P&L accounts +(revenues and expenses), and provides their state at a given moment (their +credit, debit and balance). + +The accounts are used to organize and classify the finances of the company in +order to better understand its state and health, and the chart of accounts can +be used to get a snapshot of a company's financial period: because it includes +P&L, a chart of accounts is also generally viewed over a specific period. + +.. rst-class:: force-right + +Balance = debit - credit +------------------------ + +.. h:div:: chart-of-accounts + + Requires javascript + Journals ======== @@ -141,24 +163,6 @@ T-accounts for the transactions needs javascript -Chart of Accounts -================= - -The **chart of accounts** lists all balance sheet (assets, liabilities) and -P&L (revenue, expense) accounts. These accounts are used to organize and -classify the finances of the company to better understand the company's -financial state, and the chart can be used to get a snapshot of a company's -financial period. - -.. rst-class:: force-right - -Balance = debit - credit ------------------------- - -.. h:div:: chart-of-accounts - - Requires javascript - Debit and credit ================