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.