[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
This commit is contained in:
Xavier Morel 2015-02-24 12:02:29 +01:00
parent 1570f58835
commit e79e32721c
5 changed files with 175 additions and 250 deletions

View File

@ -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',

172
_static/entries.js Normal file
View File

@ -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}
]
}
]);
}());

View File

@ -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'));
});
})();

View File

@ -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')

View File

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