[MERGE] Forward-port of branch 12.0 to 13.0

This commit is contained in:
Antoine Vandevenne (anv)
2021-05-04 16:31:06 +02:00
3332 changed files with 40884 additions and 59758 deletions
+175
View File
@@ -0,0 +1,175 @@
/* global Immutable, React */
(function () {
// NOTE: used by memento.rst
'use strict';
function highlight(primary, secondary) {
return {
className: React.addons.classSet({
related: primary,
secondary: secondary
})
};
}
var AccountsTable = React.createClass({
render: function () {
return React.DOM.div(
{ style: { marginTop: '0' } },
React.DOM.div(// P&L
highlight(this.props.current === 'p-l'),
React.DOM.h4(null, "Profit & Loss"),
React.DOM.div(
null,
React.DOM.h5(
highlight(null, this.props.current === 'retained'),
"Net Profit"),
React.DOM.div(
highlight(null, this.props.current === 'gross-profit'),
React.DOM.h5(
highlight(this.props.current === 'gross-profit'),
"Gross Profit"),
React.DOM.dl(
null,
React.DOM.dt(null, "Revenue"),
React.DOM.dd(
null,
"Revenue"
),
React.DOM.dt(null, "Less ", "Costs of Revenue"),
React.DOM.dd(
null,
"Cost of Goods Sold"
)
)
),
React.DOM.div(
highlight(this.props.current === 'opex'),
React.DOM.h5(null, "Operating Income or Loss"),
React.DOM.dl(
null,
React.DOM.dt(
null,
"Less ", "Operating Expenses"),
React.DOM.dd(
null,
"R&D", React.DOM.br(),
"Sales, General & Administrative"
)
)
),
React.DOM.dl(
null,
React.DOM.dt(null, "Plus ", "Other Income"),
React.DOM.dd(
null,
"Foreign Exchange Gains", React.DOM.br(),
"Asset write-downs"
),
React.DOM.dt(
null,
"Less ", "Other Expenses"),
React.DOM.dd(
null,
"Interest on debt", React.DOM.br(),
"Depreciation"
)
)
)
),
React.DOM.div(//Balance Sheet
highlight(this.props.current === 'balance'),
React.DOM.h4(null, "Balance Sheet"),
React.DOM.div(
null,
React.DOM.h5(null, "Net Assets"),
React.DOM.div(
null,
React.DOM.h5(highlight(this.props.current === 'assets'), "Total Assets"),
React.DOM.dl(
highlight(null, this.props.current === 'assets'),
React.DOM.dt(null, "Current Assets"),
React.DOM.dd(
null,
"Cash & Bank Accounts", React.DOM.br(),
"Accounts Receivable", React.DOM.br(),
"Deferred Tax Assets"
),
React.DOM.dt(null, "Plus ", "Non-current Assets"),
React.DOM.dd(
null,
"Land & buildings", React.DOM.br(),
"Intangible Assets"
)
)
),
React.DOM.dl(
highlight(this.props.current === 'liabilities'),
React.DOM.dt(null, "Less ", "Current Liabilities"),
React.DOM.dd(
null,
"Accounts Payable", React.DOM.br(),
"Deferred Revenue", React.DOM.br(),
"Deferred Tax Liabilities"),
React.DOM.dt(null, "Less ", "Non-current liabilities"),
React.DOM.dd(
null,
"Long-term loans")
)
),
React.DOM.div(
highlight(null, this.props.current === 'equity'),
React.DOM.h5(highlight(this.props.current === 'equity'), "Total Equity"),
React.DOM.dl(
null,
React.DOM.dt(null, "Equity"),
React.DOM.dd(
null,
"Common Stock"
),
React.DOM.dt(
highlight(this.props.current === 'retained'),
"Plus ", "Retained Earnings"
)
)
)
)
);
}
});
document.addEventListener('DOMContentLoaded', function () {
var target = document.querySelector('.accounts-table');
if (!target) { return; }
function render(current) {
React.render(
React.createElement(AccountsTable, { current: current }),
target);
}
var list = document.querySelectorAll('.intro-list p');
Array.prototype.forEach.call(list, function (node) {
node.addEventListener('mouseover', function (e) {
if (!e.currentTarget.contains(e.target)) { return; }
e.currentTarget.classList.add('secondary');
render(e.currentTarget.className.split(/\s+/).reduce(function (acc, cls) {
if (acc) { return acc; }
var m = /^intro-(.*)$/.exec(cls);
return m && m[1];
}, null));
});
node.addEventListener('mouseout', function (e) {
// mouseout always precedes mousenter even when going into a
// child element. Since re-render should be pretty fast (just
// setting or unsetting a pair of classes) don't try to avoid
// any thrashing, things should be fast enough either way. If
// they're not, batch operations on requestAnimationFrame
// instead.
e.currentTarget.classList.remove('secondary');
render(null);
});
});
render(null);
});
})();
+56
View File
@@ -0,0 +1,56 @@
function findAncestor(element, name) {
name = name.toUpperCase();
while(element && element.nodeName.toUpperCase() !== name) {
element = element.parentElement;
};
return element;
}
function createAtom(val, options) {
var watchers = {};
var validator = options && options.validator || function () { return true; };
function transition(next) {
if (!validator(next)) {
var err = new Error(next + " failed validation");
err.name = "AssertionError";
throw err;
}
var prev = val;
val = next;
Object.keys(watchers).forEach(function (k) {
watchers[k](k, atom, prev, next);
});
}
var atom = {
addWatch: function (key, fn) {
watchers[key] = fn;
},
removeWatch: function (key) {
delete watchers[key];
},
swap: function (fn) {
var args = [val].concat([].slice.call(arguments, 1));
transition(fn.apply(null, args));
},
reset: function (v) {
transition(v);
},
deref: function () {
return val;
},
toString: function () {
return "Atom(" + JSON.stringify(val) + ")";
}
};
return atom;
}
+372
View File
@@ -0,0 +1,372 @@
/* global Immutable, React */
/* global createAtom */
(function () {
// NOTE: used by memento.rst
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
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 === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations)
});
});
} else {
data.swap(function (d) {
return d.set('active', null) // keep visible in state map
.update('operations', function (ops) {
return ops.remove(operations);
})
});
}
}
}),
" ",
label
);
}));
}
});
var Chart = React.createClass({
render: function () {
var lastop = Immutable.Map(
(this.props.p.get('active') || Immutable.List()).map(function (op) {
return [op.get('account'), op.has('credit') ? 'credit' : 'debit'];
})
);
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th({ className: 'text-right' }, "Debit"),
React.DOM.th({ className: 'text-right' }, "Credit"),
React.DOM.th({ className: 'text-right' }, "Balance"))
),
React.DOM.tbody(
null,
this.accounts().map(function (data) {
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'
})
}, format(data.get('debit'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'credit'
})
}, format(data.get('credit'))),
React.DOM.td(
{ className: 'text-right' },
((data.get('debit') || data.get('credit'))
? format(data.get('debit') - data.get('credit'), 0)
: '')
)
);
})
)
)
);
},
accounts: function () {
var data = this.props.p.get('operations');
var totals = data.toIndexedSeq().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);
})
);
});
}
});
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 chart = findAncestor(document.querySelector('.chart-of-accounts'), 'section');
if (!chart) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-controls');
chart.insertBefore(controls, chart.lastElementChild);
data.reset(Immutable.Map({
// last-selected operation
active: null,
// set of all currently enabled operations
operations: Immutable.OrderedSet()
}));
});
var NULL = Immutable.Map({ debit: 0, credit: 0 });
var ASSETS = {
code: 1,
label: "Assets",
BANK: { code: 11000, label: "Cash" },
ACCOUNTS_RECEIVABLE: { code: 13100, label: "Accounts Receivable" },
STOCK: { code: 14000, label: "Inventory" },
STOCK_OUT: { code: 14600, label: "Goods Issued Not Invoiced" },
BUILDINGS: { code: 17200, label: "Buildings" },
DEPRECIATION: { code: 17800, label: "Accumulated Depreciation" },
TAXES_PAID: { code: 19000, label: "Deferred Tax Assets" }
};
var LIABILITIES = {
code: 2,
label: "Liabilities",
ACCOUNTS_PAYABLE: { code: 21000, label: "Accounts Payable" },
DEFERRED_REVENUE: { code: 22300, label: "Deferred Revenue" },
STOCK_IN: { code: 23000, label: "Goods Received Not Purchased" },
TAXES_PAYABLE: { code: 26200, label: "Deferred Tax Liabilities" }
};
var EQUITY = {
code: 3,
label: "Equity",
CAPITAL: { code: 31000, label: "Common Stock" }
};
var REVENUE = {
code: 4,
label: "Revenue",
SALES: { code: 41000, label: "Goods" },
SALES_SERVICES: { code: 42000, label: "Services" }
};
var EXPENSES = {
code: 5,
label: "Expenses",
GOODS_SOLD: { code: 51100, label: "Cost of Goods Sold" },
DEPRECIATION: { code: 52500, label: "Other Operating Expenses" },
PRICE_DIFFERENCE: { code: 53000, label: "Price Difference" }
};
var categories = Immutable.fromJS([ASSETS, LIABILITIES, EQUITY, REVENUE, EXPENSES], function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
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; })
)
);
});
var sale = 100,
cor = 50,
cor_tax = cor * 0.09,
tax = sale * 0.09,
total = sale + tax,
refund = sale,
refund_tax = refund * 0.09,
purchase = 52,
purchase_tax = 52 * 0.09;
var operations = Immutable.fromJS([{
label: "Company Incorporation (Initial Capital $1,000)",
operations: [
{ account: ASSETS.BANK.code, debit: constant(1000) },
{ account: EQUITY.CAPITAL.code, credit: constant(1000) }
]
}, {
label: "Customer Invoice ($100 + 9% tax) & Shipping of the Goods",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total) },
{ account: EXPENSES.GOODS_SOLD.code, debit: constant(cor) },
{ account: REVENUE.SALES.code, credit: constant(sale) },
{ account: ASSETS.STOCK_OUT.code, credit: constant(cor) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax) }
]
}, {
label: "Goods Shipment to Customer",
operations: [
{ account: ASSETS.STOCK_OUT.code, debit: constant(cor) },
{ account: ASSETS.STOCK.code, credit: constant(cor) }
]
}, {
id: 'refund',
label: "Customer Refund",
operations: [
{ account: REVENUE.SALES.code, debit: constant(refund) },
{ account: LIABILITIES.TAXES_PAYABLE.code, debit: constant(refund_tax) },
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: constant(refund + refund_tax) }
]
}, {
label: "Customer Payment",
operations: [
{
account: ASSETS.BANK.code, debit: function (ops) {
var refund_op = operations.find(function (op) {
return op.get('id') === 'refund';
});
return ops.contains(refund_op.get('operations'))
? total - (refund + refund_tax)
: total;
}
},
{
account: ASSETS.ACCOUNTS_RECEIVABLE.code, credit: function (ops) {
var refund_op = operations.find(function (op) {
return op.get('id') === 'refund';
});
return ops.contains(refund_op.get('operations'))
? total - (refund + refund_tax)
: total;
}
}
]
}, {
label: "Vendor Goods Received (Purchase Order: $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, credit: constant(cor) },
{ account: ASSETS.STOCK.code, debit: constant(cor) },
]
}, {
label: "Vendor Bill (Invoice: $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, debit: constant(cor) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(cor_tax) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(cor + cor_tax) },
]
}, {
label: "Vendor Bill (Invoice: $52 but PO $50)",
operations: [
{ account: EXPENSES.PRICE_DIFFERENCE.code, debit: constant(purchase - cor) },
{ account: LIABILITIES.STOCK_IN.code, debit: constant(cor) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(purchase_tax) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(purchase + purchase_tax) },
]
}, {
label: "Vendor Bill Paid ($52 + 9% tax)",
operations: [
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, debit: constant(purchase + purchase_tax) },
{ account: ASSETS.BANK.code, credit: constant(purchase + purchase_tax) }
]
}, {
label: "Acquire a building (purchase contract)",
operations: [
{ account: ASSETS.BUILDINGS.code, debit: constant(3000) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(300) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(3300) }
]
}, {
label: "Pay for building",
operations: [
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, debit: constant(3300) },
{ account: ASSETS.BANK.code, credit: constant(3300) }
]
}, {
label: "Yearly Asset Depreciation (10% per year)",
operations: [
{ account: EXPENSES.DEPRECIATION.code, debit: constant(300) },
{ account: ASSETS.DEPRECIATION.code, credit: constant(300) }
]
}, {
label: "Customer Invoice (3 years service contract, $300)",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total * 3) },
{ account: LIABILITIES.DEFERRED_REVENUE.code, credit: constant(sale * 3) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax * 3) }
]
}, {
label: "Revenue Recognition (each year, including first)",
operations: [
{ account: LIABILITIES.DEFERRED_REVENUE.code, debit: constant(sale) },
{ account: REVENUE.SALES_SERVICES.code, credit: constant(sale) },
]
}, {
id: 'pay_taxes',
label: "Pay Taxes Due",
operations: [
{
account: LIABILITIES.TAXES_PAYABLE.code, debit: function (ops) {
var this_ops = operations.find(function (op) {
return op.get('id') === 'pay_taxes';
}).get('operations');
return ops.filter(function (_ops) {
return _ops !== this_ops;
}).flatten(true).filter(function (op) {
return op.get('account') === LIABILITIES.TAXES_PAYABLE.code
}).reduce(function (acc, op) {
return acc + op.get('credit', zero)(ops) - op.get('debit', zero)(ops);
}, 0);
}
},
{
account: ASSETS.BANK.code, credit: function (ops) {
return operations.find(function (op) {
return op.get('id') === 'pay_taxes';
}).getIn(['operations', 0, 'debit'])(ops);
}
}
]
}
]);
function constant(val) { return function () { return val; }; }
var zero = constant(0);
function format(val, def) {
if (!val) { return def === undefined ? '' : def; }
if (val % 1 === 0) { return val; }
return val.toFixed(2);
}
})();
+276
View File
@@ -0,0 +1,276 @@
/* global Immutable, React */
/* global createAtom */
(function () {
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
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 === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations);
});
});
} else {
data.swap(function (d) {
return d.set('active', null) // keep visible in state map
.update('operations', function (ops) {
return ops.remove(operations);
});
});
}
}
}),
" ",
label
);
}));
}
});
var Chart = React.createClass({
render: function () {
var lastop = Immutable.Map(
(this.props.p.get('active') || Immutable.List()).map(function (op) {
return [op.get('account'), op.has('credit') ? 'credit' : 'debit'];
})
);
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th({ className: 'text-right' }, "Debit"),
React.DOM.th({ className: 'text-right' }, "Credit"),
React.DOM.th({ className: 'text-right' }, "Balance"))
),
React.DOM.tbody(
null,
this.accounts().map(function (data) {
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'
})
}, format(data.get('debit'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'credit'
})
}, format(data.get('credit'))),
React.DOM.td(
{ className: 'text-right' },
((data.get('debit') || data.get('credit'))
? format(data.get('debit') - data.get('credit'), 0)
: '')
)
);
})
)
)
);
},
accounts: function () {
var data = this.props.p.get('operations');
var totals = data.toIndexedSeq().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);
})
);
});
}
});
data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, { p: next }),
document.getElementById('chart-controls-anglo-saxon'));
React.render(
React.createElement(Chart, { p: next }),
document.querySelector('.valuation-chart-anglo-saxon'));
});
document.addEventListener('DOMContentLoaded', function () {
var chart = document.querySelector('.valuation-chart-anglo-saxon');
if (!chart) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-controls-anglo-saxon');
chart.parentNode.insertBefore(controls, chart);
data.reset(Immutable.Map({
// last-selected operation
active: null,
// set of all currently enabled operations
operations: Immutable.OrderedSet()
}));
});
var NULL = Immutable.Map({ debit: 0, credit: 0 });
var ASSETS = {
code: 1,
label: "Assets",
BANK: { code: 11000, label: "Cash" },
ACCOUNTS_RECEIVABLE: { code: 13100, label: "Accounts Receivable" },
STOCK: { code: 14000, label: "Inventory" },
RAW_MATERIALS: { code: 14100, label: "Raw Materials Inventory" },
STOCK_OUT: { code: 14600, label: "Goods Issued Not Invoiced" },
TAXES_PAID: { code: 19000, label: "Deferred Tax Assets" }
};
var LIABILITIES = {
code: 2,
label: "Liabilities",
ACCOUNTS_PAYABLE: { code: 21000, label: "Accounts Payable" },
STOCK_IN: { code: 23000, label: "Goods Received Not Purchased" },
TAXES_PAYABLE: { code: 26200, label: "Deferred Tax Liabilities" }
};
var EQUITY = {
code: 3,
label: "Equity",
CAPITAL: { code: 31000, label: "Common Stock" }
};
var REVENUE = {
code: 4,
label: "Revenue",
SALES: { code: 41000, label: "Goods" },
};
var EXPENSES = {
code: 5,
label: "Expenses",
GOODS_SOLD: { code: 51100, label: "Cost of Goods Sold" },
MANUFACTURING_OVERHEAD: { code: 52000, label: "Manufacturing Overhead" },
PRICE_DIFFERENCE: { code: 53000, label: "Price Difference" }
};
var categories = Immutable.fromJS([ASSETS, LIABILITIES, EQUITY, REVENUE, EXPENSES], function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
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; })
)
);
});
var sale = 100,
cor = 50,
cor_tax = cor * 0.09,
tax = sale * 0.09,
total = sale + tax,
purchase = 52,
purchase_tax = 52 * 0.09;
var operations = Immutable.fromJS([{
label: "Vendor Bill (PO $50, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Supplier Goods Reception (PO $50, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, credit: constant(50) },
{ account: ASSETS.STOCK.code, debit: constant(50) },
]
}, {
label: "Vendor Bill (PO $48, Invoice $50)",
operations: [
{ account: EXPENSES.PRICE_DIFFERENCE.code, debit: constant(2) },
{ account: LIABILITIES.STOCK_IN.code, debit: constant(48) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Supplier Goods Reception (PO $48, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, credit: constant(48) },
{ account: ASSETS.STOCK.code, debit: constant(48) },
]
}, {
label: "Customer Invoice",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total) },
{ account: EXPENSES.GOODS_SOLD.code, debit: constant(cor) },
{ account: REVENUE.SALES.code, credit: constant(sale) },
{ account: ASSETS.STOCK_OUT.code, credit: constant(cor) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax) }
]
}, {
label: "Customer Shipping",
operations: [
{ account: ASSETS.STOCK_OUT.code, debit: constant(cor) },
{ account: ASSETS.STOCK.code, credit: constant(cor) }
]
}, {
label: "Production Order",
operations: [
{ account: ASSETS.STOCK.code, debit: constant(50) },
{ account: EXPENSES.MANUFACTURING_OVERHEAD.code, debit: constant(2) },
{ account: ASSETS.RAW_MATERIALS.code, credit: constant(52) }
]
}]);
function constant(val) { return function () { return val; }; }
var zero = constant(0);
function format(val, def) {
if (!val) { return def === undefined ? '' : def; }
if (val % 1 === 0) { return val; }
return val.toFixed(2);
}
})();
+265
View File
@@ -0,0 +1,265 @@
/* global Immutable, React */
/* global createAtom */
(function () {
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
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 === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations);
});
});
} else {
data.swap(function (d) {
return d.set('active', null) // keep visible in state map
.update('operations', function (ops) {
return ops.remove(operations);
})
});
}
}
}),
" ",
label
);
}));
}
});
var Chart = React.createClass({
render: function () {
var lastop = Immutable.Map(
(this.props.p.get('active') || Immutable.List()).map(function (op) {
return [op.get('account'), op.has('credit') ? 'credit' : 'debit'];
})
);
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th({ className: 'text-right' }, "Debit"),
React.DOM.th({ className: 'text-right' }, "Credit"),
React.DOM.th({ className: 'text-right' }, "Balance"))
),
React.DOM.tbody(
null,
this.accounts().map(function (data) {
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'
})
}, format(data.get('debit'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'credit'
})
}, format(data.get('credit'))),
React.DOM.td(
{ className: 'text-right' },
((data.get('debit') || data.get('credit'))
? format(data.get('debit') - data.get('credit'), 0)
: '')
)
);
})
)
)
);
},
accounts: function () {
var data = this.props.p.get('operations');
var totals = data.toIndexedSeq().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);
})
);
});
}
});
data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, { p: next }),
document.getElementById('chart-controls-continental'));
React.render(
React.createElement(Chart, { p: next }),
document.querySelector('.valuation-chart-continental'));
});
document.addEventListener('DOMContentLoaded', function () {
var chart = document.querySelector('.valuation-chart-continental');
if (!chart) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-controls-continental');
chart.parentNode.insertBefore(controls, chart);
data.reset(Immutable.Map({
// last-selected operation
active: null,
// set of all currently enabled operations
operations: Immutable.OrderedSet()
}));
});
var NULL = Immutable.Map({ debit: 0, credit: 0 });
var ASSETS = {
code: 1,
label: "Assets",
BANK: { code: 11000, label: "Cash" },
ACCOUNTS_RECEIVABLE: { code: 13100, label: "Accounts Receivable" },
STOCK: { code: 14000, label: "Inventory" },
RAW_MATERIALS: { code: 14100, label: "Raw Materials Inventory" },
TAXES_PAID: { code: 19000, label: "Deferred Tax Assets" }
};
var LIABILITIES = {
code: 2,
label: "Liabilities",
ACCOUNTS_PAYABLE: { code: 21000, label: "Accounts Payable" },
TAXES_PAYABLE: { code: 26200, label: "Deferred Tax Liabilities" }
};
var EQUITY = {
code: 3,
label: "Equity",
CAPITAL: { code: 31000, label: "Common Stock" }
};
var REVENUE = {
code: 4,
label: "Revenue",
SALES: { code: 41000, label: "Goods" },
};
var EXPENSES = {
code: 5,
label: "Expenses",
PURCHASED_GOODS: { code: 51000, label: "Purchased Goods" },
PURCHASED_SERVICES: { code: 52000, label: "Purchased Services" },
INVENTORY_VARIATIONS: { code: 58000, label: "Inventory Variations" },
OTHER_OPERATING_EXPENSES: { code: 59000, label: "Other Operating Expenses" },
};
var categories = Immutable.fromJS([ASSETS, LIABILITIES, EQUITY, REVENUE, EXPENSES], function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
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; })
)
);
});
var sale = 100,
cor = 50,
cor_tax = cor * 0.09,
tax = sale * 0.09,
total = sale + tax,
purchase = 52,
purchase_tax = 52 * 0.09;
var operations = Immutable.fromJS([{
label: "Vendor Invoice (PO €50, Invoice €50)",
operations: [
{ account: EXPENSES.PURCHASED_GOODS.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Vendor Goods Reception (PO €50, Invoice €50)",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, credit: constant(50) },
{ account: ASSETS.STOCK.code, debit: constant(50) },
]
}, {
label: "Vendor Invoice (PO €48, Invoice €50)",
operations: [
{ account: EXPENSES.PURCHASED_GOODS.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Vendor Goods Reception (PO €48, Invoice €50)",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, credit: constant(48) },
{ account: ASSETS.STOCK.code, debit: constant(48) },
]
}, {
label: "Customer Invoice (€100 + 9% tax)",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total) },
{ account: REVENUE.SALES.code, credit: constant(sale) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax) }
]
}, {
label: "Customer Shipping",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, debit: constant(cor) },
{ account: ASSETS.STOCK.code, credit: constant(cor) }
]
}]);
function constant(val) { return function () { return val; }; }
var zero = constant(0);
function format(val, def) {
if (!val) { return def === undefined ? '' : def; }
if (val % 1 === 0) { return val; }
return val.toFixed(2);
}
})();
+297
View File
@@ -0,0 +1,297 @@
/* global Immutable, React */
/* global createAtom, findAncestor */
(function () {
'use strict';
// NOTE: memento.rst
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_section = findAncestor(document.querySelector('.journal-entries'), 'section');
if (!entries_section) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'entries-control');
entries_section.insertBefore(controls, entries_section.lastElementChild);
data.reset(entries.first());
});
var Controls = React.createClass({
render: function () {
var _this = this;
return React.DOM.div(
null,
entries.map(function (entry, index) {
return React.DOM.label(
{
key: index,
style: { display: 'block' },
},
React.DOM.input({
type: 'radio',
checked: Immutable.is(entry, this.props.entry),
onChange: function (e) {
data.reset(entry);
}
}),
' ',
entry.get('title')
);
}, this),
this.props.entry && React.DOM.p(null, this.props.entry.get('help'))
);
}
});
var FormatEntry = React.createClass({
render: function () {
var entry = this.props.entry;
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm d-c-table' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th(null, "Debit"),
React.DOM.th(null, "Credit")
)
),
React.DOM.tbody(
null,
this.render_rows()
)
),
React.createElement(Listing, {
heading: "Explanation",
items: entry && entry.get('explanation')
}),
React.createElement(Listing, {
heading: "Configuration",
items: entry && entry.get('configuration')
})
);
},
render_rows: function () {
if (!this.props.entry) { return; }
return this.props.entry.get('operations').map(this.render_row);
},
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(null, entry.get('debit')),
React.DOM.td(null, entry.get('credit'))
);
}
});
var Listing = React.createClass({
render: function () {
if (!this.props.items || this.props.items.isEmpty()) {
return React.DOM.div();
}
var items = this.props.items, epilog = Immutable.List();
var idx = items.indexOf(null);
if (idx !== -1) {
epilog = items.slice(idx + 1);
items = items.take(idx);
}
return React.DOM.div(
{ className: 'entries-listing' },
React.DOM.h4(null, this.props.heading, ':'),
React.DOM.ul(
null,
items.map(function (item, index) {
return React.DOM.li({ key: index }, item);
})
),
epilog.map(function (item, index) {
return React.DOM.p({ key: index }, item);
})
);
}
});
var entries = Immutable.fromJS([
{
title: "Company Incorporation",
operations: [
{ account: 'Assets: Cash', debit: 1000 },
{ account: 'Equity: Common Stock', credit: 1000 }
],
explanation: [
"The company receives $1,000 in cash",
"Shares worth of $1,000 belong to the founders",
null,
"The initial capital can be cash, but could also be intellectual property, goodwill from a previous company, licences, know how, etc…",
"Sometimes, capital is not released immediately, accounts for \"capital to be released\" may be necessary."
],
configuration: []
}, {
title: "Customer Invoice ($100 + 9% tax)",
operations: [
{ account: 'Revenue: Goods', credit: 100 },
{ account: 'Liabilities: Deferred Tax Liabilities', credit: 9 },
{ account: 'Assets: Accounts Receivable', debit: 109 },
{ account: 'Assets: Inventory', credit: 50 },
{ account: 'Expenses: Cost of Goods Sold', debit: 50 }
],
explanation: [
"Revenues increase by $100",
"A tax to pay at the end of the month of $9",
"The customer owes $109",
"The inventory is decreased by $50 (shipping of the goods)",
"The cost of goods sold decreases the gross profit by $50"
],
configuration: [
"Revenue: defined on the product, or the product category if not on the product, field Income Account",
"Defered Tax Liabilities: defined on the tax used on the invoice line",
"Accounts Receivable: defined on the customer (property)",
"Inventory: defined on the category of the related product (property)",
"Expenses: defined on the product, or the category of product (property)",
null,
"The fiscal position used on the invoice may have a rule that replaces the Income Account or the tax defined on the product by another one."
]
}, {
title: "Customer payment",
operations: [
{ account: 'Assets: Cash', debit: 109 },
{ account: 'Assets: Accounts Receivable', credit: 109 }
],
explanation: [
"The company receives $109 in cash",
"The customer owes $109 less"
],
configuration: [
"Cash: defined on the journal used when registering the payment, fields Default Credit Account and Default Debit Account",
"Accounts Receivable: defined on the customer (property)"
]
}, {
title: "Supplier Bill (Purchase Order $50 but Invoice $52)",
operations: [
{ account: 'Assets: Uninvoiced Inventory', debit: 50 },
{ account: 'Assets: Deferred Tax Assets', debit: 4.68 },
{ account: 'Expenses: Price Difference', debit: 2 },
{ account: 'Liabilities: Accounts Payable', credit: 56.68 }
],
explanation: [
"A temporary account is used to note goods to receive",
"The purchase order provides prices of goods, the actual invoice may include extra costs such as shipping",
"The company still needs to pay the vendor (traded an asset against a liability)"
],
configuration: [
"Uninvoiced Inventory: defined on the product or the category of related product, field: Stock Input Account",
"Deferred Tax Assets: defined on the tax used on the purchase order line",
"Accounts Payable: defined on the supplier related to the bill",
null,
"In this scenario, the purchase order was $50 but the company received an invoice for $52 as there were extra shipping costs"
]
}, {
title: "Supplier Goods Received (Purchase Order: $50)",
operations: [
{ account: 'Assets: Inventory', debit: 50 },
{ account: 'Assets: Uninvoiced Inventory', credit: 50 },
],
explanation: [
"Inventory is increased by $50, the expected amount coming from the purchase order",
"A temporary account is used for the counterpart and will be cleared when receiving the invoice"
],
configuration: [
"Uninvoiced Inventory: defined on the product or the category of related product, field: Stock Input Account",
"Inventory: defined on the product category, field: Stock Valuation"
]
}, {
title: "Buy an asset ($300,000 - no tax)",
operations: [
{ account: 'Assets: Buildings', debit: 300000 },
{ account: 'Liabilities: Accounts Payable', credit: 300000 }
],
explanation: [
"The company gets an asset worth of $300,000",
"The company needs to pay $300,000 to the vendor (traded an asset against a liability)"
],
configuration: [
"Buildings: Defined on the Asset category selected on the supplier bill line",
"Accounts Payable: defined on the supplier related to the bill (property)"
]
}, {
title: "Pay supplier invoice",
operations: [
{ account: 'Liabilities: Accounts Payable', debit: 300000 },
{ account: 'Assets: Cash', credit: 300000 }
],
explanation: [
"The company owns $300,000 less to the supplier (liabilities are settled)",
"The company's cash is reduced by $300,000 (reduction of asset)"
],
configuration: [
"Accounts Payable: defined on the supplier you pay (property)",
"Cash: defined on the journal related to the payment method"
]
}, {
title: "Cash sale (Sales Receipt)",
operations: [
{ account: 'Assets: Cash', debit: 109 },
{ account: 'Revenue: Goods', credit: 100 },
{ account: 'Liabilities: Deferred Tax Liabilities', credit: 9 }
],
explanation: [
"Company's cash is increased by $109",
"Revenues increase by $100",
"A tax of $9 has to be paid"
],
configuration: [
"Cash: Payment method defined on the Sales Receipt",
"Sales: Defined on the product used in the sales receipt, or the category of product if empty",
"Deferred Tax Liabilities: Defined on the tax used in the sales receipt (coming from the product)"
]
}, {
title: "Customer pays invoice, 5% early payment rebate",
operations: [
{ account: 'Assets: Cash', debit: 950 },
{ account: 'Revenue: Sales Discount', debit: 50 },
{ account: 'Assets: Accounts Receivable', credit: 1000 }
],
explanation: [
"Company's cash is increased by $950",
"Sales discounts lowering effective revenues by $50",
"The customer owns $1000 less to the company"
],
configuration: [
"Cash: is defined on the journal related to the payment / bank statement",
"Sales Discount: is selected during the payment matching process",
"Accounts Receivable: is defined on the customer associated to the payment"
]
}, {
title: "Fiscal year closing — positive earnings and 50% dividends",
operations: [
{ account: 'Net Profit', debit: 1000 },
{ account: 'Equity: Retained Earnings', credit: 500 },
{ account: 'Liabilities: Dividend Payable', credit: 500 }
],
explanation: [
"The P&L is cleared (net profit)",
"50% is transferred to retained earnings",
"50% will be paid to shareholders as dividends"
],
configuration: [
"This transaction is recorded by the advisor before closing the fiscal year, depending on how the company uses its net profit."
]
}
]);
}());
+217
View File
@@ -0,0 +1,217 @@
/* global Immutable, React */
/* global createAtom */
(function () {
// NOTE: used for double_entry.rst file
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
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 === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations);
});
});
} else {
data.swap(function (d) {
return d.set('active', null)
.update('operations', function (ops) {
return ops.remove(operations);
});
});
}
}
}),
' ',
label
);
}));
}
});
var UNIT_PRICE = 100;
function format_qty(val) {
if (val == null) { return ''; }
if (val < 0) { return val; }
return '+' + String(val);
}
function format_value(val) {
if (isNaN(val)) { return ''; }
if (val < 0) { return '-$' + String(Math.abs(val)); }
return '$' + String(val);
}
var Chart = React.createClass({
render: function () {
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(null, "Location"),
React.DOM.th({ className: 'text-right' }, "Quantity"),
React.DOM.th({ className: 'text-right' }, "Value"))
),
React.DOM.tbody(
null,
this.locations().map(function (data) {
var highlight = false;
return React.DOM.tr(
{ key: toKey(data.get('label')) },
React.DOM.th(null, data.get('level') ? '\u2001' : '', data.get('label')),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight
})
}, format_qty(data.get('qty'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight
})
}, format_value(data.get('qty') * UNIT_PRICE))
);
})
)
)
);
},
locations: function () {
var data = this.props.p.get('operations');
// {location: total_qty}
var totals = data.toIndexedSeq().flatten(true).reduce(function (acc, op) {
return acc.update(op.get('location'), function (qty) {
return (qty || 0) + op.get('qty');
});
}, Immutable.Map());
return locations.valueSeq().flatMap(function (loc) {
var sub_locations = loc.get('locations').valueSeq().map(function (subloc) {
return subloc.set('level', 1).set('qty', totals.get(subloc));
});
return Immutable.Seq.of(loc.set('level', 0)).concat(sub_locations);
});
}
});
data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, { p: next }),
document.getElementById('chart-of-locations-controls'));
React.render(
React.createElement(Chart, { p: next }),
document.getElementById('chart-of-locations'));
});
document.addEventListener('DOMContentLoaded', function () {
var chart = document.querySelector('.chart-of-locations');
if (!chart) { return; }
chart.setAttribute('id', 'chart-of-locations');
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-of-locations-controls');
chart.parentNode.insertBefore(controls, chart);
data.reset(Immutable.Map({
active: null,
operations: Immutable.OrderedSet()
}));
});
var locations = Immutable.fromJS({
warehouse: {
label: "Warehouse",
locations: {
zone1: { label: "Zone 1" },
zone2: { label: "Zone 2" }
}
},
partners: {
label: "Partner Locations",
locations: {
customers: { label: "Customers" },
suppliers: { label: "Suppliers" }
}
},
virtual: {
label: "Virtual Locations",
locations: {
initial: { label: "Initial Inventory" },
loss: { label: "Inventory Loss" },
scrap: { label: "Scrapped" },
manufacturing: { label: "Manufacturing" }
}
}
}, function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
var operations = Immutable.fromJS([{
label: "Initial Inventory",
operations: [
{ location: locations.getIn(['virtual', 'locations', 'initial']), qty: -3 },
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: +3 }
]
}, {
label: "Reception",
operations: [
{ location: locations.getIn(['partners', 'locations', 'suppliers']), qty: -2 },
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: +2 }
]
}, {
label: "Delivery",
operations: [
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: -1 },
{ location: locations.getIn(['partners', 'locations', 'customers']), qty: +1 }
]
}, {
label: "Return",
operations: [
{ location: locations.getIn(['partners', 'locations', 'customers']), qty: -1 },
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: +1 }
]
}, {
label: "1 product broken in Zone 1",
operations: [
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: -1 },
{ location: locations.getIn(['virtual', 'locations', 'scrap']), qty: +1 }
]
}, {
label: "Inventory check of Zone 1",
operations: [
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: -1 },
{ location: locations.getIn(['virtual', 'locations', 'loss']), qty: +1 }
]
}, {
label: "Move from Zone 1 to Zone 2",
operations: [
{ location: locations.getIn(['warehouse', 'locations', 'zone1']), qty: -1 },
{ location: locations.getIn(['warehouse', 'locations', 'zone2']), qty: +1 }
]
}]);
})();
+101
View File
@@ -0,0 +1,101 @@
(function () {
document.addEventListener('DOMContentLoaded', function () {
alternatives();
highlight();
checks_handling();
});
function highlight() {
// NOTE: used by double-entry.rst
$('.highlighter-list').each(function () {
var $this = $(this),
$target = $($this.data('target'));
$this.on('mouseout', 'li', function (e) {
$(e.currentTarget).removeClass('secondary');
$target.find('.related').removeClass('related');
}).on('mouseover', 'li', function (e) {
if (!e.currentTarget.contains(e.target)) { return; }
var $li = $(e.currentTarget);
// console.log($li, $li.data('highlight'), $target.find($li.data('highlight')));
$li.addClass('secondary');
$target.find($li.data('highlight')).addClass('related');
});
});
}
/** alternatives display:
* - prepend control for each <dt>
* - radio input with link to following dd
* - label is <dt> content
* - hide all first-level dt and dd (CSS)
* - on change
* - hide all dds
* - show dd corresponding to the selected radio
* - automatically select first control on startup
*/
function alternatives() {
// NOTE: used by double-entry.rst & valuation_methods pages
$('dl.alternatives').each(function (index) {
var $list = $(this),
$contents = $list.children('dd');
var $controls = $('<div class="alternatives-controls">').append(
$list.children('dt').map(function () {
var label = document.createElement('label'),
input = document.createElement('input');
input.setAttribute('type', 'radio');
input.setAttribute('name', 'alternatives-' + index);
var sibling = this;
while ((sibling = sibling.nextSibling) && sibling.nodeType !== 1);
input.content = sibling;
label.appendChild(input);
label.appendChild(document.createTextNode(' '));
label.appendChild(document.createTextNode(this.textContent));
return label;
}))
.insertBefore($list)
.on('change', 'input', function (e) {
// change event triggers only on newly selected input, not
// on the one being deselected
$contents.css('display', '');
var content = e.target.content;
content && (content.style.display = 'block');
})
.find('input:first').click();
});
}
function checks_handling() {
// NOTE: used by memento.rst
var $section = $('.checks-handling');
if (!$section.length) { return; }
var $ul = $section.find('ul')
.find('li').each(function () {
var txt = this.textContent;
while (this.firstChild) {
this.removeChild(this.firstChild)
}
$('<label style="display: block;">')
.append('<input type="radio" name="checks-handling">')
.append(' ')
.append(txt)
.appendTo(this);
}).end()
.on('change', 'input', update);
update();
function update() {
var $inputs = $ul.find('input');
var idx = 0;
$inputs.each(function (index) {
if (this.checked) {
idx = index;
}
}).eq(idx).prop('checked', true);
$ul.nextAll('div').hide().eq(idx).show();
}
}
})();
+72
View File
@@ -0,0 +1,72 @@
(function () {
// NOTE: memento.rst
document.addEventListener('DOMContentLoaded', function () {
var $rec = $('#reconciliation .reconciliation-example');
if (!$rec.length) { return; }
var state = 0;
var operations = [
function reconcile2() {
$rec.addClass('reconcile2');
return 1;
},
function reconcile1() {
$rec.addClass('reconcile1');
return 2;
},
function unreconcile() {
$rec.removeClass('reconcile1 reconcile2');
$1.add($2).removeClass('reconciled');
setTimeout(update_btn, 0);
return 0;
}
];
var $buttons = $('<div class="buttons">').on('click', 'button', function () {
this.disabled = true;
state = operations[state]();
}).appendTo($rec);
var $1 = $rec.find('td:contains("Invoice 1"), td:contains("Payment 1")')
.parent()
.addClass('invoice1');
var $2 = $rec.find('td:contains("Invoice 2"), td:contains("Payment 2")')
.parent()
.addClass('invoice2');
// will be called multiple times (each tr + each td), only take trs in
// account
$rec.on('animationend webkitAnimationEnd MSAnimationEnd', function (e) {
switch (e.originalEvent.target) {
case $1[0]:
$1.addClass('reconciled');
break;
case $2[0]:
$2.addClass('reconciled');
break;
}
update_btn();
});
update_btn();
function update_btn() {
var $reconcile = $('<button class="btn btn-success" type="button">')
.text("Next Reconcile")
.appendTo($buttons.empty())
switch (state) {
case 0:
$reconcile.text("Reconcile");
break;
case 1:
break;
case 2:
$reconcile.prop('disabled', true);
$('<button class="btn btn-primary" type="button">')
.text("Unreconcile")
.appendTo($buttons);
break;
default:
throw new Error("Unknown button state " + state);
}
}
});
})();