[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 {
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('-');
}
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<AccountCode> 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; }}
]
}
]);
})();

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
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
================