diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation.rst b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation.rst index 05704a93f9..b48c1b7a7c 100644 --- a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation.rst +++ b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation.rst @@ -7,6 +7,7 @@ Inventory valuation .. toctree:: :titlesonly: + inventory_valuation/cheat_sheet inventory_valuation/inventory_valuation_config inventory_valuation/using_inventory_valuation inventory_valuation/landed_costs diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet.rst b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet.rst new file mode 100644 index 0000000000..45c136d804 --- /dev/null +++ b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet.rst @@ -0,0 +1,624 @@ +:code-column: +:custom-css: valuation.css +:custom-js: misc.js,valuation-data.js,valuation-journal.js,valuation-accounting.js + +=================== +Inventory valuation +=================== + +.. role:: good +.. role:: meh +.. role:: bad + +.. important:: + This documentation is for Odoo 19 or later. + :ref:`Discover why we changed. ` + + +Costing Methods +=============== + +Odoo supports 3 costing methods configured in accounting's settings and, optionally, +the product's category. + +.. rst-class:: alternatives doc-aside + +Standard Cost: fixed unit cost, updated manually + .. 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 + +Average Cost: weighted average of all units + .. 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 + +FIFO: First In, First Out + .. 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 + +.. note:: Removal strategies also support :abbr:`LIFO (Last In, First Out)` and + :abbr:`FEFO (First Expiry, First Out)`, but they only impact which product is first picked, not + the valuation method. For example, you can pick using LIFO, but using Average Cost for valuation, + as LIFO is not allowed by :abbr:`IFRS (International Financial Reporting Standards)`. + + +Inventory vs Accounting +======================= + +.. rst-class:: inventory-app-paragraph + + :doc:`The inventory app ` keeps track of the inventory + value in real time as you **receive and deliver goods**. The reporting menu allows analysing + inventory on hand and values per company, location, product, etc. + +.. rst-class:: accounting-app-paragraph + + :doc:`The accounting app ` updates accounts when you receive + **invoices or bills**. Even though receipts and invoices differ, it’s not practical for + accountants to post journal entries for every inventory movement. So, they post a closing entry + to account for the difference between what has been invoiced and received/delivered. This closing + process happens usually once a year for SMEs, or once a month for larger companies. + +.. h:div:: feature-table doc-aside + + +------------------+------------+-----------+ + | | Accounting | Inventory | + +==================+============+===========+ + | Purchase Order | :meh:`/` | :meh:`/` | + +------------------+------------+-----------+ + | Receipt | :meh:`/` | :good:`✓` | + +------------------+------------+-----------+ + | Vendor Bill | :good:`✓` | :meh:`/` | + +------------------+------------+-----------+ + | Sales Order | :meh:`/` | :meh:`/` | + +------------------+------------+-----------+ + | Customer Invoice | :good:`✓` | :meh:`/` | + +------------------+------------+-----------+ + | Delivery | :meh:`/` | :good:`✓` | + +------------------+------------+-----------+ + | Closing Entry | :good:`✓` | :meh:`/` | + +------------------+------------+-----------+ + + +Accounting Methods +================== + +There are two accounting practices on how to maintain your accounts: + +**Periodic:** Post vendor bills as expenses by nature, and update stock valuation in the closing +entry by reducing expenses (stock variation). It’s the best practice in Europe. + +**Perpetual:** Post vendor bills as assets (stock valuation), report expenses when goods are sold +(cost of goods sold). It’s the best practice in countries that follow Anglo-Saxon accounting, like +the USA and India. + +.. role:: pink +.. role:: yellow +.. role:: green +.. role:: blue +.. role:: darkblue +.. role:: purple +.. role:: washed + +* :purple:`Stock Account`: defined on the product's category +* :yellow:`Stock Variation`: defined on the stock account +* :blue:`Expense/Cost of Goods Sold`: defined on the product/product category +* :green:`Inventory Adjustment`: defined on the Inventory Loss location + (optional, recommended for Anglo-Saxon accounting) +* :pink:`Cost of Production`: defined on the Production location + (optional, recommended for Anglo-Saxon accounting) +* :darkblue:`Expense`: defined on the stock account + (necessary only for perpetual Continental accounting) + +.. rst-class:: config-table + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + - + + - OPERATION + - BE Periodic + - US Perpetual + - US Periodic + - BE Perpetual + - Debit + - Credit + - + + - ADJUSTMENT + - + - :purple:`Stock` + - + - + - + - 40 + - + + - + - + - :green:`Loss` + - + - + - 40 + - + - + + - BILL + - :blue:`Expenses` + - :purple:`Stock` + - :blue:`COGS` + - :purple:`Stock` + - 500 + - + - + + - + - :washed:`Payable` + - :washed:`Payable` + - :washed:`Payable` + - :washed:`Payable` + - + - :washed:`500` + - + + - INVOICE + - + - :blue:`COGS` + - + - :blue:`Expenses` + - 400 + - + - + + - + - + - :purple:`Stock` + - + - :purple:`Stock` + - + - 400 + - + + - + - :washed:`Income` + - :washed:`Income` + - :washed:`Income` + - :washed:`Income` + - + - :washed:`750` + - + + - + - :washed:`Receivable` + - :washed:`Receivable` + - :washed:`Receivable` + - :washed:`Receivable` + - :washed:`750` + - + - + + - MANUFACTURING + - + - :purple:`Stock` + - + - + - 52 + - + - + + - + - + - :pink:`COP` + - + - + - + - 52 + - + + - + - + - :purple:`Stock` + - + - + - + - 50 + - + + - + - + - :pink:`COP` + - + - + - 50 + - + - + + - CLOSING + - :yellow:`Stock Variation` + - + - + - + - + - 62 [#copc-stockvar]_ + - + + - + - :purple:`Stock` + - + - + - + - 62 + - + - + + - + - + - :yellow:`Stock Variation` + - + - + - 0 [#aspl-stockvar]_ + - + - + + - + - + - :purple:`Stock` + - + - + - + - 0 + - + + - + - + - + - :yellow:`COGS` + - + - + - 100 [#aspc-cogs]_ + - + + - + - + - + - :purple:`Stock` + - + - 100 + - + - + + - + - + - + - :pink:`COP` + - + - + - 52 + - + + - + - + - + - :purple:`Stock` + - + - 52 + - + - + + - + - + - + - :pink:`COP` + - + - 50 + - + - + + - + - + - + - :purple:`Stock` + - + - + - 50 + - + + - + - + - + - :green:`Loss` + - + - 40 + - + - + + - + - + - + - :purple:`Stock` + - + - + - 40 + - + + - + - + - + - + - :blue:`Expenses` + - 38 [#copl-expenses]_ + - + - + + - + - + - + - + - :purple:`Stock` + - + - 38 + - + + - + - + - + - + - :yellow:`Stock Variation` + - + - 62 [#copl-stockvar]_ + - + + - + - + - + - + - :darkblue:`Expenses` + - 62 + - + +.. [#copc-stockvar] Inventory - Stock = 102 - 40 + +.. [#aspl-stockvar] Inventory - Stock = 0 + +.. [#aspc-cogs] Inventory - Stock = 140 - 40 + (What comes from loss/:abbr:`COP (Cost of Production)` is done separately.) + +.. [#copl-expenses] Inventory - Stock = 102 - 140 + +.. [#copl-stockvar] Stock Y - Stock Y-1 = 102 - 40 + +.. _accounting-entries: + +Accounting Entries +================== + +.. h:div:: accounting-entries doc-aside + + .. placeholder + + +.. _journal-entries: + +Journal Entries Configuration +============================= + + +.. h:div:: journal-entries doc-aside + + .. placeholder + + +.. _changes-in-19: + +Changes in Odoo 19 +================== + +Before Odoo 19, the Perpetual accounting method was implemented by posting real-time accounting +entries at each stock movement. That created a lot of journal items in accounting, which was an +issue for performance, general ledger clarity and auditability. + +Since Odoo 19, the Perpetual method impacts the stock valuation account at the invoice level. The +closing entry is then used to manage bills to receive, invoices to issue, deferred revenues, prepaid +expenses, and other gaps between inventory values and accounting ones. + +.. h:div:: feature-table doc-aside + + +-----------------------+--------------------------------+--------------------------------+ + | | Odoo 18 | Odoo 19 | + +=======================+================================+================================+ + | Periodic Continental | :meh:`Manual closing` | :good:`Automated closing` | + +-----------------------+--------------------------------+--------------------------------+ + | Periodic Anglo-Saxon | :bad:`Not supported` | :good:`Fully supported` | + +-----------------------+--------------------------------+--------------------------------+ + | Perpetual Continental | :meh:`Manual closing` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Perpetual Anglo-Saxon | :meh:`Manual closing` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Accounting valuation | :meh:`Requires inventory` | :good:`Accounting only` | + +-----------------------+--------------------------------+--------------------------------+ + | Perpetual Entries | :good:`Invoices + every moves` | :good:`Invoices + one closing` | + +-----------------------+--------------------------------+--------------------------------+ + | Invoices to issue | :bad:`✗` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Prepaid expenses | :bad:`✗` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Bills to receive | :bad:`✗` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Deferred revenues | :bad:`✗` | :good:`✓` | + +-----------------------+--------------------------------+--------------------------------+ + | Performance | :bad:`Slower` | :good:`Faster` | + +-----------------------+--------------------------------+--------------------------------+ + | General ledger | :good:`More journal entries` | :good:`Fewer journal entries` | + +-----------------------+--------------------------------+--------------------------------+ + + +What does it look like in Odoo? +=============================== + +In Inventory +------------ + +Open :menuselection:`Reporting --> Stock` to have a view on the current average cost for the product +and the total value of the stock. They can both be open to display details. + +.. image:: cheat_sheet/valuation-stock.png + + +AVCO product +~~~~~~~~~~~~ + +You can open :guilabel:`Unit Cost` to check all existing updates and their origins. In +:abbr:`AVCO (Average Cost)` this allows you to understand how the currently used value was +calculated. + +.. image:: cheat_sheet/avco-justification.png + +By opening :guilabel:`Total Value`, you can see all incoming quantities for which you still have a +remaining quantity and the value used for their valuation. In AVCO or standard cost, the used value +is always the current average unit cost. + +.. image:: cheat_sheet/avco-valuation.png + + +FIFO product +~~~~~~~~~~~~ + +In :abbr:`FIFO (First In, First Out)`, remaining units from each previous incoming move keep their +own individual valuation as shown in :guilabel:`Total Value`. + +.. image:: cheat_sheet/fifo-valuation.png + +In FIFO or AVCO remaining quantities from a previous incoming move can have their value adjusted if +necessary. + +.. image:: cheat_sheet/fifo-adjust.png + + +In Accounting +------------- + +Open :menuselection:`Review --> Inventory Valuation` to have a look at the difference between the +accounting stock value and the current inventory value recorded thanks to the incoming moves with a +remaining quantity. + +Click on :guilabel:`Generate Entry` to get a new accounting entry to review and post. + +.. image:: cheat_sheet/valuation-accounting.png + +Open +:menuselection:`Review --> Invoices not received, Invoices to be issued, Prepaid expenses and Deferred Revenues` +to easily record these entries. + +With Anglo-Saxon perpetual accounting, this will also help to distribute recorded Inventory +Variations to accounts such as Bill to Receive/:abbr:`GRNI (Goods Received Not Invoiced)` or +:abbr:`COGS (Cost of Goods Sold)` as shown in the :ref:`Accounting Entries ` +and :ref:`Journal Entries Configuration ` sections. diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-justification.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-justification.png new file mode 100644 index 0000000000..759a017732 Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-justification.png differ diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-valuation.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-valuation.png new file mode 100644 index 0000000000..114bee1012 Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/avco-valuation.png differ diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-adjust.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-adjust.png new file mode 100644 index 0000000000..cacf9d07aa Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-adjust.png differ diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-valuation.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-valuation.png new file mode 100644 index 0000000000..e214d19786 Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/fifo-valuation.png differ diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-accounting.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-accounting.png new file mode 100644 index 0000000000..715d85a11d Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-accounting.png differ diff --git a/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-stock.png b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-stock.png new file mode 100644 index 0000000000..673d491936 Binary files /dev/null and b/content/applications/inventory_and_mrp/inventory/product_management/inventory_valuation/cheat_sheet/valuation-stock.png differ diff --git a/static/css/valuation.css b/static/css/valuation.css new file mode 100644 index 0000000000..b693c43bd8 --- /dev/null +++ b/static/css/valuation.css @@ -0,0 +1,119 @@ +/* Used in valuation cheat_sheet.rst */ + +.alternatives-controls label { + display: block; +} +dl.alternatives > dt, dl.alternatives > dd { + display: none; +} +dl.alternatives > dd { + margin-left: 0; +} + +.values-table { + text-align: right; + tr > th:first-of-type { + text-align: left; + } +} + +.accounting-entries, .journal-entries, .values-table { + border: 1px solid #d5d5d5; + background-color: #f8f8f8; + /* width: 80%; */ + margin: 0px auto; +} +.accounting-entries { + th, td { + padding-top: 0px !important; + padding-bottom: 0px !important; + } + thead th, tr > td { + padding-left: 48px; + } + tbody th { + font-weight: normal; + } + .parent-line { background-color: #fafafa; } + .child-line { background-color: #f0f0f0; } +} +.entries-listing { + padding: .5rem; +} + +#accounting-entries-controls label, +#journaling-entries-controls label { + display: block; +} + +label:hover, .highlighter-list li:hover { + background-color: hsl(0, 0%, 94%); + cursor: pointer; +} +.related { + background-color: hsl(317deg 16% 90%) !important; + border: 1px solid #000000 !important; + transition: .3s; +} +.secondary { + background-color: hsl(180deg 67% 94%) !important; + transition: .3s; +} +.highlight-op { + background-color: hsl(317deg 16% 90%) !important; + transition: .3s; +} +.highlighter-target { + th { + font-weight: 400; + } + .related { + /*background-color: #eee !important;*/ + color: #7A436B !important; + } + .secondary { + background-color: #eee !important; + color: #7A436B !important; + } +} +.valuation-chart .highlight-op { + background-color: #030035; + border-bottom: 1px solid #000000 !important; +} + +.feature-table, .config-table { + table { + /* width: unset; */ + margin: auto; + white-space: nowrap; + text-align: center; + } + tbody td:first-child { + text-align: left; + } +} + +.accounting-app-paragraph:hover ~ .feature-table tr > td:first-child:has(+ td .good), +.inventory-app-paragraph:hover ~ .feature-table tr > td:first-child:has(+ td + td .good) +{ font-weight: bold; } + +.feature-table { + td { width: 32px; } + td:has(.good) { background-color: #d9ead3 !important; } + td:has(.meh) { background-color: #fce5cd !important; } + td:has(.bad) { background-color: #f4cccc !important; } +} +.config-table { + th, td { + padding-top: 0px !important; + padding-bottom: 0px !important; + } + td:has(.washed) { color: var(--bs-gray); } +} + +.pink, td:has(.pink) { background-color: #fcc0fc !important; } +.yellow, td:has(.yellow) { background-color: #fff2cc !important; } +.green, td:has(.green) { background-color: #d9ead3 !important; } +.blue, td:has(.blue) { background-color: #cfe2f3 !important; } +.darkblue, td:has(.darkblue) { background-color: #6d9eeb !important; } +.purple, td:has(.purple) { background-color: #d9d2e9 !important; } diff --git a/static/js/accounts.js b/static/js/accounts.js index f7ba5c7763..744cb3073e 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -1,6 +1,6 @@ /* global Immutable, React */ (function () { - // NOTE: used by cheat_sheet.rst + // NOTE: used by accounting cheat_sheet.rst 'use strict'; function highlight(primary, secondary) { diff --git a/static/js/chart-of-accounts.js b/static/js/chart-of-accounts.js index 960e8af53e..462bf5fdda 100644 --- a/static/js/chart-of-accounts.js +++ b/static/js/chart-of-accounts.js @@ -1,7 +1,7 @@ /* global Immutable, React */ /* global createAtom */ (function () { - // NOTE: used by cheat_sheet.rst + // NOTE: used by accounting cheat_sheet.rst 'use strict'; var data = createAtom(); diff --git a/static/js/entries.js b/static/js/entries.js index f6bbf20978..f30fdffb9d 100644 --- a/static/js/entries.js +++ b/static/js/entries.js @@ -2,7 +2,7 @@ /* global createAtom, findAncestor */ (function () { 'use strict'; - // NOTE: cheat_sheet.rst + // NOTE: used by accounting cheat_sheet.rst var data = createAtom(); data.addWatch('chart', function (k, m, prev, next) { diff --git a/static/js/misc.js b/static/js/misc.js index 1bc7cd3e3e..8272594726 100644 --- a/static/js/misc.js +++ b/static/js/misc.js @@ -6,7 +6,7 @@ }); function highlight() { - // NOTE: used by double-entry.rst + // NOTE: used by valuation cheat_sheet.rst $('.highlighter-list').each(function () { var $this = $(this), $target = $($this.data('target')); @@ -34,7 +34,7 @@ * - automatically select first control on startup */ function alternatives() { - // NOTE: used by double-entry.rst & valuation_methods pages + // NOTE: used by valuation cheat_sheet.rst $('dl.alternatives').each(function (index) { var $list = $(this), $contents = $list.children('dd'); @@ -51,7 +51,18 @@ label.appendChild(input); label.appendChild(document.createTextNode(' ')); - label.appendChild(document.createTextNode(this.textContent)); + + // Hack to bold the definition since we have to strip rST formatting + const [headText, tailText] = this.textContent.split(':', 2); + if (tailText) { + const bold = document.createElement('b'), + defined = document.createTextNode(`${headText}:`); + bold.appendChild(defined); + label.appendChild(bold); + } + + label.appendChild(document.createTextNode(tailText || headText)); + label.normalize(); return label; })) @@ -67,7 +78,7 @@ }); } function checks_handling() { - // NOTE: used by cheat_sheet.rst + // NOTE: used by accounting cheat_sheet.rst var $section = $('.checks-handling'); if (!$section.length) { return; } diff --git a/static/js/reconciliation.js b/static/js/reconciliation.js index 84c4ee07df..8925f95c0d 100644 --- a/static/js/reconciliation.js +++ b/static/js/reconciliation.js @@ -1,5 +1,5 @@ (function () { - // NOTE: cheat_sheet.rst + // NOTE: used by accounting cheat_sheet.rst document.addEventListener('DOMContentLoaded', function () { var $rec = $('#reconciliation .reconciliation-example'); if (!$rec.length) { return; } diff --git a/static/js/valuation-accounting.js b/static/js/valuation-accounting.js new file mode 100644 index 0000000000..f2f8d053a2 --- /dev/null +++ b/static/js/valuation-accounting.js @@ -0,0 +1,264 @@ +/* global Immutable, React */ +/* global createAtom */ +/* global VALUATION_{STANDARDS,METHODS,JOURNALS,ENTRIES,REVIEWS} */ +(function () { + 'use strict'; + // NOTE: used by valuation cheat_sheet.rst + + const mode = createAtom(['continental', 'periodic']); + const data = createAtom(); + + function watch (next) { + React.render( + React.createElement(Controls, { p: next }), + document.getElementById('accounting-entries-controls')); + React.render( + React.createElement(Chart, { p: next }), + document.querySelector('.accounting-entries')); + } + + data.addWatch('chart', (k, m, prev, next) => watch(next)); + mode.addWatch('chart', (k, m, prev, next) => watch(data.deref())); + + document.addEventListener('DOMContentLoaded', function () { + const chart = document.querySelector('.accounting-entries'); + if (!chart) { return; } + + const controls = document.createElement('div'); + controls.setAttribute('id', 'accounting-entries-controls'); + chart.parentNode.insertBefore(controls, chart); + + data.reset(Immutable.Map({ + // last-selected operation + active: null, + // set of all currently enabled operations + operations: Immutable.OrderedSet() + })); + }); + + function toKey(s, postfix) { + if (postfix) { + s += ' ' + postfix; + } + return s.replace(/[^0-9a-z ]/gi, '').toLowerCase().split(/\s+/).join('-'); + } + + const Controls = React.createClass({ + render: function () { + const state = this.props.p; + return React.DOM.div( + null, + React.DOM.b(null, 'Choose a standard:'), + VALUATION_STANDARDS.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.get('name') === mode.deref()[0], + onChange: function (e) { + const newValue = item.get('name'); + mode.reset([newValue, newValue === 'continental' ? 'periodic' : 'perpetual']); + } + }), + ' ', + item.get('text') + ); + }), + React.DOM.br(), + React.DOM.b(null, 'Choose an accounting method:'), + VALUATION_METHODS.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.get('name') === mode.deref()[1], + onChange: e => mode.swap(vals => [vals[0], item.get('name')]), + }), + ' ', + item.get('text') + ); + }), + React.DOM.br(), + React.DOM.b(null, 'Activate operations to see the impact:'), + React.DOM.br(), React.DOM.br(), + 'Operations', + VALUATION_ENTRIES.map(function (item, key) { + return React.DOM.label( + { + key: key, + style: { display: 'block' }, + className: (key === state.get('active') ? 'highlight-op' : void 0) + }, + React.DOM.input({ + type: 'checkbox', + checked: state.get('operations').contains(key), + onChange: function (e) { + if (e.target.checked) { + data.swap(d => d.set('active', key) + .update('operations', ops => ops.add(key))); + } else { + data.swap(d => d.set('active', null) + .update('operations', ops => ops.remove(key))); + } + } + }), + ' ', + item.get('title') + ); + }), + React.DOM.br(), + 'Review', + VALUATION_REVIEWS.map(function (item, key) { + // We bold the text if any of the operations in this review is + // relevant to the currently selected operations. + const boldable = item.getIn([...mode.deref(), 'operations']) + .some(function (op) { + if (!op.has('entries') && !op.has('except')) + return true; + const opset = state.get('operations').toSet(); + if (opset.isSuperset(op.get('entries', [])) + && opset.intersect(op.get('except', [])).isEmpty()) + return true; + }); + return React.DOM.label( + { + key: key, + style: { display: 'block' }, + className: (key === state.get('active') ? 'highlight-op' : void 0) + }, + React.DOM.input({ + type: 'checkbox', + checked: state.get('operations').contains(key), + onChange: function (e) { + if (e.target.checked) { + data.swap(d => d.set('active', key) + .update('operations', ops => ops.add(key))); + } else { + data.swap(d => d.set('active', null) + .update('operations', ops => ops.remove(key))); + } + } + }), + ' ', + boldable ? React.DOM.b(null, item.get('title')) : item.get('title'), + ); + }), + React.DOM.br(), + ); + } + }); + + const Chart = React.createClass({ + render: function () { + // Only used for highlighting cells. + const lastop = Immutable.Map( + this.props.p.get('active') + ? (VALUATION_ENTRIES.concat(VALUATION_REVIEWS) + .getIn([this.props.p.get('active'), ...mode.deref(), 'operations'], Immutable.List())) + .map(op => [VALUATION_JOURNALS.getIn([mode.deref()[0], ...op.get('account'), 'code']), + op.has('credit') ? 'credit' : 'debit']) + : Immutable.Map()); + 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) { + // Don't highlight the cell if it's going to be empty. + const highlight = lastop.get(data.get('code')), + debit = format(data.get('debit')), + credit = format(data.get('credit')); + return React.DOM.tr( + { + key: data.get('code'), + className: data.get('level') ? 'parent-line' : 'child-line', + }, + React.DOM.th( + null, + data.get('level') ? '\u2001 ' : '', + data.get('code') || '', ' ', data.get('title') + ), + React.DOM.td( + { className: React.addons.classSet({ + 'text-right': true, + 'highlight-op': debit ? highlight === 'debit' : void 0 }) }, + debit), + React.DOM.td( + { className: React.addons.classSet({ + 'text-right': true, + 'highlight-op': credit ? highlight === 'credit' : void 0 }) }, + credit), + React.DOM.td( + { className: 'text-right' }, + ((data.get('debit') || data.get('credit')) + ? format(data.get('debit') - data.get('credit'), 0) + : ''), + ) + ); + }) + ) + ) + ); + }, + accounts: function() { + const currentOperations = this.props.p.get('operations'); + if (!currentOperations) + return null; + const totals = VALUATION_ENTRIES.concat(VALUATION_REVIEWS) + .filter((val, key) => currentOperations.includes(key)) + .valueSeq() + .flatMap(entry => entry.getIn([...mode.deref(), 'operations'])) + .reduce(function (acc, op) { + // `entries' and `except' fields are explained in valuation-data.js (quod vide) + if (op.has('entries') || op.has('except')) { + const opset = currentOperations.toSet(); + if (!(opset.isSuperset(op.get('entries', [])) + && opset.intersect(op.get('except', [])).isEmpty())) { + return acc; + } + } + const code = VALUATION_JOURNALS.getIn([mode.deref()[0], ...op.get('account'), 'code']); + return acc + .updateIn([code, 'debit'], + d => (d || 0) + op.get('debit', 0)) + .updateIn([code, 'credit'], + c => (c || 0) + op.get('credit', 0)); + }, Immutable.Map()); + return accounts.get(mode.deref()[0]).map(account => + account.merge(account.get('accounts') + .map(code => totals.get(code, NULL)) + .reduce((acc, it) => acc.mergeWith((a, b) => a + b, it, NULL)))); + } + }); + + const NULL = Immutable.Map({ debit: 0, credit: 0 }); + const accounts = VALUATION_JOURNALS.map(method => method.toList().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; }) + ) + ); + })); + function format(val, def) { + if (!val) { return def === undefined ? '' : def; } + if (val % 1 === 0) { return val; } + return val.toFixed(2); + } +})(); diff --git a/static/js/valuation-data.js b/static/js/valuation-data.js new file mode 100644 index 0000000000..7a7b5b7806 --- /dev/null +++ b/static/js/valuation-data.js @@ -0,0 +1,1343 @@ +/* global Immutable */ +// NOTE: used by valuation-journal.js & valuation-accounting.js + +// Deep conversion with OrderedMap as default +Immutable.fromJSButOrdered = code => Immutable.fromJS(code, (key, value) => + Immutable.Iterable.isKeyed(value) ? value.toOrderedMap() : value.toList()); + +const VALUATION_STANDARDS = Immutable.fromJS([ + { name: 'continental', text: "Continental (Expenses = Purchase Expenses)" }, + { name: 'anglo_saxon', text: "Anglo-Saxon (Expenses = Cost of Goods Sold)" }, +]); + +const VALUATION_METHODS = Immutable.fromJS([ + { name: 'periodic', text: "Periodic: focuses on expenses by nature" }, + { name: 'perpetual', text: "Perpetual: focuses on inventory value" }, +]); + +const VALUATION_JOURNALS = Immutable.fromJSButOrdered({ + continental: { + EQUITY: { code: 1, title: "Equity and Liabilities" }, + ASSETS: { code: 2, title: "Fixed Assets" }, + STOCK: { + code: 3, title: "Stock (Current Assets)", + RAW: { code: 300000, title: "Inventory Raw Materials" }, + WIP: { code: 320000, title: "Work in Progress" }, + FINISHED_GOODS: { code: 330000, title: "Inventory Finished Goods" }, + GOODS_FOR_RESALE: { code: 340000, title: "Inventory Goods Purchase for Resale" }, + }, + LIABILITIES: { + code: 4, title: "Receivables and Payables", + CUSTOMERS: { code: 400000, title: "Customers" }, + VAT_RECOVERABLE: { code: 411000, title: "VAT Recoverable" }, + RECEIVABLE: { code: 414000, title: "Income Receivable" }, + SUPPLIERS: { code: 440000, title: "Suppliers" }, + INVOICES_UNRECEIVED: { code: 444000, title: "Invoices to Be Received" }, + VAT_PAYABLE: { code: 451000, title: "VAT Payable" }, + DEFERRED_CHARGES: { code: 490000, title: "Deferred Charges" }, + DEFERRED_INCOME: { code: 493000, title: "Deferred Income" }, + }, + CASH: { code: 5, title: "Financial Accounts and Cash" }, + EXPENSES: { + code: 6, title: "Expenses", + RAW: { code: 600000, title: "Raw Materials" }, + FINISHED: { code: 600200, title: "Cost of Finished Goods" }, + GOODS_FOR_RESALE: { code: 604000, title: "Goods for Resale" }, + CHANGE_IN_RAW: { code: 609000, title: "Decrease (Increase) in Stocks of Raw Materials" }, + CHANGE_IN_GOODS_FOR_RESALE: { code: 609400, title: "Decrease (Increase) in Stocks of Goods Purchased for Resale" }, + }, + REVENUES: { + code: 7, title: "Revenues", + SALES: { code: 700000, title: "Sales" }, + CHANGE_IN_WIP: { code: 712000, title: "Increase (Decrease) in Stocks of Work in Progress" }, + CHANGE_IN_FINISHED_GOODS: { code: 713000, title: "Increase (Decrease) in Stocks of Finished Goods" }, + }, + INITIAL_BALANCE: { code: 0, title: "General Balance for Inventory Initial Value" }, + }, + anglo_saxon: { + ASSETS: { + code: 1, title: "Assets", + BANK: { code: 101401, title: "Bank" }, + INVENTORY: { code: 110100, title: "Inventory" }, + RAW: { code: 110101, title: "Raw Materials Inventory" }, + MANUFACTURED: { code: 110102, title: "Manufactured Products Inventory" }, + COST: { code: 110400, title: "Cost of Production" }, + VARIATIONS: { code: 110600, title: "Inventory Variations" }, + RECEIVABLE: { code: 121000, title: "Accounts Receivable" }, + UNINVOICED: { code: 121200, title: "Uninvoiced Receivable" }, + PREPAID: { code: 128000, title: "Prepaid Expenses" }, + TAX_PAID: { code: 131000, title: "Tax Paid" }, + }, + LIABILITIES: { + code: 2, title: "Liabilities", + PAYABLE: { code: 211000, title: "Accounts Payable" }, + UNINVOICED: { + code: 211100, + title: "Bills to Receive/Goods Received Not Invoiced", + }, + DEFERRED: { code: 212000, title: "Deferred Revenue" }, + TAX_RECEIVED: { code: 251000, title: "Tax Received" }, + }, + EQUITY: { + code: 3, title: "Equity", + COMMON: { code: 303000, title: "Common Stock" }, + }, + REVENUES: { + code: 4, title: "Revenue", + SALES: { code: 400000, title: "Product Sales" }, + }, + EXPENSES: { + code: 5, title: "Expenses", + COST: { code: 500000, title: "Cost of Goods Sold" }, + PRICE_DIFFERENCE: { code: 530000, title: "Price Difference" }, + INVENTORY_LOSS: { code: 609100, title: "Inventory Loss Expense" }, + MISC: { code: 609000, title: "Miscellanous Expenses" }, + }, + INITIAL_BALANCE: { code: 0, title: "General Balance for Inventory Initial Value" }, + }, +}); + +/* Entry and review operations follow the following schema: + { name: { + title: "...", + continental: { + periodic: { + operations: [{ account: [...], // corresponds to the above table + debit OR credit: n, // mutually exclusive + entries: [...], // reviews only, optional + except: [...], // reviews only, optional + }, ...], + journal_operations: [...], // `closing' reviews only + explanation: [...], + configuration: [...], + }, + perpetual: { ditto }, + }, + anglo_saxon: { + periodic: { ditto }, + perpetual: { ditto }, + }, + }, + }, ... + See below for more on review operations. + */ +const VALUATION_ENTRIES = Immutable.fromJSButOrdered({ + initial_inventory: { + title: "Initial Inventory (Goods for Resale $50)", + continental: { + periodic: { + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['INITIAL_BALANCE'], credit: 50 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + ], + }, + perpetual: { + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['INITIAL_BALANCE'], credit: 50 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50 }, + { account: ['INITIAL_BALANCE'], credit: 50 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50 }, + { account: ['INITIAL_BALANCE'], credit: 50 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + ], + }, + }, + }, + supplier_reception: { + title: "Supplier Goods Reception (PO $50, Bill $50)", + continental: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods' receipts are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods receipts are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + anglo_saxon: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods' receipts are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods receipts are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + }, + supplier_bill: { + title: "Supplier Bill (PO $50, Bill $50)", + continental: { + periodic: { + operations: [ + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['LIABILITIES', 'SUPPLIERS'], credit: 54.5 }, + { account: ['LIABILITIES', 'VAT_RECOVERABLE'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['LIABILITIES', 'SUPPLIERS'], credit: 54.5 }, + { account: ['LIABILITIES', 'VAT_RECOVERABLE'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + "Account Payable Account: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['EXPENSES', 'COST'], debit: 50 }, + { account: ['LIABILITIES', 'PAYABLE'], credit: 54.5 }, + { account: ['ASSETS', 'TAX_PAID'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50 }, + { account: ['LIABILITIES', 'PAYABLE'], credit: 54.5 }, + { account: ['ASSETS', 'TAX_PAID'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + }, + }, + supplier_reception_extra: { + title: "Supplier Goods Reception (PO $48, Bill $50)", + continental: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods' receipts are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods receipts are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + anglo_saxon: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods' receipts are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods receipts are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + }, + supplier_bill_extra: { + title: "Supplier Bill (PO $48, Bill $50)", + continental: { + periodic: { + operations: [ + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['LIABILITIES', 'SUPPLIERS'], credit: 54.5 }, + { account: ['LIABILITIES', 'VAT_RECOVERABLE'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['LIABILITIES', 'SUPPLIERS'], credit: 54.5 }, + { account: ['LIABILITIES', 'VAT_RECOVERABLE'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + "Account Payable Account: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['EXPENSES', 'COST'], debit: 50 }, + { account: ['LIABILITIES', 'PAYABLE'], credit: 54.5 }, + { account: ['ASSETS', 'TAX_PAID'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50 }, + { account: ['LIABILITIES', 'PAYABLE'], credit: 54.5 }, + { account: ['ASSETS', 'TAX_PAID'], debit: 4.5 }, + ], + explanation: [], + configuration: [ + "Account Payable: defined on the supplier contact", + "Tax Account: defined on the account from the Purchase Taxes set on the product", + "Expense Account: defined on the product/product category", + ], + }, + }, + }, + customer_shipping: { + title: "Customer Shipping (SO $100, Invoice $100, Good Value $50)", + continental: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods shipments are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods shipments are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + anglo_saxon: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, goods shipments are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "In a perpetual inventory valuation, goods shipments are not directly posted in the accounting.", + "The inventory value is updated:", + "- when the invoice/bill is posted", + "- when entries are generated from stock valuation report at closing", + ], + configuration: [], + }, + }, + }, + customer_invoice: { + title: "Customer Invoice (SO $100, Invoice $100, Good Value $50)", + continental: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'CUSTOMERS'], debit: 109 }, + { account: ['LIABILITIES', 'VAT_PAYABLE'], credit: 9 }, + { account: ['REVENUES', 'SALES'], credit: 100 }, + ], + explanation: [], + configuration: [ + "Account Receivable Account: defined on the customer contact", + "Tax Account: defined on the account from the Sales Taxes set on the product", + "Income Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 50 }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50 }, + { account: ['LIABILITIES', 'CUSTOMERS'], debit: 109 }, + { account: ['LIABILITIES', 'VAT_PAYABLE'], credit: 9 }, + { account: ['REVENUES', 'SALES'], credit: 100 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + "Expense/COGS Account: defined on the product/product category", + "Account Receivable Account: defined on the customer contact", + "Tax Account: defined on the account from the Sales Taxes set on the product", + "Income Account: defined on the product/product category", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['ASSETS', 'RECEIVABLE'], debit: 109 }, + { account: ['LIABILITIES', 'TAX_RECEIVED'], credit: 9 }, + { account: ['REVENUES', 'SALES'], credit: 100 }, + ], + explanation: [], + configuration: [ + "Account Receivable Account: defined on the customer contact", + "Tax Account: defined on the account from the Sales Taxes set on the product", + "Income Account: defined on the product/product category", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], credit: 50 }, + { account: ['EXPENSES', 'COST'], debit: 50 }, + { account: ['ASSETS', 'RECEIVABLE'], debit: 109 }, + { account: ['LIABILITIES', 'TAX_RECEIVED'], credit: 9 }, + { account: ['REVENUES', 'SALES'], credit: 100 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + "Expense/COGS Account: defined on the product/product category", + "Account Receivable Account: defined on the customer contact", + "Tax Account: defined on the account from the Sales Taxes set on the product", + "Income Account: defined on the product/product category", + ], + }, + }, + }, + manufacturing_order: { + title: "Manufacturing Order (Finished Product at $52, Component at $50)", + continental: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation manufacturing orders are not directly posted in the accounting.", + "The inventory value is updated when entries are genarted from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "No 'Cost of Production' account is set on the production location, therefore variations in" + + " inventories of manufactured goods and components will be recorded at closing.", + ], + configuration: [], + }, + }, + anglo_saxon: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation manufacturing orders are not directly posted in the accounting.", + "The inventory value is updated when entries are genarted from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'MANUFACTURED'], debit: 52 }, + { account: ['ASSETS', 'COST'], credit: 52 }, + { account: ['ASSETS', 'RAW'], credit: 50 }, + { account: ['ASSETS', 'COST'], debit: 50 }, + ], + explanation: [], + configuration: [ + "Stock Account for manufactured product: defined on the product category", + "Stock Account for component: defined on the product category", + "Cost of Production Account: defined on the production location", + ], + }, + }, + }, + inventory_loss: { + title: "Inventory Loss (Scrap $30)", + continental: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, inventory adjustments are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [], + explanation: [ + "No 'Inventory Loss' account is set on the inventory loss location, therefore variations in" + + " inventories coming from inventory adjustments will be recorded at closing.", + ], + configuration: [], + }, + }, + anglo_saxon: { + periodic: { + operations: [], + explanation: [ + "In a periodic inventory valuation, inventory adjustments are not directly posted in the accounting.", + "The inventory value is updated when entries are generated from stock valuation report at closing.", + ], + configuration: [], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'INVENTORY'], credit: 30 }, + { account: ['EXPENSES', 'INVENTORY_LOSS'], debit: 30 }, + ], + explanation: [], + configuration: [ + "Stock Account: defined on the product category", + "Inventory Adjustment Account: defined on the inventory loss location", + ], + }, + }, + }, +}); + +/* >Hic sunt dracones< + * + * Reviews are treated almost identically to entry operations in both scripts, + * except for the following difference in behaviour. + * + * [valuation-journal.js] + * The journal chart will use `journal_operations' (unique to `closing') if it + * exists, falling back to `operations' as normal otherwise. It will ignore the + * `entries' and `except' fields. + * + * [valuation-accounting.js] + * The accounting chart will ignore `journal_operations'. If an `entries' or + * `except' field is defined, it will only use that operation if said fields + * match the currently active operations (see above for keys). It must match + * ALL in `entries' and NONE in `except' in order to be used. The commented out + * parts in the `explanation' field (otherwise unused here) are relevant only + * for this chart. + */ +const VALUATION_REVIEWS = Immutable.fromJSButOrdered({ + unreceived: { + title: "Invoices not received", + continental: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'INVOICES_UNRECEIVED'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['LIABILITIES', 'INVOICES_UNRECEIVED'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that expenses are recognized in the period in which they are incurred, regardless of when cash is paid.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "GRNI Account: defined when generating accrual entries from 'Invoices not received'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'INVOICES_UNRECEIVED'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['LIABILITIES', 'INVOICES_UNRECEIVED'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that expenses are recognized in the period in which they are incurred, regardless of when cash is paid.", + "They are reverted after the period we are closing.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "GRNI Account: defined when generating accrual entries from 'Invoices not received'", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'UNINVOICED'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['EXPENSES', 'COST'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['LIABILITIES', 'UNINVOICED'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'COST'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that expenses are recognized in the period in which they are incurred, regardless of when cash is paid.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "GRNI Account: defined when generating accrual entries from 'Invoices not received'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'UNINVOICED'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['LIABILITIES', 'UNINVOICED'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + ], + explanation: [ + "Accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that revenues/expenses are recognized in the period in which they are earned/incurred, regardless of" + + " when cash is received / paid.", + "They are reverted after the period we are closing.", + "From a stock point of view, it also enables us to specify a part of the origin of stock variations (between" + + " Accounting Stock and Inventory Stock) recorded from the stock valuation report.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + ], + configuration: [ + "Inventory Variation Account: defined on the used Stock Account for the product category", + "GRNI Account: defined when generating accrual entries from 'Invoices not received'", + ], + }, + }, + }, + uninvoiced: { + title: "Invoices to be issued", + continental: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'RECEIVABLE'], debit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['REVENUES', 'SALES'], credit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that revenues are recognized in the period in which they are earned, regardless of when cash is received.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Uninvoiced Receivable Account: defined when generating accrual entries from 'Invoices to be issued'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'RECEIVABLE'], debit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['REVENUES', 'SALES'], credit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that revenues are recognized in the period in which they are earned, regardless of when cash is received.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Uninvoiced Receivable Account: defined when generating accrual entries from 'Invoices to be issued'", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['ASSETS', 'UNINVOICED'], debit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['REVENUES', 'SALES' ], credit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that revenues are recognized in the period in which they are earned, regardless of" + + " when cash is received.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Uninvoiced Receivable Account: defined when generating accrual entries from 'Invoices to be issued'", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'UNINVOICED'], debit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['REVENUES', 'SALES'], credit: 100, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['EXPENSES', 'COST'], debit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + ], + explanation: [ + "These accrual entries are adjusting journal entries made at the end of an accounting period to ensure" + + " that revenues/expenses are recognized in the period in which they are earned/incurred, regardless of" + + " when cash is received/paid.", + "From a stock point of view, it also enables us to specify a part of the origin of stock variations (between" + + " Accounting Stock and Inventory Stock) recorded from the stock valuation report.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + ], + configuration: [ + "Inventory Variation Account: defined on the used Stock Account for the product category", + "Uninvoiced Receivable Account: defined when generating accrual entries from 'Invoices to be issued'", + "Revenue Account: defined on the product/product category", + "Expense/COGS Account: defined on the product/product category", + ], + }, + }, + }, + prepaid: { + title: "Prepaid Expenses", + continental: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED_CHARGES'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['LIABILITIES', 'DEFERRED_CHARGES'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + ], + explanation: [ + "Prepaid expenses are journal entries made during closing when a supplier bill is received before goods" + + " or services are received, so the amount is recorded as an asset until the expense is recognized.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + "2) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "Prepaid Expense Account: defined when generating entries from 'Prepaid Expenses'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED_CHARGES'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['LIABILITIES', 'DEFERRED_CHARGES'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + ], + explanation: [ + "Prepaid expenses are journal entries made during closing when a supplier bill is received before goods" + + " or services are received, so the amount is recorded as an asset until the expense is recognized.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + "2) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "Prepaid Expense Account: defined when generating entries from 'Prepaid Expenses'", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['ASSETS', 'PREPAID'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['EXPENSES', 'COST'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['ASSETS', 'PREPAID'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['EXPENSES', 'COST'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + ], + explanation: [ + "Prepaid expenses are journal entries made during closing when a supplier bill is received before goods" + + " or services are received, so the amount is recorded as an asset until the expense is recognized.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + "2) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "Prepaid Expense Account: defined when generating entries from 'Prepaid Expenses'", + ], + }, + perpetual: { + operations: [ + { account: ['ASSETS', 'PREPAID'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['ASSETS', 'PREPAID'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + ], + explanation: [ + "Prepaid expenses are journal entries made during closing when a supplier bill is received before goods" + + " or services are received, so the amount is recorded as an asset until the expense is recognized.", + "From a stock point of view, it also enables us to specify a part of the origin of stock variations (between" + + " Accounting Stock and Inventory Stock) recorded from the stock valuation report.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + "2) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + ], + configuration: [ + "Inventory Variation Account: defined on the used Stock Account for the product category", + "Prepaid Expense Account: defined when generating entries from 'Prepaid Expenses'", + ], + }, + }, + }, + deferred: { + title: "Deferred Revenues", + continental: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED_INCOME'], credit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['REVENUES', 'SALES'], debit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "Deferred revenues are journal entries made during closing when an invoice has been issued but goods or" + + " services are not delivered yet, so the amount has to be recorded as a liability until the revenue is earned.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Deferred Revenue Account: defined when generating accrual entries from 'Deferred Revenues'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED_INCOME'], credit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['REVENUES', 'SALES'], debit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "Deferred revenues are journal entries made during closing when an invoice has been issued but goods or" + + " services are not delivered yet, so the amount has to be recorded as a liability until the revenue is earned.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Deferred Revenue Account: defined when generating accrual entries from 'Deferred Revenues'", + ], + }, + }, + anglo_saxon: { + periodic: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED'], credit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['REVENUES', 'SALES'], debit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "Deferred revenues are journal entries made during closing when an invoice has been issued but goods or" + + " services are not delivered yet, so the amount has to be recorded as a liability until the revenue is earned.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Revenue Account: defined on the product/product category", + "Deferred Revenue Account: defined when generating accrual entries from 'Deferred Revenues'", + ], + }, + perpetual: { + operations: [ + { account: ['LIABILITIES', 'DEFERRED'], credit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['REVENUES', 'SALES'], debit: 100, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['EXPENSES', 'COST'], credit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "Deferred revenues are journal entries made during closing when an invoice has been issued but goods or" + + " services are not delivered yet, so the amount has to be recorded as a liability until the revenue is earned.", + "From a stock point of view, it also enables us to specify a part of the origin of stock variations (between" + + " Accounting Stock and Inventory Stock) recorded from the stock valuation report.", + "They are reverted after the period we are closing.", + "Related operation:", + "Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Inventory Variation Account: defined on the used Stock Account for the product category", + "Deferred Revenue Account: defined when generating accrual entries from 'Deferred Revenues'", + "Revenue Account: defined on the product/product category", + "Expense/COGS Account : defined on the product/product category", + ], + }, + }, + }, + closing: { + title: "Closing Entry from Stock Valuation Report", + continental: { + periodic: { + journal_operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 20 }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 20 }, + { account: ['STOCK', 'FINISHED_GOODS'], debit: 52 }, + { account: ['STOCK', 'RAW'], credit: 50 }, + { account: ['REVENUES', 'CHANGE_IN_FINISHED_GOODS'], credit: 52 }, + { account: ['EXPENSES', 'CHANGE_IN_RAW'], debit: 50 }, + ], + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_reception'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['customer_shipping'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], debit: 50, + entries: ['customer_shipping'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 30, + entries: ['inventory_loss'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], debit: 30, + entries: ['inventory_loss'] }, + { account: ['STOCK', 'FINISHED_GOODS'], debit: 52, + entries: ['manufacturing_order'] }, + { account: ['STOCK', 'RAW'], credit: 50, + entries: ['manufacturing_order'] }, + { account: ['REVENUES', 'CHANGE_IN_FINISHED_GOODS'], credit: 52, + entries: ['manufacturing_order'] }, + { account: ['EXPENSES', 'CHANGE_IN_RAW'], debit: 50, + entries: ['manufacturing_order'] }, + ], + explanation: [ + "At closing, Stock Valuation must be updated to reflect reality. This will be done by generating entries from" + + " the Stock Valuation Report. This will recognize value coming from operations such as the following ones:", + "1) Supplier Goods Reception (PO $50, Bill $50)", + "2) Supplier Goods Reception (PO $48, Bill $50)", + "3) Customer Shipping (SO $100, Invoice $100, Good Value $50)", + "4) Inventory Loss (Scrap $30)", + "5) Manufacturing Order (Finished Product at $52, Component at $50)", + ], + configuration: [ + "Stock Account: defined on the product category", + "Inventory Variation Account: defined on the used Stock Account", + ], + }, + perpetual: { + journal_operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 18 }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 18 }, + { account: ['STOCK', 'FINISHED_GOODS'], debit: 52 }, + { account: ['EXPENSES', 'FINISHED'], credit: 52 }, + { account: ['STOCK', 'RAW'], credit: 50 }, + { account: ['EXPENSES', 'RAW'], debit: 50 }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 18 }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 18 }, + { account: ['EXPENSES', 'RAW'], credit: 50 }, + { account: ['EXPENSES', 'CHANGE_IN_RAW'], debit: 50 }, + { account: ['EXPENSES', 'FINISHED'], debit: 52 }, + { account: ['REVENUES', 'CHANGE_IN_FINISHED_GOODS'], credit: 52 }, + ], + operations: [ + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 30, + entries: ['inventory_loss'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 30, + entries: ['inventory_loss'] }, + { account: ['STOCK', 'FINISHED_GOODS'], debit: 52, + entries: ['manufacturing_order'] }, + { account: ['EXPENSES', 'FINISHED'], credit: 52, + entries: ['manufacturing_order'] }, + { account: ['STOCK', 'RAW'], credit: 50, + entries: ['manufacturing_order'] }, + { account: ['EXPENSES', 'RAW'], debit: 50, + entries: ['manufacturing_order'] }, + // Adjustment + // a) Purchased good for resale + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_reception'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], debit: 50, + entries: ['customer_shipping'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['customer_shipping'] }, + { account: ['EXPENSES', 'CHANGE_IN_GOODS_FOR_RESALE'], debit: 30, + entries: ['inventory_loss'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 30, + entries: ['inventory_loss'] }, + // b) Component + { account: ['EXPENSES', 'RAW'], credit: 50, + entries: ['manufacturing_order'] }, + { account: ['EXPENSES', 'CHANGE_IN_RAW'], debit: 50, + entries: ['manufacturing_order'] }, + // c) Finished product + { account: ['EXPENSES', 'FINISHED'], debit: 52, + entries: ['manufacturing_order'] }, + { account: ['REVENUES', 'CHANGE_IN_FINISHED_GOODS'], credit: 52, + entries: ['manufacturing_order'] }, + // Second half, excluded from journal chart + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['STOCK', 'GOODS_FOR_RESALE'], debit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['EXPENSES', 'GOODS_FOR_RESALE'], credit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "At closing Accounting Stock and Inventory Stock must have the same value.", + "A difference could come from cases such as the ones listed below and this will be corrected thanks to" + + " the Stock Valuation Report closing entry.", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + "3) Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + "4) Inventory Loss (Scrap $30)", + "5) Manufacturing Order (Finished Product at $52, Component at $50)", + "At closing, an adjusting entry is created to ensure that the values are correctly recorded in the" + + " variation and expenses accounts according to continental accounting logic. Indeed, even though" + + " expenses are recorded according to the COGS logic of Anglo-Saxon accounting during the period," + + " at closing the expenses recorded should represent what was actually purchased during this period.", + "The variation is known thanks to the calculation (Stock Account y - Stock Account y-1) for each of" + + " the following categories:", + "a. Purchased good for resale", + "b. Component", + "c. Finished product", + // "6) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + // "7) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + // "8) Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Expense Account: defined on the product/product category", + "Stock Account: defined on the product category", + "Inventory Variation Account: defined on the used Stock Account", + "Expense Account for adjusting entry: defined on the used Stock Account", + ], + }, + }, + anglo_saxon: { + periodic: { + journal_operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50 }, + { account: ['EXPENSES', 'COST'], credit: 50 }, + { account: ['ASSETS', 'INVENTORY'], credit: 30 }, + { account: ['EXPENSES', 'INVENTORY_LOSS'], debit: 30 }, + { account: ['ASSETS', 'MANUFACTURED'], debit: 52 }, + { account: ['ASSETS', 'COST'], credit: 52 }, + { account: ['ASSETS', 'RAW'], credit: 50 }, + { account: ['ASSETS', 'COST'], debit: 50 }, + ], + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50, + entries: ['supplier_reception'] }, + { account: ['EXPENSES', 'COST'], credit: 50, + entries: ['supplier_reception'] }, + { account: ['ASSETS', 'INVENTORY'], debit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['EXPENSES', 'COST'], credit: 50, + entries: ['supplier_reception_extra', 'supplier_bill_extra'] }, + { account: ['ASSETS', 'INVENTORY'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['EXPENSES', 'COST'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['ASSETS', 'INVENTORY'], credit: 50, + entries: ['customer_shipping'] }, + { account: ['EXPENSES', 'COST'], debit: 50, + entries: ['customer_shipping'] }, + { account: ['ASSETS', 'INVENTORY'], credit: 30, + entries: ['inventory_loss'] }, + { account: ['EXPENSES', 'INVENTORY_LOSS'], debit: 30, + entries: ['inventory_loss'] }, + { account: ['ASSETS', 'MANUFACTURED'], debit: 52, + entries: ['manufacturing_order'] }, + { account: ['ASSETS', 'COST'], credit: 52, + entries: ['manufacturing_order'] }, + { account: ['ASSETS', 'RAW'], credit: 50, + entries: ['manufacturing_order'] }, + { account: ['ASSETS', 'COST'], debit: 50, + entries: ['manufacturing_order'] }, + ], + explanation: [ + "At closing, Stock Valuation must be updated to reflect reality. This will be done by generating entries from" + + " the Stock Valuation Report. This will recognize value coming from operations such as the following ones:", + "1) Supplier Goods Reception (PO $50, Bill $50)", + "2) Supplier Goods Reception (PO $48, Bill $50)", + "3) Customer Shipping (SO $100, Invoice $100, Good Value $50)", + "4) Inventory Loss (Scrap $30)", + "5) Manufacturing Order (Finished Product at $52, Component at $50)", + ], + configuration: [ + "Stock Account: defined on the product category", + "Inventory Variation Account: defined on the used Stock Account", + "Inventory Adjustment Account: defined on the Inventory Loss location", + "Cost of Production Account: defined on the Production location", + ], + }, + perpetual: { + journal_operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 48 }, + { account: ['ASSETS', 'VARIATIONS'], credit: 48 }, + ], + operations: [ + { account: ['ASSETS', 'INVENTORY'], debit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 50, + entries: ['supplier_reception'], + except: ['supplier_bill'] }, + { account: ['ASSETS', 'INVENTORY'], debit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 48, + entries: ['supplier_reception_extra'], + except: ['supplier_bill_extra'] }, + { account: ['ASSETS', 'INVENTORY'], credit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 50, + entries: ['customer_shipping'], + except: ['customer_invoice'] }, + { account: ['ASSETS', 'INVENTORY'], credit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 50, + entries: ['supplier_bill'], + except: ['supplier_reception'] }, + { account: ['ASSETS', 'INVENTORY'], credit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['ASSETS', 'VARIATIONS'], debit: 50, + entries: ['supplier_bill_extra'], + except: ['supplier_reception_extra'] }, + { account: ['ASSETS', 'INVENTORY'], debit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + { account: ['ASSETS', 'VARIATIONS'], credit: 50, + entries: ['customer_invoice'], + except: ['customer_shipping'] }, + ], + explanation: [ + "At closing, Accounting Stock and Inventory Stock must have the same value.", + "A difference could come from cases such as the ones listed below and this will be corrected thanks to" + + " the Stock Valuation Report closing entry.", + "Related operations:", + "1) Supplier Goods Reception (PO $50, Bill $50) is done but no bill is received yet", + "2) Supplier Goods Reception (PO $48, Bill $50) is done but no bill is received yet", + "3) Customer Shipping (SO $100, Invoice $100, Good Value $50) is done but no invoice is sent yet", + // "4) Supplier Goods Reception (PO $50, Bill $50) is not done but the bill is received", + // "5) Supplier Goods Reception (PO $48, Bill $50) is not done but the bill is received", + // "6) Customer Shipping (SO $100, Invoice $100, Good Value $50) is not done but the invoice is sent", + ], + configuration: [ + "Stock Account: defined on the product category", + "Inventory Variation Account: defined on the used Stock Account", + ], + }, + }, + }, +}); diff --git a/static/js/valuation-journal.js b/static/js/valuation-journal.js new file mode 100644 index 0000000000..74920bd0b6 --- /dev/null +++ b/static/js/valuation-journal.js @@ -0,0 +1,173 @@ +/* global Immutable, React */ +/* global createAtom, findAncestor */ +/* global VALUATION_{STANDARDS,METHODS,JOURNALS,ENTRIES,REVIEWS} */ +(function () { + 'use strict'; + // NOTE: used by valuation cheat_sheet.rst + + const mode = createAtom(['continental', 'periodic']); + const data = createAtom(Immutable.Map()); + + function watch (next) { + React.render( + React.createElement(Controls, { entry: next }), + document.getElementById('journaling-entries-controls')); + React.render( + React.createElement(FormatEntry, { entry: next }), + document.querySelector('.journal-entries')); + } + + data.addWatch('chart', (k, m, prev, next) => watch(next.getIn(mode.deref()))); + mode.addWatch('chart', (k, m, prev, next) => watch(data.deref().getIn(next, Immutable.Map()))); + + document.addEventListener('DOMContentLoaded', function () { + const entriesSection = findAncestor(document.querySelector('.journal-entries'), 'section'); + if (!entriesSection) { return; } + + const controls = document.createElement('div'); + controls.setAttribute('id', 'journaling-entries-controls'); + entriesSection.insertBefore(controls, entriesSection.lastElementChild); + + data.reset(VALUATION_ENTRIES.first().getIn(['continental', 'periodic'])); + }); + + const Controls = React.createClass({ + render: function () { + const entry = this.props.entry; + return React.DOM.div( + null, + React.DOM.b(null, 'Choose a standard:'), + VALUATION_STANDARDS.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.get('name') === mode.deref()[0], + onChange: function (e) { + const newValue = item.get('name'); + mode.reset([newValue, newValue === 'continental' ? 'periodic' : 'perpetual']); + } + }), + ' ', + item.get('text') + ); + }), + React.DOM.br(), + React.DOM.b(null, 'Choose an accounting method:'), + VALUATION_METHODS.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.get('name') === mode.deref()[1], + onChange: e => mode.swap(vals => [vals[0], item.get('name')]), + }), + ' ', + item.get('text') + ); + }), + React.DOM.br(), + React.DOM.b(null, 'Activate operations to see the impact:'), + React.DOM.br(), React.DOM.br(), + 'Operations', + VALUATION_ENTRIES.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.getIn(mode.deref()) === entry, + onChange: e => data.reset(item), + }), + ' ', + item.get('title') + ); + }), + React.DOM.br(), + 'Review', + VALUATION_REVIEWS.map(function (item, index) { + return React.DOM.label( + { key: index }, + React.DOM.input({ + type: 'radio', + checked: item.getIn(mode.deref()) === entry, + onChange: e => data.reset(item), + }), + ' ', + item.get('title') + ); + }), + React.DOM.br(), + ); + } + }); + const FormatEntry = React.createClass({ + render: function () { + const entry = this.props.entry; + return React.DOM.div( + null, + React.DOM.table( + { className: 'table table-sm d-c-table' }, + React.DOM.thead( + null, + React.DOM.tr( + null, + React.DOM.th(), + React.DOM.th(null, "Debit"), + React.DOM.th(null, "Credit"), + ) + ), + React.DOM.tbody( + null, + entry && entry.get('journal_operations', entry.get('operations', [])).map(this.renderRow) + ) + ), + React.createElement(Listing, { + heading: "Explanation", + items: entry && entry.get('explanation'), + }), + React.createElement(Listing, { + heading: "Configuration", + items: entry && entry.get('configuration'), + }) + ); + }, + renderRow: function (entry, index) { + if (!entry) { + return React.DOM.tr( + { key: 'spacer-' + index }, + React.DOM.td({ colSpan: 3 }, "\u00A0") + ); + } + const journalEntry = VALUATION_JOURNALS.getIn([mode.deref()[0], ...entry.get('account')]); + const title = journalEntry.get('title'); + // Don't display 0 for 'General Balance for Inventory Initial Value' + const code = journalEntry.get('code') || ''; + return React.DOM.tr( + { key: index }, + React.DOM.td(null, `${code} ${title}`), + React.DOM.td(null, entry.get('debit')), + React.DOM.td(null, entry.get('credit')) + ); + } + }); + const Listing = React.createClass({ + render: function () { + if (!this.props.items || this.props.items.isEmpty()) { + return React.DOM.div(); + } + const items = this.props.items; + const idx = items.indexOf(null); + if (idx !== -1) { + console.log(items.slice(idx + 1).deref()); + items = items.take(idx); + } + return React.DOM.div( + { className: 'entries-listing' }, + React.DOM.h4(null, this.props.heading, ':'), + items.map(function (item, index) { + return React.DOM.p({ key: index }, item); + }) + ); + } + }); +}());