Merge pull request #2 from odoo/inventory

Inventory mementoes (wip-ish)
This commit is contained in:
xmo-odoo 2015-04-17 22:01:28 +02:00
commit 08cd20a121
11 changed files with 1118 additions and 11 deletions

View File

@ -13,8 +13,9 @@
background-color: hsl(219, 67%, 94%); background-color: hsl(219, 67%, 94%);
} }
#chart-controls label:hover, label:hover,
#entries-control label:hover { label:hover,
.highlighter-list li:hover {
background-color: hsl(0, 0%, 94%); background-color: hsl(0, 0%, 94%);
cursor: pointer; cursor: pointer;
} }
@ -77,6 +78,13 @@
font-style: normal; font-style: normal;
} }
.values-table tr > * {
text-align: right;
}
.values-table tr > :first-child {
text-align: left;
}
/* 3-column (thing, debit, credit) tables */ /* 3-column (thing, debit, credit) tables */
/* 2nd and 3rd th & td of each row right-aligned and 1/4th width */ /* 2nd and 3rd th & td of each row right-aligned and 1/4th width */
.d-c-table tr > :nth-child(2), .d-c-table tr > :nth-child(2),
@ -86,20 +94,25 @@
} }
@media (min-width: 992px) { @media (min-width: 992px) {
.accounts-table { .accounts-table, .force-right .highlighter-target {
font-size: 90%; font-size: 90%;
color: #888 !important; color: #888 !important;
} }
.accounts-table .related { .force-right .highlighter-target th {
font-weight: normal;
font-size: 110%;
}
.accounts-table .related, .force-right .highlighter-target .related {
background-color: transparent !important; background-color: transparent !important;
color: #eee !important; color: #eee !important;
} }
.accounts-table .secondary { .accounts-table .secondary, .force-right .highlighter-target .secondary {
background-color: transparent !important; background-color: transparent !important;
color: #aaa !important; color: #aaa !important;
} }
.chart-of-accounts .highlight-op { .chart-of-accounts .highlight-op,
.valuation-chart .highlight-op {
background-color: #030035; background-color: #030035;
} }
} }
@ -153,3 +166,17 @@ blockquote.highlights {
margin-bottom: 0; margin-bottom: 0;
text-align: center; 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;
}

View File

@ -99,10 +99,9 @@
); );
}, },
accounts: function() { accounts: function() {
var _this = this;
var data = this.props.p.get('operations'); 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 return acc
.updateIn([op.get('account'), 'debit'], function (d) { .updateIn([op.get('account'), 'debit'], function (d) {
return (d || 0) + op.get('debit', zero)(data); return (d || 0) + op.get('debit', zero)(data);

270
_static/coa-valuation.js Normal file
View File

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

210
_static/inventory.js Normal file
View File

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

View File

@ -1,5 +1,70 @@
(function () { (function () {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
alternatives();
highlight();
checks_handling();
});
function highlight() {
$('.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() {
$('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() {
var $section = $('.checks-handling'); var $section = $('.checks-handling');
if (!$section.length) { return; } if (!$section.length) { return; }
@ -28,5 +93,6 @@
}).eq(idx).prop('checked', true); }).eq(idx).prop('checked', true);
$ul.nextAll('div').hide().eq(idx).show(); $ul.nextAll('div').hide().eq(idx).show();
} }
});
}
})(); })();

View File

