[CHG] chart of accounts interaction impl

* move chart of accounts higher in document
* rewrite text a bit
* split controls to left-hand column
* remove dependencies between operations
* highlight result of last-applied operation (not very good looking atm)
This commit is contained in:
Xavier Morel 2015-02-19 12:17:50 +01:00
parent 78765edae5
commit 60822587b7
3 changed files with 181 additions and 292 deletions

View File

@ -77,3 +77,10 @@ li > p {
.accounts-table dt span:last-child { .accounts-table dt span:last-child {
font-style: normal; font-style: normal;
} }
.highlight-op {
background-color: #dce6f8;
}
.chart-of-accounts .highlight-op {
background-color: #030035;
}

View File

@ -18,296 +18,127 @@
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-'); 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) { function isEnabled(transaction) {
var item = data.deref().get(transaction); var item = data.deref().get(transaction);
return item.get('enabled') && isFulfilled(item.get('depends')); return item.get('enabled');
} }
var Controls = React.createClass({ var Controls = React.createClass({
getInitialState: function () {
return { folded: true };
},
toggle: function () {
this.setState({folded: !this.state.folded});
},
render: function () { render: function () {
return React.DOM.div( var _this = this;
null, return React.DOM.div(null, operations.map(function (op) {
React.DOM.h4( var label = op.get('label'), operations = op.get('operations');
{ onClick: this.toggle, style: { cursor: 'pointer' } }, return React.DOM.label(
this.state.folded ? "\u25B8 " : "\u25BE ", {
"Operations"), key: toKey(label),
this.state.folded ? undefined : this.props.p.map(function (v, k) { style: {display: 'block'},
return React.DOM.label( className: (operations === _this.props.p.last() && 'highlight-op')
{key: k, style: {display: 'block' } }, },
React.DOM.input({ React.DOM.input({
type: 'checkbox', type: 'checkbox',
disabled: !isFulfilled(v.get('depends')), checked: _this.props.p.contains(operations),
checked: v.get('enabled'), onChange: function (e) {
onChange: function () { if (e.target.checked) {
data.swap(function (d) { data.swap(function (ops) {
return d.updateIn( return ops.add(operations);
[k, 'enabled'], });
function (check) { return !check; }); } else {
data.swap(function (ops) {
return ops.remove(operations);
}); });
} }
}), }
" ", }),
v.get('label') " ",
); label
}, this).toArray() );
); }).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({ var Chart = React.createClass({
render: function () { 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( return React.DOM.div(
null, null,
React.createElement(Controls, {p: this.props.p}), React.DOM.table(
DOM.table(
{className: 'table'}, {className: 'table'},
DOM.tr( React.DOM.tr(
null, null,
DOM.th(), React.DOM.th(),
DOM.th({className: 'text-right'}, "Debit"), React.DOM.th({className: 'text-right'}, "Debit"),
DOM.th({className: 'text-right'}, "Credit"), React.DOM.th({className: 'text-right'}, "Credit"),
DOM.th({className: 'text-right'}, "Balance")), React.DOM.th({className: 'text-right'}, "Balance")),
this.accounts().map(function (data) { this.accounts().map(function (data) {
return DOM.tr( var highlight = lastop.get(data.get('code'));
{key: data.code}, return React.DOM.tr(
DOM.th(null, {key: data.get('code')},
data.level ? '\u2001 ' : '', React.DOM.th(null,
data.code, ' ', data.label), data.get('level') ? '\u2001 ' : '',
DOM.td({className: 'text-right'}, data.debit), data.get('code'), ' ', data.get('label')),
DOM.td({className: 'text-right'}, data.credit), React.DOM.td({className: React.addons.classSet({
DOM.td({className: 'text-right'}, data.debit - data.credit) '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() { accounts: function() {
var _this = this; var _this = this;
var out = []; var zero = function () { return 0; };
var zero = function () { return 0; }
var data = this.props.p; var data = this.props.p;
// for each operated-on account, apply all operations and save the
// resulting (debit, credit) state var totals = data.flatten(true).reduce(function (acc, op) {
var chart = data return acc
.filter(function (v, k) { return isEnabled(k); }) .updateIn([op.get('account'), 'debit'], function (d) {
.valueSeq() return (d || 0) + op.get('debit', zero)(data);
.flatMap(function (v) { return v.get('operations'); }) })
.reduce(function (acc, op) { .updateIn([op.get('account'), 'credit'], function (c) {
// update operation's account debit and credit by adding return (c || 0) + op.get('credit', zero)(data);
// operation's debit and credit to them, initialize to 0 });
// if not set yet }, Immutable.Map());
return acc
.updateIn([op.get('account'), 'debit'], 0, function (d) { return accounts.map(function (account) {
return d + op.get('debit', zero)(data); // for each account, add sum
}) return account.merge(
.updateIn([op.get('account'), 'credit'], 0, function (c) { account.get('accounts').map(function (code) {
return c + op.get('credit', zero)(data); return totals.get(code, NULL);
}); }).reduce(function (acc, it) {
}, Immutable.Map()); return acc.mergeWith(function (a, b) { return a + b; }, it, NULL);
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);
}); });
return out;
} }
}); });
data.addWatch('chart', function (k, m, prev, next) { data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, {p: next}),
document.getElementById('chart-controls'));
React.render( React.render(
React.createElement(Chart, {p: next}), React.createElement(Chart, {p: next}),
document.querySelector('.chart-of-accounts')); document.querySelector('.chart-of-accounts'));
}); });
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var sale = 100, var chart = document.getElementById('chart-of-accounts'),
tax = sale * 0.09, controls = document.createElement('div');
total = sale + tax, controls.setAttribute('id', 'chart-controls');
refund = sale * 0.1, chart.insertBefore(controls, chart.lastElementChild);
purchase = 80; data.reset(Immutable.OrderedSet());
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 DOM = React.DOM; var NULL = Immutable.Map({debit: 0, credit: 0});
var ASSETS = { var ASSETS = {
code: 1, code: 1,
label: "Assets", label: "Assets",
@ -330,26 +161,73 @@
label: "Expenses", label: "Expenses",
PURCHASES: { code: 50100, label: "Purchases" } PURCHASES: { code: 50100, label: "Purchases" }
}; };
var categories = [ASSETS, LIABILITIES, REVENUE, EXPENSES]; var categories = Immutable.fromJS([ASSETS, LIABILITIES, REVENUE, EXPENSES]);
var accounts = (function () { var accounts = categories.toSeq().flatMap(function (cat) {
var acs = {}; return Immutable.Seq.of(cat.set('level', 0)).concat(cat.filter(function (v, k) {
categories.forEach(function (cat) { return k.toUpperCase() === k;
acs[cat.code] = cat; }).toIndexedSeq().map(function (acc) { return acc.set('level', 1) }));
accs(cat).forEach(function (acc) { }).map(function (account) { // add accounts: Seq<AccountCode> to each account
acs[acc.code] = acc; return account.set(
}); 'accounts',
}); Immutable.Seq.of(account.get('code')).concat(
return acs; account.toIndexedSeq().map(function (val) {
})(); return Immutable.Map.isMap(val) && val.get('code');
}).filter(function (val) { return !!val; })
)
);
});
var sale = 100,
function accs(category) { tax = sale * 0.09,
var out = []; total = sale + tax,
for(var k in category) { refund = sale * 0.1,
if (k.toUpperCase() === k) { purchase = 80;
out.push(category[k]); var operations = Immutable.fromJS([{
} label: "Customer Invoice ($100 + 9% tax)",
} operations: [
return out; {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; }}
]
} }
]);
})(); })();

View File

@ -69,6 +69,28 @@ them being consumed for the company to "work".
What is owned has been financed through debts to reimburse or acquired What is owned has been financed through debts to reimburse or acquired
assets (profits, capical). 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 Journals
======== ========
@ -141,24 +163,6 @@ T-accounts for the transactions
needs javascript 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 Debit and credit
================ ================