[IMP] inventory: update automated inventory valuation

closes odoo/documentation#3242

Signed-off-by: Brandon Seltenrich (brse) <brse@odoo.com>
This commit is contained in:
Brandon Seltenrich (BRSE) 2022-12-21 20:18:36 +00:00
parent b76b3fa3f3
commit aaa1de94bd
5 changed files with 129 additions and 962 deletions

View File

@ -1,424 +1,132 @@
:code-column:
:custom-css: accounting.css
:custom-js: coa-valuation-continental.js,coa-valuation-anglo-saxon.js,misc.js
=================================
Inventory valuation configuration
Inventory Valuation Configuration
=================================
Inventory valuation refers to how you value your stock. Its a very
important aspect of a business as the inventory can be the biggest asset
of a company.
Inventory valuation implies two main choices:
- The cost method you use to value your goods (standard, fifo, avco)
- The way you record this value into your accounting books (manually or automatically)
Those two concepts are explained in the sections below.
Costing Methods: Standard, FIFO, AVCO
=====================================
The costing method is defined in the product category. There are three
options available. Each of them is explained in detail below.
.. rst-class:: alternatives doc-aside
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
* -
- €10
- 0
-
- €0
* - Receive 8 Products at €10
- €10
- 8
- +8*€10
- €80
* - Receive 4 Products at €16
- €10
- 12
- +4*€10
- €120
* - Deliver 10 Products
- €10
- 2
- | -10*€10
|
- €20
* - Receive 2 Products at €9
- €10
- 4
- +2*€10
- €40
In **Standard Price**, any product will be valued at the cost that you defined
manually on the product form. Usually, this cost is an estimation based
on the material and labor needed to obtain the product. This cost must
be reviewed periodically.
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
* -
- €0
- 0
-
- €0
* - Receive 8 Products at €10
- €10
- 8
- +8*€10
- €80
* - Receive 4 Products at €16
- €12
- 12
- +4*€16
- €144
* - Deliver 10 Products
- €12
- 2
- | -10*€12
|
- €24
* - Receive 2 Products at €6
- €9
- 4
- +2*€6
- €36
In **AVCO (Average Cost)**, each product has the same value and this
value is the average purchase cost of the product. With this costing method, the
cost of the product is recomputed as each receipt.
The average cost does not change when products leave the warehouse.
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
* -
- €0
- 0
-
- €0
* - 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
In **FIFO (First In First Out)**, the products are valued at their
purchase cost. When a product leaves the stock, thats the “First in,
first out” rule that applies.
Pay attention, that this is a financial FIFO. The first value “in”
is the first value “out”, no matter the storage location, warehouse
or serial number.
FIFO is advised if you manage all your workflows into Odoo (Sales,
Purchases, Inventory). It suits any kind of users.
Inventory Valuation: Manual or Automated
========================================
There are two ways to record your inventory valuation in your accounting
books. As the costing method, this is defined in your product category.
Those two methods are detailed below.
It is important to also note that the accounting entries will depend on
your accounting mode: it can be continental or anglo-saxon. In
continental accounting, the cost of a good is taken into account as soon
as the product is received in stock. In anglo-saxon accounting, the cost
of a good is only recorded as an expense when this good is invoiced to a
final customer. In the tables below, you can easily compare those two
accounting modes.
Usually, based on your country, the correct accounting mode will be
chosen by default. If you want to verify your accounting mode, activate
the :ref:`developer mode <developer-mode>` and open your accounting
settings.
Manual Inventory Valuation
--------------------------
In this case, goods receipts and deliveries wont have any direct impact
on your accounting books. Periodically, you create a manual journal
entry representing the value of what you have in stock. To know that
value, go in :menuselection:`Inventory --> Reporting --> Inventory Valuation`.
This is the default configuration in Odoo and it works
out-of-the-box. Check following operations and find out how
Odoo is managing the accounting postings.
Continental Accounting
~~~~~~~~~~~~~~~~~~~~~~
.. rst-class:: alternatives doc-aside
Vendor Bill
.. rst-class:: values-table
============================= ===== ======
\ Debit Credit
============================= ===== ======
Assets: Inventory 50
Assets: Deferred Tax Assets 4.68
Liabilities: Accounts Payable 54.68
============================= ===== ======
Configuration:
* Purchased Goods: defined on the product or on the internal category of related product (Expense Account field)
* Deferred Tax Assets: defined on the tax used on the purchase order line
* Accounts Payable: defined on the vendor related to the bill
Goods Receptions
No Journal Entry
Customer Invoice
.. rst-class:: values-table
===================================== ===== ======
\ Debit Credit
===================================== ===== ======
Revenues: Sold Goods 100
Liabilities: Deferred Tax Liabilities 9
Assets: Accounts Receivable 109
===================================== ===== ======
Configuration:
* Revenues: defined on the product or on the internal category of related product (Income Account field)
* Deferred Tax Liabilities: defined on the tax used on the invoice line
* Accounts Receivable: defined on the customer (Receivable Account)
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, your company does a physical inventory
or just relies on the inventory in Odoo to value the stock into your books.
Create a journal entry to move the stock variation value from your
Profit&Loss section to your assets.
.. h:div:: doc-aside
.. rst-class:: values-table
===================================== ===== ======
\ Debit Credit
===================================== ===== ======
Assets: Inventory X
Expenses: Inventory Variations X
===================================== ===== ======
If the stock value decreased, the **Inventory** account is credited
and the **Inventory Variations** debited.
.. raw:: html
<hr style="float: none; visibility: hidden; margin: 0;">
Anglo-Saxon Accounting
~~~~~~~~~~~~~~~~~~~~~~
.. rst-class:: alternatives doc-aside
Vendor Bill
.. rst-class:: values-table
============================= ===== ======
\ Debit Credit
============================= ===== ======
Assets: Inventory 50
Assets: Deferred Tax Assets 4.68
Liabilities: Accounts Payable 54.68
============================= ===== ======
Configuration:
* Purchased Goods: defined on the product or on the internal category of related product
(Expense Account field)
* Deferred Tax Assets: defined on the tax used on the purchase order line
* Accounts Payable: defined on the vendor related to the bill
Goods Receptions
No Journal Entry
Customer Invoice
.. rst-class:: values-table
===================================== ===== ======
\ Debit Credit
===================================== ===== ======
Revenues: Sold Goods 100
Liabilities: Deferred Tax Liabilities 9
Assets: Accounts Receivable 109
===================================== ===== ======
Configuration:
* Revenues: defined on the product or on the internal category of related
product (Income Account field)
* Deferred Tax Liabilities: defined on the tax used on the invoice line
* Accounts Receivable: defined on the customer (Receivable Account)
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, your company does a physical inventory
or just relies on the inventory in Odoo to value the stock into your books.
Then you need to break down the purchase balance into both the inventory and
the cost of goods sold using the following formula:
Cost of goods sold (COGS) = Starting inventory value + Purchases Closing inventory value
To update the stock valuation in your books, record such an entry:
.. h:div:: doc-aside
.. rst-class:: values-table
===================================== ===== ======
\ Debit Credit
===================================== ===== ======
Assets: Inventory (closing value) X
Expenses: Cost of Good Sold X
Expenses: Purchased Goods X
Assets: Inventory (starting value) X
===================================== ===== ======
Automated Inventory Valuation
-----------------------------
In that case, when a product enters or leaves your stock, an accounting
entry will be automatically created. This means your accounting books
are always up-to-date. This mode is dedicated to expert accountants and
advanced users only. As opposed to periodic valuation, it requires some
extra configuration & testing.
First, you need to define the accounts that will be used for those
accounting entries. This is done on the product category.
Continental Accounting
~~~~~~~~~~~~~~~~~~~~~~
.. h:div:: valuation-chart-continental doc-aside
.. placeholder
.. raw:: html
<hr style="float: none; visibility: hidden; margin: 0;">
.. h:div:: doc-aside
**Configuration:**
- Accounts Receivable/Payable: defined on the partner (Accounting tab)
- Deferred Tax Assets/Liabilities: defined on the tax used on the invoice line
- Revenues/Expenses: defined by default on product's internal category; can be
also set in product form (Accounting tab) as a replacement value.
- Inventory Variations: to set as Stock Input/Output Account in product's internal
category
- Inventory: to set as Stock Valuation Account in product's internal category
Anglo-Saxon Accounting
~~~~~~~~~~~~~~~~~~~~~~
.. h:div:: valuation-chart-anglo-saxon doc-aside
.. placeholder
.. raw:: html
<hr style="float: none; visibility: hidden; margin: 0;">
.. h:div:: doc-aside
**Configuration:**
- Accounts Receivable/Payable: defined on the partner (Accounting tab)
- Deferred Tax Assets/Liabilities: defined on the tax used on the
invoice line
- Revenues: defined on the product category as a default, or specifically
to a specific product.
- Expenses: this is where you should set the "Cost of Goods Sold" account.
Defined on the product category as a default value, or specifically on
the product form.
- Goods Received Not Purchased: to set as Stock Input Account in product's
internal category
- Goods Issued Not Invoiced: to set as Stock Output Account in product's
internal category
- Inventory: to set as Stock Valuation Account in product's internal category
- Price Difference: to set in product's internal category or in product
form as a specific replacement value
All of a company's stock on-hand contributes to the valuation of its inventory. That value should
be reflected in the company's accounting records to accurately show the value of the company and
all of its assets.
By default, Odoo uses a periodic inventory valuation (also known as manual inventory valuation).
This method implies that the accounting team posts journal entries based on the physical inventory
of the company, and that warehouse employees take the time to count the stock. In Odoo, this method
is reflected inside each product category, where the :guilabel:`Costing Method` field will be set
to `Standard Price` by default, and the :guilabel:`Inventory Valuation` field will be set to
`Manual`.
.. image:: inventory_valuation_config/inventory-valuation-fields.png
:align: center
:alt: The Inventory Valuation fields are located on the Product Categories form.
Alternatively, automated inventory valuation is an integrated valuation method that updates the
inventory value in real-time by creating journal entries whenever there are stock moves initiated
between locations in a company's inventory.
.. note::
Automated inventory valuation is a method recommended for expert accountants, given the extra
steps involved in journal entry configuration. Even after the initial setup, the method will
need to be periodically checked to ensure accuracy, and adjustments may be needed on an ongoing
basis depending on the needs and priorities of the business.
Types of Accounting
-------------------
Accounting entries will depend on the accounting mode: Continental or Anglo-Saxon.
.. tip::
Verify the accounting mode by activating the :ref:`developer-mode`
and navigating to :menuselection:`Accounting --> Configuration --> Settings`.
In Anglo-Saxon accounting, the costs of goods sold (COGS) are reported when products are sold or
delivered. This means that the cost of a good is only recorded as an expense when a customer is
invoiced for a product. Interim Stock Accounts are used for the input and output accounts, and are
both Asset Accounts in the Balance Sheet.
In Continental accounting, the cost of a good is reported as soon as a product is received into
stock. Additionally, a *single* Expense account is used for both input and output accounts in
the Balance Sheet.
Costing Methods
---------------
Below are the three costing methods that can be used in Odoo for inventory valuation.
- **Standard Price**: is the default costing method in Odoo. The cost of the product is manually
defined on the product form, and this cost is used to compute the valuation. Even if the purchase
price on a Purchase Order differs, the valuation will still use the cost defined on the product
form.
- **Average Cost (AVCO)**: calculates the valuation of a product based on the average cost of that
product, divided by the total number of available stock on-hand. With this costing method,
inventory valuation is *dynamic*, and constantly adjusts based on the purchase price of products.
- **First In First Out (FIFO)**: tracks the costs of incoming and outgoing items in real-time and
uses the real price of the products to change the valuation. The oldest purchase price is used as
the cost for the next good sold until an entire lot of that product is sold. When the next
inventory lot moves up in the queue, an updated product cost is used based on the valuation of
that specific lot. This method is arguably the most accurate inventory valuation method for a
variety of reasons, however, it's highly sensitive to input data and human error.
.. warning::
Changing the costing method greatly impacts inventory valuation. It's highly recommended to
consult an accountant first before making any adjustments here.
Configure automated inventory valuation in Odoo
-----------------------------------------------
Make changes to inventory valuation options by navigating to :menuselection:`Inventory -->
Configuration --> Product Categories`, and choose the category/categories where the automated
valuation method should apply.
.. note::
It is possible to use different valuation settings for different product categories.
Under the :guilabel:`Inventory Valuation` heading are two labels: :guilabel:`Costing Method` and
:guilabel:`Inventory Valuation`. Pick the desired :guilabel:`Costing Method` using the drop-down
menu (e.g. :guilabel:`Standard`, :guilabel:`Average Cost (AVCO)`, or :guilabel:`First In First Out
(FIFO)` and switch the :guilabel:`Inventory Valuation` to :guilabel:`Automated`.
.. seealso::
:doc:`Using the inventory valuation <using_inventory_valuation>`
.. note::
When choosing :guilabel:`Average Cost (AVCO)` as the :guilabel:`Costing Method`, the numerical
value in the :guilabel:`Cost` field for products in the respective product category will no
longer be editable, and will appear grayed out. The :guilabel:`Cost` amount will instead
automatically update based on the average purchase price both of inventory on hand and the costs
accumulated from validated purchase orders.
On the same screen, the :guilabel:`Account Stock Properties` fields will appear, as they are now
required fields given the change to automated inventory valuation. These accounts are defined as
follows:
- :guilabel:`Stock Valuation Account`: when automated inventory valuation is enabled on a product,
this account will hold the current value of the products.
- :guilabel:`Stock Input Account`: counterpart journal items for all incoming stock moves will be
posted in this account, unless there is a specific valuation account set on the source location.
This is the default value for all products in a given category, and can also be set directly on
each product.
- :guilabel:`Stock Output Account`: counterpart journal items for all outgoing stock moves will be
posted in this account, unless there is a specific valuation account set on the destination
location. This is the default value for all products in a given category, and can also be set
directly on each product.
Access reporting data generated by inventory valuation
------------------------------------------------------
To start, go to :menuselection:`Accounting --> Reporting --> Balance Sheet`. At the top of the
dashboard, change the :guilabel:`As of` field value to :guilabel:`Today`, and adjust the filtering
:guilabel:`Options` to :guilabel:`Unfold All` in order to see all of the latest data displayed,
all at once.
Under the parent :guilabel:`Current Assets` line item, look for the nested :guilabel:`Stock
Valuation Account` line item, where the total valuation of all of the inventory on hand is
displayed.
Access more specific information with the :guilabel:`Stock Valuation Account` drop-down menu, by
selecting either the :guilabel:`General Ledger` to see an itemized view of all of the journal
entries, or by selecting :guilabel:`Journal Items` to review all of the individualized journal
entries that were submitted to the account. As well, annotations to the :guilabel:`Balance Sheet`
can be added by choosing :guilabel:`Annotate`, filling in the text box, and clicking
:guilabel:`Save`.
.. image:: inventory_valuation_config/stock-valuation-breakdown-in-accounting.png
:align: center
:alt: See the full inventory valuation breakdown in Odoo Accounting app.

View File

@ -1,276 +0,0 @@
/* global Immutable, React */
/* global createAtom */
(function () {
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
return React.DOM.div(null, operations.map(function (op) {
var label = op.get('label'), operations = op.get('operations');
return React.DOM.label(
{
key: toKey(label),
style: { display: 'block' },
className: (operations === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations);
});
});
} else {
data.swap(function (d) {
return d.set('active', null) // keep visible in state map
.update('operations', function (ops) {
return ops.remove(operations);
});
});
}
}
}),
" ",
label
);
}));
}
});
var Chart = React.createClass({
render: function () {
var lastop = Immutable.Map(
(this.props.p.get('active') || Immutable.List()).map(function (op) {
return [op.get('account'), op.has('credit') ? 'credit' : 'debit'];
})
);
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th({ className: 'text-right' }, "Debit"),
React.DOM.th({ className: 'text-right' }, "Credit"),
React.DOM.th({ className: 'text-right' }, "Balance"))
),
React.DOM.tbody(
null,
this.accounts().map(function (data) {
var highlight = lastop.get(data.get('code'));
return React.DOM.tr(
{ key: data.get('code') },
React.DOM.th(null,
data.get('level') ? '\u2001 ' : '',
data.get('code'), ' ', data.get('label')),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'debit'
})
}, format(data.get('debit'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'credit'
})
}, format(data.get('credit'))),
React.DOM.td(
{ className: 'text-right' },
((data.get('debit') || data.get('credit'))
? format(data.get('debit') - data.get('credit'), 0)
: '')
)
);
})
)
)
);
},
accounts: function () {
var data = this.props.p.get('operations');
var totals = data.toIndexedSeq().flatten(true).reduce(function (acc, op) {
return acc
.updateIn([op.get('account'), 'debit'], function (d) {
return (d || 0) + op.get('debit', zero)(data);
})
.updateIn([op.get('account'), 'credit'], function (c) {
return (c || 0) + op.get('credit', zero)(data);
});
}, Immutable.Map());
return accounts.map(function (account) {
// for each account, add sum
return account.merge(
account.get('accounts').map(function (code) {
return totals.get(code, NULL);
}).reduce(function (acc, it) {
return acc.mergeWith(function (a, b) { return a + b; }, it, NULL);
})
);
});
}
});
data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, { p: next }),
document.getElementById('chart-controls-anglo-saxon'));
React.render(
React.createElement(Chart, { p: next }),
document.querySelector('.valuation-chart-anglo-saxon'));
});
document.addEventListener('DOMContentLoaded', function () {
var chart = document.querySelector('.valuation-chart-anglo-saxon');
if (!chart) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-controls-anglo-saxon');
chart.parentNode.insertBefore(controls, chart);
data.reset(Immutable.Map({
// last-selected operation
active: null,
// set of all currently enabled operations
operations: Immutable.OrderedSet()
}));
});
var NULL = Immutable.Map({ debit: 0, credit: 0 });
var ASSETS = {
code: 1,
label: "Assets",
BANK: { code: 11000, label: "Cash" },
ACCOUNTS_RECEIVABLE: { code: 13100, label: "Accounts Receivable" },
STOCK: { code: 14000, label: "Inventory" },
RAW_MATERIALS: { code: 14100, label: "Raw Materials Inventory" },
STOCK_OUT: { code: 14600, label: "Goods Issued Not Invoiced" },
TAXES_PAID: { code: 19000, label: "Deferred Tax Assets" }
};
var LIABILITIES = {
code: 2,
label: "Liabilities",
ACCOUNTS_PAYABLE: { code: 21000, label: "Accounts Payable" },
STOCK_IN: { code: 23000, label: "Goods Received Not Purchased" },
TAXES_PAYABLE: { code: 26200, label: "Deferred Tax Liabilities" }
};
var EQUITY = {
code: 3,
label: "Equity",
CAPITAL: { code: 31000, label: "Common Stock" }
};
var REVENUE = {
code: 4,
label: "Revenue",
SALES: { code: 41000, label: "Goods" },
};
var EXPENSES = {
code: 5,
label: "Expenses",
GOODS_SOLD: { code: 51100, label: "Cost of Goods Sold" },
MANUFACTURING_OVERHEAD: { code: 52000, label: "Manufacturing Overhead" },
PRICE_DIFFERENCE: { code: 53000, label: "Price Difference" }
};
var categories = Immutable.fromJS([ASSETS, LIABILITIES, EQUITY, REVENUE, EXPENSES], function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
var accounts = categories.toSeq().flatMap(function (cat) {
return Immutable.Seq.of(cat.set('level', 0)).concat(cat.filter(function (v, k) {
return k.toUpperCase() === k;
}).toIndexedSeq().map(function (acc) { return acc.set('level', 1) }));
}).map(function (account) { // add accounts: Seq<AccountCode> to each account
return account.set(
'accounts',
Immutable.Seq.of(account.get('code')).concat(
account.toIndexedSeq().map(function (val) {
return Immutable.Map.isMap(val) && val.get('code');
}).filter(function (val) { return !!val; })
)
);
});
var sale = 100,
cor = 50,
cor_tax = cor * 0.09,
tax = sale * 0.09,
total = sale + tax,
purchase = 52,
purchase_tax = 52 * 0.09;
var operations = Immutable.fromJS([{
label: "Vendor Bill (PO $50, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Supplier Goods Reception (PO $50, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, credit: constant(50) },
{ account: ASSETS.STOCK.code, debit: constant(50) },
]
}, {
label: "Vendor Bill (PO $48, Invoice $50)",
operations: [
{ account: EXPENSES.PRICE_DIFFERENCE.code, debit: constant(2) },
{ account: LIABILITIES.STOCK_IN.code, debit: constant(48) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Supplier Goods Reception (PO $48, Invoice $50)",
operations: [
{ account: LIABILITIES.STOCK_IN.code, credit: constant(48) },
{ account: ASSETS.STOCK.code, debit: constant(48) },
]
}, {
label: "Customer Invoice",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total) },
{ account: EXPENSES.GOODS_SOLD.code, debit: constant(cor) },
{ account: REVENUE.SALES.code, credit: constant(sale) },
{ account: ASSETS.STOCK_OUT.code, credit: constant(cor) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax) }
]
}, {
label: "Customer Shipping",
operations: [
{ account: ASSETS.STOCK_OUT.code, debit: constant(cor) },
{ account: ASSETS.STOCK.code, credit: constant(cor) }
]
}, {
label: "Production Order",
operations: [
{ account: ASSETS.STOCK.code, debit: constant(50) },
{ account: EXPENSES.MANUFACTURING_OVERHEAD.code, debit: constant(2) },
{ account: ASSETS.RAW_MATERIALS.code, credit: constant(52) }
]
}]);
function constant(val) { return function () { return val; }; }
var zero = constant(0);
function format(val, def) {
if (!val) { return def === undefined ? '' : def; }
if (val % 1 === 0) { return val; }
return val.toFixed(2);
}
})();

