diff --git a/_static/inventory.js b/_static/inventory.js new file mode 100644 index 000000000..32241525f --- /dev/null +++ b/_static/inventory.js @@ -0,0 +1,210 @@ +(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) + .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-condensed'}, + 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: "Inventaire Initial", + 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} + ] + }]); +})(); diff --git a/conf.py b/conf.py index 99adea4f4..7af613445 100644 --- a/conf.py +++ b/conf.py @@ -276,6 +276,8 @@ def setup(app): app.add_javascript('reconciliation.js') app.add_javascript('misc.js') + app.add_javascript('inventory.js'); + app.connect('html-page-context', analytics) app.add_config_value('google_analytics_key', '', 'env') diff --git a/double-entry.rst b/double-entry.rst index 11ae88e4c..27b02ffe3 100644 --- a/double-entry.rst +++ b/double-entry.rst @@ -7,13 +7,9 @@ Double-Entry Inventory Management In a double-entry inventory, there is no stock input, output (disparition) or transformation. Instead, there are only stock moves between locations. -* Inventory: 3 products in Zone 1 -* Reception: 2 products in Zone 1 -* Delivery: 1 product to client -* Return: 1 product from client -* Scrap: 1 product broken in zone 1 -* Inventory Zone 1: loss of 1 product -* Move: 1 product Zone 1 ➔ Zone 2 +.. h:div:: force-right chart-of-locations + + .. placeholder Operations ========== @@ -95,4 +91,3 @@ Some example: Procurement Groups ================== -