From 13761b4557ffe1ad00e2df9216ca273159670eaa Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Mar 2015 14:58:55 +0100 Subject: [PATCH 1/9] [CHG] move things around for wider functional concern It looks like we're going with this for the functional documentation/mementoes thing, so move the accounting memento out of the way and create a first draft for a documents hierarchy --- double-entry.rst | 5 +++++ index.rst | 3 ++- inventory.rst | 9 +++++++++ valuation.rst | 5 +++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 double-entry.rst create mode 100644 inventory.rst create mode 100644 valuation.rst diff --git a/double-entry.rst b/double-entry.rst new file mode 100644 index 000000000..e331bca67 --- /dev/null +++ b/double-entry.rst @@ -0,0 +1,5 @@ +:classes: stripe + +================================= +Double-Entry Inventory Management +================================= diff --git a/index.rst b/index.rst index 3ec65b257..02f2c2880 100644 --- a/index.rst +++ b/index.rst @@ -5,6 +5,7 @@ Odoo Business Mementoes .. rst-class:: index-tree .. toctree:: - :maxdepth: 1 + :titlesonly: accounting + inventory diff --git a/inventory.rst b/inventory.rst new file mode 100644 index 000000000..f118810ef --- /dev/null +++ b/inventory.rst @@ -0,0 +1,9 @@ +========= +Warehouse +========= + +.. toctree:: + :titlesonly: + + double-entry + valuation diff --git a/valuation.rst b/valuation.rst new file mode 100644 index 000000000..dae4681bf --- /dev/null +++ b/valuation.rst @@ -0,0 +1,5 @@ +:classes: stripe + +==================== +Inventory Valuations +==================== From 4ae0fd613d4d615a3b811ad4d0acfcf0b6a82439 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 15 Apr 2015 17:06:16 +0200 Subject: [PATCH 2/9] [FIX] remove unnecessary local, convert set to indexedseq By default "iterating" a set will use a setseq which may still uniquify information if it finds duplicates (but it may not as it's lazy & al). #flatten conserves the seq type, so if there are two equal operation items amongst all selected operations they may get deduplicated, with the end result of operations disappearing in the final formatted result. --- _static/chart-of-accounts.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/_static/chart-of-accounts.js b/_static/chart-of-accounts.js index 8d79f0f02..b4a42dd2e 100644 --- a/_static/chart-of-accounts.js +++ b/_static/chart-of-accounts.js @@ -99,10 +99,9 @@ ); }, accounts: function() { - var _this = this; var data = this.props.p.get('operations'); - var totals = data.flatten(true).reduce(function (acc, op) { + 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); From 2a714115b17641729cd1f6e2a52b8374b7d66a89 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 23 Mar 2015 17:33:58 +0100 Subject: [PATCH 3/9] [ADD] double entry basic doc --- _themes/odoodoc/static/style.css | 1 + double-entry.rst | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/_themes/odoodoc/static/style.css b/_themes/odoodoc/static/style.css index 4dbcbc6e2..079d7a3b4 100644 --- a/_themes/odoodoc/static/style.css +++ b/_themes/odoodoc/static/style.css @@ -6621,6 +6621,7 @@ div.section > h1 { div.section > h2 { padding-bottom: 9px; margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; font-size: 36px; padding-top: 20px; margin-top: 0; diff --git a/double-entry.rst b/double-entry.rst index e331bca67..11ae88e4c 100644 --- a/double-entry.rst +++ b/double-entry.rst @@ -3,3 +3,96 @@ ================================= 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 + +Operations +========== + +Stock moves represent the transit of goods and materials between inventory +locations. + +* Manufacturing Order +* Drop-shipping +* Picking ➔ Packing ➔ Shipping +* Inter-Warehouse transfert +* Loss of product +* Inventory +* Reception + +Analysis +======== + +Inventory analysis can use products count or value (number of products * +products cost). + +For each inventory location, multiple data points can be analysed: + +* inventory valuation +* value creation (difference between the value of manufactured products and + the cost of raw materials used during manufacturing) +* value of lost/stolen products +* value of scrapped products +* value of products delivered to clients over a period +* value of products in transit between locations + +Procurements & Pull Rules +========================= + +A procurement is a request for a specific quantity of products to a specific +location. They can be created manually or automatically triggered by: + +* sale orders +* minimum stock rules +* rules + +*Pull rules* describe how to fulfill procurements on specific locations: + +* where the product should come from (source location) +* whether the procurement is :abbr:`MTO (Made To Order)` or :abbr:`MTS (Made + To Stock)` + +.. h:div:: force-right + + .. todo:: needs schema thing from FP + +Routes +====== + +At each step or a procurement's fulfillment, multiple rules may be +available. *Routes* define which rules should be used based on the environment +(product, sales orders, warehouse, …). To fulfill a procurement, the system +will search for routes in the following order: + +1. sale order line routes +2. product routes +3. product category routes +4. warehouse routes + +Push Rules +========== + +Push rules are triggered when a product enters a specific location, and allows +chaining locations. Push rules can also be configured and filtered using +routes. + +Some example: +* quality control +* transit warehouse 1 + +.. warning:: push rules and pull rules are *not* symmetrical, pull rules are + triggered by procurement requests whereas push rules are + triggered by stock moves + +Procurement Groups +================== + From eb20f603b58a203f4c62c176a0df33ab208937dc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 15 Apr 2015 17:09:18 +0200 Subject: [PATCH 4/9] [ADD] chart of inventory locations Quite a bit of duplication with the chart of accounts (controls are just about identical) but not quite enough that the CoA can trivially be reused. --- _static/inventory.js | 210 +++++++++++++++++++++++++++++++++++++++++++ conf.py | 2 + double-entry.rst | 11 +-- 3 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 _static/inventory.js 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 ================== - From 15c51c9845584045d380f7260348afb2aaf2a136 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 16 Apr 2015 11:57:02 +0200 Subject: [PATCH 5/9] [ADD] support for generic alternative content in right-hand side dd.alternatives triggers "widget", each (dt, dd) is an alternative with dt being the alternative (and radio button) label and dd being the actual content to display on selection. --- _static/accounting.css | 14 +++++ _static/misc.js | 52 +++++++++++++++- double-entry.rst | 131 +++++++++++++++++++++++++++++++---------- 3 files changed, 165 insertions(+), 32 deletions(-) diff --git a/_static/accounting.css b/_static/accounting.css index 8eb5e6298..a08136ab6 100644 --- a/_static/accounting.css +++ b/_static/accounting.css @@ -153,3 +153,17 @@ blockquote.highlights { margin-bottom: 0; text-align: center; } + +/* + lists of alternatives +*/ +.alternatives-controls label { + display: block; +} +dl.alternatives > dt, +dl.alternatives > dd { + display: none; +} +dl.alternatives > dd { + margin-left: 0; +} diff --git a/_static/misc.js b/_static/misc.js index c3d559f9a..f139777ff 100644 --- a/_static/misc.js +++ b/_static/misc.js @@ -1,5 +1,52 @@ (function () { document.addEventListener('DOMContentLoaded', function () { + alternatives() + checks_handling(); + }); + + /** alternatives display: + * - prepend control for each
+ * - radio input with link to following dd + * - label is
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() { + $('dl.alternatives').each(function (index) { + var $list = $(this), + $contents = $list.children('dd'); + var $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() { var $section = $('.checks-handling'); if (!$section.length) { return; } @@ -9,7 +56,7 @@ while (this.firstChild) { this.removeChild(this.firstChild) } - + $('
* - radio input with link to following dd diff --git a/double-entry.rst b/double-entry.rst index d6d76f5b4..d055bdcd3 100644 --- a/double-entry.rst +++ b/double-entry.rst @@ -50,13 +50,69 @@ products cost). For each inventory location, multiple data points can be analysed: -* inventory valuation -* value creation (difference between the value of manufactured products and - the cost of raw materials used during manufacturing) -* value of lost/stolen products -* value of scrapped products -* value of products delivered to clients over a period -* value of products in transit between locations +.. raw:: html + +
    +
  • inventory valuation
  • +
  • + value creation (difference between the value of manufactured products + and the cost of raw materials used during manufacturing) (negative) +
  • +
  • value of lost/stolen products
  • +
  • value of scrapped products
  • +
  • value of products delivered to clients over a period
  • +
  • value of products in transit between locations
  • +
+ +.. h:div:: force-right analysis-table + + .. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location Value
Physical Locations $1,000
 Warehouse 1 $600
 Warehouse 2 $400
Partner Locations - $1,500
 Customers $2,000
 Suppliers - $3,500
Virtual Locations $500
 Transit Location $600
 Initial Inventory $0
 Inventory Loss $350
 Scraped $550
 Manufacturing - $1,000
Procurements & Procurement Rules ================================ From 65ad67b85d602669c3f0a825d3e252a1750bf520 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 16 Apr 2015 14:18:43 +0200 Subject: [PATCH 7/9] [IMP] wording, leftover bits of french --- _static/inventory.js | 2 +- double-entry.rst | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/_static/inventory.js b/_static/inventory.js index 32241525f..6eef6d4ad 100644 --- a/_static/inventory.js +++ b/_static/inventory.js @@ -165,7 +165,7 @@ : v.toOrderedMap(); }); var operations = Immutable.fromJS([{ - label: "Inventaire Initial", + label: "Initial Inventory", operations: [ {location: locations.getIn(['virtual','locations','initial']), qty: -3}, {location: locations.getIn(['warehouse','locations','zone1']), qty: +3} diff --git a/double-entry.rst b/double-entry.rst index d055bdcd3..611334efc 100644 --- a/double-entry.rst +++ b/double-entry.rst @@ -4,8 +4,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. +A double-entry inventory has no stock input, output (disparition of products) +or transformation. Instead, all operations are stock moves between locations +(possibly virtual). .. h:div:: force-right chart-of-locations @@ -45,8 +46,8 @@ Reception Analysis ======== -Inventory analysis can use products count or value (number of products * -products cost). +Inventory analysis can use products count or products value (= number of +products * product cost). For each inventory location, multiple data points can be analysed: @@ -122,25 +123,25 @@ location. They can be created manually or automatically triggered by: .. rst-class:: alternatives force-right -Sale order +New sale orders Effect A procurement is created at the customer location for every product ordered by the customer (you have to deliver the customer) Configuration Procurement Location: on the customer, field “Customer Location” (property) -Minimum Stock Rule +Minimum Stock Rules Effect todo Configuration todo -Rules +Custom rules Effect todo Configuration todo -*Procurement rules* describe how to fulfill procurements on specific -locations: +*Procurement rules* describe how procurements on specific locations should be +fulfilled e.g.: * where the product should come from (source location) * whether the procurement is :abbr:`MTO (Made To Order)` or :abbr:`MTS (Made @@ -186,10 +187,9 @@ Sale Order Line Push Rules ========== - -Push rule are trigered when products arrive at a specific location and allows -to automatically move them to another location. Push rules applications also -depends on applicable routes. +Push rules trigger when products enter a specific location. They automatically +move the product to a new location. Whether a push rule can be used depends on +applicable routes. .. rst-class:: alternatives force-right @@ -203,8 +203,8 @@ Transit Warehouse 1 Procurement Groups ================== -Routes and rules defines the inventory moves. On every rule, the document type -is provided: +Routes and rules define inventory moves. For every rule, a document type is +provided: * Picking * Packing From a7aba04e33ecb5248b3a9164c180b28178521d3f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 16 Apr 2015 17:36:55 +0200 Subject: [PATCH 8/9] [ADD] examples with jco --- double-entry.rst | 93 +++++++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/double-entry.rst b/double-entry.rst index 611334efc..fa38e0eb5 100644 --- a/double-entry.rst +++ b/double-entry.rst @@ -15,33 +15,46 @@ or transformation. Instead, all operations are stock moves between locations Operations ========== -Stock moves represent the transit of goods and materials between inventory -locations. +Stock moves represent the transit of goods and materials between locations. .. rst-class:: alternatives force-right Manufacturing Order Consume: - | 2 Wheels: Warehouse → Manufacturing - | 1 Bike Frame: Warehouse → Manufacturing + | 2 Wheels: Stock → Production + | 1 Bike Frame: Stock → Production Produce: - 1 Bicycle: Manufacturing → Warehouse + 1 Bicycle: Production → Stock Configuration: - | Warehouse: the location the Manufacturing Order is initiated - | Manufacturing: on the product form, field “Manufacturing Location” + | Stock: the location the Manufacturing Order is initiated from + | Production: on the product form, field "Production Location" Drop-shipping - stuff 1 -Picking ➔ Packing ➔ Shipping - stuff 2 -Inter-Warehouse transfert - stuff 3 -Loss of product - stuff 4 + 1 Bicycle: Supplier → Customer +Client Delivery + Pick + 1 Bicycle: Stock → Packing Zone + Pack + 1 Bicycle: Packing Zone → Output + Shipping + 1 Bicycle: Output → Customer +Inter-Warehouse transfer + Transfer: + | 1 Bicycle: Warehouse 1 → Transit + | 1 Bicycle: Transit → Warehouse 2 + Configuration: + | Warehouse 2: the location the transfer is initiated from + | Warehouse 1: on the transit route +Broken Product (scrapped) + 1 Bicycle: Warehouse → Scrap Inventory - stuff 5 + Missing products in inventory + 1 Bicycle: Warehouse → Inventory Loss + Extra products in inventory + 1 Bicycle: Inventory Loss → Warehouse Reception - stuff 6 + | 1 Bicycle: Supplier → Input + | 1 Bicycle: Input → Stock Analysis ======== @@ -131,14 +144,12 @@ New sale orders Procurement Location: on the customer, field “Customer Location” (property) Minimum Stock Rules Effect - todo + A procurement is created at the rule's location. Configuration - todo -Custom rules + Procurement location: on the rule, field "Location" +Procurement rules Effect - todo - Configuration - todo + A new procurement is created on the rule's source location *Procurement rules* describe how procurements on specific locations should be fulfilled e.g.: @@ -164,8 +175,8 @@ that are defined in (by order of priority): .. rst-class:: alternatives force-right Warehouses - Warehouse Route Example: - Pick → Pack → Ship + Warehouse Route Example: Pick → Pack → Ship + Picking List: Pick Zone → Pack Zone Pack List: @@ -175,13 +186,28 @@ Warehouses Routes that describe how you organize your warehouse should be defined on the warehouse. A Product - Product Route Example: - Supplier → Quality Control → Inventory + Product Route Example: Quality Control + + Reception: + Supplier → Input + Confirmation: + Input → Quality Control + Storage: + Quality Control → Stock + Product Category - Product Category Route Example: - Supplier → Cross-Docks → Pack Zone + Product Category Route Example: cross-dock + + Reception: + Supplier → Input + Cross-Docks: + Input → Output + Delivery: + Output → Customer Sale Order Line Sale Order Line Example: Drop-shipping + + Order: Supplier → Customer Push Rules @@ -194,11 +220,12 @@ applicable routes. .. rst-class:: alternatives force-right Quality Control - * Product lands in Arrival Zone - * Push 1: Arrival Zone → Quality Control - * Push 2: Quality Control → Inventry -Transit Warehouse 1 - * Product lands in ? + * Product lands in Input + * Push 1: Input → Quality Control + * Push 2: Quality Control → Stock +Warehouse Transit + * Product lands in Transit + * Push: Transit → Warehouse 2 Procurement Groups ================== From 051d680fddaae63978d8be14b88392f18e47bc0c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 17 Apr 2015 21:59:21 +0200 Subject: [PATCH 9/9] [ADD] inventory valuation --- _static/accounting.css | 10 +- _static/coa-valuation.js | 270 +++++++++++++++++++++++++++++++++++++++ conf.py | 1 + valuation.rst | 269 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 _static/coa-valuation.js diff --git a/_static/accounting.css b/_static/accounting.css index df279a479..3eb94887b 100644 --- a/_static/accounting.css +++ b/_static/accounting.css @@ -78,6 +78,13 @@ label:hover, font-style: normal; } +.values-table tr > * { + text-align: right; +} +.values-table tr > :first-child { + text-align: left; +} + /* 3-column (thing, debit, credit) tables */ /* 2nd and 3rd th & td of each row right-aligned and 1/4th width */ .d-c-table tr > :nth-child(2), @@ -104,7 +111,8 @@ label:hover, color: #aaa !important; } - .chart-of-accounts .highlight-op { + .chart-of-accounts .highlight-op, + .valuation-chart .highlight-op { background-color: #030035; } } diff --git a/_static/coa-valuation.js b/_static/coa-valuation.js new file mode 100644 index 000000000..9b84a9778 --- /dev/null +++ b/_static/coa-valuation.js @@ -0,0 +1,270 @@ +(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-condensed'}, + 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('.valuation-chart')); + }); + + document.addEventListener('DOMContentLoaded', function () { + var chart = document.querySelector('.valuation-chart'); + if (!chart) { return; } + + var controls = document.createElement('div'); + controls.setAttribute('id', 'chart-controls'); + 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 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: "Supplier Invoice (PO $50, Invoice $40)", + 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: "Supplier Invoice (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: "Manufacturing Order", + operations: [ + {account: ASSETS.STOCK.code, debit: constant(50)}, + {account: EXPENSES.MANUFACTURING_OVERHEAD.code, debit: constant(2)}, + {account: ASSETS.RAW_MATERIALS.code, debit: 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); + } +})(); diff --git a/conf.py b/conf.py index 7af613445..cd135f886 100644 --- a/conf.py +++ b/conf.py @@ -277,6 +277,7 @@ def setup(app): app.add_javascript('misc.js') app.add_javascript('inventory.js'); + app.add_javascript('coa-valuation.js') app.connect('html-page-context', analytics) app.add_config_value('google_analytics_key', '', 'env') diff --git a/valuation.rst b/valuation.rst index dae4681bf..b73239f0c 100644 --- a/valuation.rst +++ b/valuation.rst @@ -3,3 +3,272 @@ ==================== Inventory Valuations ==================== + +Costing Method +============== + +International accounting standards define several ways to compute product +costs: + +.. rst-class:: alternatives force-right + +Standard Price + .. rst-class:: values-table + + .. list-table:: + :widths: 28 18 18 18 18 + :header-rows: 1 + :stub-columns: 1 + + * - Operation + - Unit Cost + - Qty On Hand + - Delta Value + - Inventory Value + * - Receive 8 Products at $10 + - $10 + - 10 + - +8*$10 + - $80 + * - Receive 4 Products at $12 + - $10 + - 12 + - +4*$10 + - $120 + * - Deliver 10 Products + - $10 + - 2 + - | -10*$10 + | + - $20 + * - Receive 2 Products at $9 + - $10 + - 4 + - +2*$10 + - $40 +Average Price + .. rst-class:: values-table + + .. list-table:: + :widths: 28 18 18 18 18 + :header-rows: 1 + :stub-columns: 1 + + * - Operation + - Unit Cost + - Qty On Hand + - Delta Value + - Inventory Value + * - Receive 8 Products at $10 + - $10 + - 8 + - +8*$10 + - $80 + * - Receive 4 Products at $16 + - $12 + - 12 + - +4*$16 + - $144 + * - Deliver 10 Products [#average-removal]_ + - $12 + - 2 + - | -10*$12 + | + - $24 + * - Receive 2 Products at $6 + - $9 + - 4 + - +2*$6 + - $36 +FIFO + .. rst-class:: values-table + + .. list-table:: + :widths: 28 18 18 18 18 + :header-rows: 1 + :stub-columns: 1 + + * - Operation + - Unit Cost + - Qty On Hand + - Delta Value + - Inventory Value + * - Receive 8 Products at $10 + - $10 + - 8 + - +8*$10 + - $80 + * - Receive 4 Products at $16 + - $12 + - 12 + - +4*$16 + - $144 + * - Deliver 10 Products + - $16 + - 2 + - | -8*$10 + | -2*$16 + - $32 + * - Receive 2 Products at $6 + - $11 + - 4 + - +2*$6 + - $44 +LIFO (not accepted in IFRS) + .. rst-class:: values-table + + .. list-table:: + :widths: 28 18 18 18 18 + :header-rows: 1 + :stub-columns: 1 + + * - Operation + - Unit Cost + - Qty On Hand + - Delta Value + - Inventory Value + * - Receive 8 Products at $10 + - $10 + - 8 + - +8*$10 + - $80 + * - Receive 4 Products at $16 + - $12 + - 12 + - +4*$16 + - $144 + * - Deliver 10 Products + - $10 + - 2 + - | -4*$16 + | -6*$10 + - $20 + * - Receive 2 Products at $6 + - $8 + - 4 + - +2*$6 + - $32 + +The costing method is defined on the product form: standard, average or real +price. + +For "real price", the costing is further refined by the removal strategy (on +the warehouse location or product category), FIFO by default. + +Periodic Inventory Valuation +============================ + +In a periodic inventory valuation, goods reception and outgoing shipments have +no direct impact in the accounting. At the end of the month or year, the +accountant post one journal entry representing the value of the physical +inventory. + +.. rst-class:: alternatives force-right + +Supplier Invoice + .. rst-class:: values-table + + ============================= ===== ====== + \ Debit Credit + ============================= ===== ====== + Assets: Uninvoiced Inventory 50 + Assets: Deferred Tax Assets 4.68 + Expenses: Price Difference 2 + Liabilities: Accounts Payable 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 + + In this scenario, the purchase order was $50 but the company received an + invoice for $52 as there were extra shipping costs. +Goods Receptions + No Journal Entry +Customer Invoice + .. rst-class:: values-table + + ===================================== ===== ====== + \ Debit Credit + ===================================== ===== ====== + Revenue: Goods 100 + Liabilities: Deferred Tax Liabilities 9 + Assets: Accounts Receivable 109 + Assets: Inventory 50 + Expenses: Cost of Goods Sold 50 + ===================================== ===== ====== + + Explanation: + * Revenues increase by $100 + * A tax to pay at the end of the month of $9 + * The customer owns you $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 + * Deferred 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) + + 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. +Customer Shipping + No Journal Entry +Manufacturing Orders + No Journal Entry + +.. raw:: html + +
+ +At the end of the month/year, the company do a physical inventory (or just +rely on the inventory in Odoo). They multiply the quantity of each product by +its cost to know the inventory value of the company. + +.. h:div:: force-right + + Current Values in Accounting: + + .. rst-class:: values-table + + =============== ====== ====== ======= + Account Debit Credit Balance + =============== ====== ====== ======= + 14000 Inventory $5,000 $800 $4,200 + =============== ====== ====== ======= + + Real Inventory Valuation: $4,800 + + Journal Entry to create: + + .. rst-class:: values-table + + ========================== ==== ==== + 14000 Inventory $600 + 14700 Inventory Variations $600 + ========================== ==== ==== + + +Perpetual Inventory Valuation +============================= + +In a perpetual inventory valuation, goods reception and outgoing shipments are +directly posted in the accounting. The inventory valuation is always +up-to-date. + +.. h:div:: valuation-chart force-right + + .. placeholder + +.. [#average-removal] products leaving the stock have no impact on the average + price.