View File

@ -1,265 +0,0 @@
/* global Immutable, React */
/* global createAtom */
(function () {
'use strict';
var data = createAtom();
function toKey(s, postfix) {
if (postfix) {
s += ' ' + postfix;
}
return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-');
}
var Controls = React.createClass({
render: function () {
var state = this.props.p;
return React.DOM.div(null, operations.map(function (op) {
var label = op.get('label'), operations = op.get('operations');
return React.DOM.label(
{
key: toKey(label),
style: { display: 'block' },
className: (operations === state.get('active') ? 'highlight-op' : void 0)
},
React.DOM.input({
type: 'checkbox',
checked: state.get('operations').contains(operations),
onChange: function (e) {
if (e.target.checked) {
data.swap(function (d) {
return d.set('active', operations)
.update('operations', function (ops) {
return ops.add(operations);
});
});
} else {
data.swap(function (d) {
return d.set('active', null) // keep visible in state map
.update('operations', function (ops) {
return ops.remove(operations);
})
});
}
}
}),
" ",
label
);
}));
}
});
var Chart = React.createClass({
render: function () {
var lastop = Immutable.Map(
(this.props.p.get('active') || Immutable.List()).map(function (op) {
return [op.get('account'), op.has('credit') ? 'credit' : 'debit'];
})
);
return React.DOM.div(
null,
React.DOM.table(
{ className: 'table table-sm' },
React.DOM.thead(
null,
React.DOM.tr(
null,
React.DOM.th(),
React.DOM.th({ className: 'text-right' }, "Debit"),
React.DOM.th({ className: 'text-right' }, "Credit"),
React.DOM.th({ className: 'text-right' }, "Balance"))
),
React.DOM.tbody(
null,
this.accounts().map(function (data) {
var highlight = lastop.get(data.get('code'));
return React.DOM.tr(
{ key: data.get('code') },
React.DOM.th(null,
data.get('level') ? '\u2001 ' : '',
data.get('code'), ' ', data.get('label')),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'debit'
})
}, format(data.get('debit'))),
React.DOM.td({
className: React.addons.classSet({
'text-right': true,
'highlight-op': highlight === 'credit'
})
}, format(data.get('credit'))),
React.DOM.td(
{ className: 'text-right' },
((data.get('debit') || data.get('credit'))
? format(data.get('debit') - data.get('credit'), 0)
: '')
)
);
})
)
)
);
},
accounts: function () {
var data = this.props.p.get('operations');
var totals = data.toIndexedSeq().flatten(true).reduce(function (acc, op) {
return acc
.updateIn([op.get('account'), 'debit'], function (d) {
return (d || 0) + op.get('debit', zero)(data);
})
.updateIn([op.get('account'), 'credit'], function (c) {
return (c || 0) + op.get('credit', zero)(data);
});
}, Immutable.Map());
return accounts.map(function (account) {
// for each account, add sum
return account.merge(
account.get('accounts').map(function (code) {
return totals.get(code, NULL);
}).reduce(function (acc, it) {
return acc.mergeWith(function (a, b) { return a + b; }, it, NULL);
})
);
});
}
});
data.addWatch('chart', function (k, m, prev, next) {
React.render(
React.createElement(Controls, { p: next }),
document.getElementById('chart-controls-continental'));
React.render(
React.createElement(Chart, { p: next }),
document.querySelector('.valuation-chart-continental'));
});
document.addEventListener('DOMContentLoaded', function () {
var chart = document.querySelector('.valuation-chart-continental');
if (!chart) { return; }
var controls = document.createElement('div');
controls.setAttribute('id', 'chart-controls-continental');
chart.parentNode.insertBefore(controls, chart);
data.reset(Immutable.Map({
// last-selected operation
active: null,
// set of all currently enabled operations
operations: Immutable.OrderedSet()
}));
});
var NULL = Immutable.Map({ debit: 0, credit: 0 });
var ASSETS = {
code: 1,
label: "Assets",
BANK: { code: 11000, label: "Cash" },
ACCOUNTS_RECEIVABLE: { code: 13100, label: "Accounts Receivable" },
STOCK: { code: 14000, label: "Inventory" },
RAW_MATERIALS: { code: 14100, label: "Raw Materials Inventory" },
TAXES_PAID: { code: 19000, label: "Deferred Tax Assets" }
};
var LIABILITIES = {
code: 2,
label: "Liabilities",
ACCOUNTS_PAYABLE: { code: 21000, label: "Accounts Payable" },
TAXES_PAYABLE: { code: 26200, label: "Deferred Tax Liabilities" }
};
var EQUITY = {
code: 3,
label: "Equity",
CAPITAL: { code: 31000, label: "Common Stock" }
};
var REVENUE = {
code: 4,
label: "Revenue",
SALES: { code: 41000, label: "Goods" },
};
var EXPENSES = {
code: 5,
label: "Expenses",
PURCHASED_GOODS: { code: 51000, label: "Purchased Goods" },
PURCHASED_SERVICES: { code: 52000, label: "Purchased Services" },
INVENTORY_VARIATIONS: { code: 58000, label: "Inventory Variations" },
OTHER_OPERATING_EXPENSES: { code: 59000, label: "Other Operating Expenses" },
};
var categories = Immutable.fromJS([ASSETS, LIABILITIES, EQUITY, REVENUE, EXPENSES], function (k, v) {
return Immutable.Iterable.isIndexed(v)
? v.toList()
: v.toOrderedMap();
});
var accounts = categories.toSeq().flatMap(function (cat) {
return Immutable.Seq.of(cat.set('level', 0)).concat(cat.filter(function (v, k) {
return k.toUpperCase() === k;
}).toIndexedSeq().map(function (acc) { return acc.set('level', 1) }));
}).map(function (account) { // add accounts: Seq<AccountCode> to each account
return account.set(
'accounts',
Immutable.Seq.of(account.get('code')).concat(
account.toIndexedSeq().map(function (val) {
return Immutable.Map.isMap(val) && val.get('code');
}).filter(function (val) { return !!val; })
)
);
});
var sale = 100,
cor = 50,
cor_tax = cor * 0.09,
tax = sale * 0.09,
total = sale + tax,
purchase = 52,
purchase_tax = 52 * 0.09;
var operations = Immutable.fromJS([{
label: "Vendor Invoice (PO €50, Invoice €50)",
operations: [
{ account: EXPENSES.PURCHASED_GOODS.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Vendor Goods Reception (PO €50, Invoice €50)",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, credit: constant(50) },
{ account: ASSETS.STOCK.code, debit: constant(50) },
]
}, {
label: "Vendor Invoice (PO €48, Invoice €50)",
operations: [
{ account: EXPENSES.PURCHASED_GOODS.code, debit: constant(50) },
{ account: ASSETS.TAXES_PAID.code, debit: constant(50 * 0.09) },
{ account: LIABILITIES.ACCOUNTS_PAYABLE.code, credit: constant(50 * 1.09) },
]
}, {
label: "Vendor Goods Reception (PO €48, Invoice €50)",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, credit: constant(48) },
{ account: ASSETS.STOCK.code, debit: constant(48) },
]
}, {
label: "Customer Invoice (€100 + 9% tax)",
operations: [
{ account: ASSETS.ACCOUNTS_RECEIVABLE.code, debit: constant(total) },
{ account: REVENUE.SALES.code, credit: constant(sale) },
{ account: LIABILITIES.TAXES_PAYABLE.code, credit: constant(tax) }
]
}, {
label: "Customer Shipping",
operations: [
{ account: EXPENSES.INVENTORY_VARIATIONS.code, debit: constant(cor) },
{ account: ASSETS.STOCK.code, credit: constant(cor) }
]
}]);
function constant(val) { return function () { return val; }; }
var zero = constant(0);
function format(val, def) {
if (!val) { return def === undefined ? '' : def; }
if (val % 1 === 0) { return val; }
return val.toFixed(2);
}
})();