@ -6621,6 +6621,7 @@ div.section > h1 {
div.section > h2 { div.section > h2 {
padding-bottom: 9px; padding-bottom: 9px;
margin: 40px 0 20px; margin: 40px 0 20px;
border-bottom: 1px solid #eeeeee;
font-size: 36px; font-size: 36px;
padding-top: 20px; padding-top: 20px;
margin-top: 0; margin-top: 0;

View File

@ -276,6 +276,9 @@ def setup(app):
app.add_javascript('reconciliation.js') app.add_javascript('reconciliation.js')
app.add_javascript('misc.js') app.add_javascript('misc.js')
app.add_javascript('inventory.js');
app.add_javascript('coa-valuation.js')
app.connect('html-page-context', analytics) app.connect('html-page-context', analytics)
app.add_config_value('google_analytics_key', '', 'env') app.add_config_value('google_analytics_key', '', 'env')

247
double-entry.rst Normal file
View File

@ -0,0 +1,247 @@
:classes: stripe
=================================
Double-Entry Inventory Management
=================================
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
.. placeholder
Operations
==========
Stock moves represent the transit of goods and materials between locations.
.. rst-class:: alternatives force-right
Manufacturing Order
Consume:
| 2 Wheels: Stock → Production
| 1 Bike Frame: Stock → Production
Produce:
1 Bicycle: Production → Stock
Configuration:
| Stock: the location the Manufacturing Order is initiated from
| Production: on the product form, field "Production Location"
Drop-shipping
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
Missing products in inventory
1 Bicycle: Warehouse → Inventory Loss
Extra products in inventory
1 Bicycle: Inventory Loss → Warehouse
Reception
| 1 Bicycle: Supplier → Input
| 1 Bicycle: Input → Stock
Analysis
========
Inventory analysis can use products count or products value (= number of
products * product cost).
For each inventory location, multiple data points can be analysed:
.. raw:: html
<ul class="highlighter-list" data-target=".analysis-table">
<li data-highlight=".analysis-valuation">inventory valuation</li>
<li data-highlight=".analysis-creation">
value creation (difference between the value of manufactured products
and the cost of raw materials used during manufacturing) (negative)
</li>
<li data-highlight=".analysis-lost">value of lost/stolen products</li>
<li data-highlight=".analysis-scrapped">value of scrapped products</li>
<li data-highlight=".analysis-delivered">value of products delivered to clients over a period</li>
<li data-highlight=".analysis-transit">value of products in transit between locations</li>
</ul>
.. h:div:: force-right analysis-table
.. raw:: html
<table class="table table-condensed highlighter-target">
<thead>
<tr>
<th>Location</th> <th class="text-right">Value</th>
</tr>
</thead>
<tbody>
<tr class="analysis-valuation">
<th>Physical Locations</th> <td class="text-right">$1,000</td>
</tr>
<tr>
<th>&#8193;Warehouse 1</th> <td class="text-right">$600</td>
</tr>
<tr>
<th>&#8193;Warehouse 2</th> <td class="text-right">$400</td>
</tr>
<tr>
<th>Partner Locations</th> <td class="text-right">- $1,500</td>
</tr>
<tr class="analysis-delivered">
<th>&#8193;Customers</th> <td class="text-right">$2,000</td>
</tr>
<tr>
<th>&#8193;Suppliers</th> <td class="text-right">- $3,500</td>
</tr>
<tr>
<th>Virtual Locations</th> <td class="text-right">$500</td>
</tr>
<tr class="analysis-transit">
<th>&#8193;Transit Location</th> <td class="text-right">$600</td>
</tr>
<tr>
<th>&#8193;Initial Inventory</th> <td class="text-right">$0</td>
</tr>
<tr class="analysis-lost">
<th>&#8193;Inventory Loss</th> <td class="text-right">$350</td>
</tr>
<tr class="analysis-scrapped">
<th>&#8193;Scraped</th> <td class="text-right">$550</td>
</tr>
<tr class="analysis-creation">
<th>&#8193;Manufacturing</th> <td class="text-right">- $1,000</td>
</tr>
</tbody>
</table>
Procurements & Procurement 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:
.. rst-class:: alternatives force-right
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 Rules
Effect
A procurement is created at the rule's location.
Configuration
Procurement location: on the rule, field "Location"
Procurement rules
Effect
A new procurement is created on the rule's source location
*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
To Stock)`
.. h:div:: force-right
.. todo:: needs schema thing from FP
Routes
======
Procurement rules are grouped in routes. Routes define paths the product must
follow. Routes may be applicable or not, depending on the products, sales
order lines, warehouse,...
To fulfill a procurement, the system will search for rules belonging to routes
that are defined in (by order of priority):
.. rst-class:: alternatives force-right
Warehouses
Warehouse Route Example: Pick → Pack → Ship
Picking List:
Pick Zone → Pack Zone
Pack List:
Pack Zone → Gate A
Delivery Order:
Gate A → Customer
Routes that describe how you organize your warehouse should be defined on the warehouse.
A Product
Product Route Example: Quality Control
Reception:
Supplier → Input
Confirmation:
Input → Quality Control
Storage:
Quality Control → Stock
Product Category
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
==========
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
Quality Control
* 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
==================
Routes and rules define inventory moves. For every rule, a document type is
provided:
* Picking
* Packing
* Delivery Order
* Purchase Order
* ...
Moves are grouped within the same document type if their procurement group and
locations are the same.
A sale order creates a procurement group so that pickings and delivery orders
of the same order are grouped. But you can define specific groups on
reordering rules too. (e.g. to group purchases of specific products together)

View File

@ -5,6 +5,7 @@ Odoo Business Mementoes
.. rst-class:: index-tree .. rst-class:: index-tree
.. toctree:: .. toctree::
:maxdepth: 1 :titlesonly:
accounting accounting
inventory

9
inventory.rst Normal file
View File

@ -0,0 +1,9 @@
=========
Warehouse
=========
.. toctree::
:titlesonly:
double-entry
valuation

274
valuation.rst Normal file
View File

@ -0,0 +1,274 @@
:classes: stripe
====================
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
<hr style="float: none; visibility: hidden; margin: 0;">
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.