mirror of
https://github.com/odoo/documentation.git
synced 2025-12-12 07:29:27 +07:00
[ADD] Inventory: add stock valuation cheat sheet
An adaptation of the venerable business memento from 8.0 to the new inventory valuation mechanics of Odoo 19.0 by scavenging and cobbling together of the scripts `entries.js` and `coa-valuation.js`. The shared data is kept in a separate file. Additionally, we remove the old inventory valuation documentation. Task ID: 5107300 closes odoo/documentation#14906 Signed-off-by: Felicia Kuan (feku) <feku@odoo.com>
This commit is contained in:
committed by
“Dallas”
parent
c97a8e6bad
commit
912dde6d26
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}))
|
||||
@@ -65,9 +76,10 @@
|
||||
})
|
||||
.find('input:first').click();
|
||||
});
|
||||
$('.alternatives-note').insertAfter($('.alternatives-controls'));
|
||||
}
|
||||
function checks_handling() {
|
||||
// NOTE: used by cheat_sheet.rst
|
||||
// NOTE: used by accounting cheat_sheet.rst
|
||||
var $section = $('.checks-handling');
|
||||
if (!$section.length) { return; }
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
262
static/js/valuation-accounting.js
Normal file
262
static/js/valuation-accounting.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/* global Immutable, React */
|
||||
/* global createAtom */
|
||||
/* global VALUATION_{STANDARDS,METHODS,JOURNALS,ENTRIES,REVIEWS} */
|
||||
(function () {
|
||||
'use strict';
|
||||
// NOTE: used by valuation cheat_sheet.rst
|
||||
|
||||
const selectedMode = createAtom(['continental', 'periodic']);
|
||||
const selectedOps = 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'));
|
||||
}
|
||||
|
||||
selectedOps.addWatch('chart', (k, m, prev, next) => watch(next));
|
||||
selectedMode.addWatch('chart', (k, m, prev, next) => watch(selectedOps.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);
|
||||
|
||||
selectedOps.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') === selectedMode.deref()[0],
|
||||
onChange: function (e) {
|
||||
const newValue = item.get('name');
|
||||
selectedMode.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') === selectedMode.deref()[1],
|
||||
onChange: e => selectedMode.swap(vals => [vals[0], item.get('name')]),
|
||||
}),
|
||||
' ',
|
||||
item.get('text')
|
||||
);
|
||||
}),
|
||||
React.DOM.br(),
|
||||
React.DOM.b(null, "Activate operations to see the impact:"),
|
||||
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) {
|
||||
selectedOps.swap(d => d.set('active', key)
|
||||
.update('operations', ops => ops.add(key)));
|
||||
} else {
|
||||
selectedOps.swap(d => d.set('active', null)
|
||||
.update('operations', ops => ops.remove(key)));
|
||||
}
|
||||
}
|
||||
}),
|
||||
' ',
|
||||
item.get('title')
|
||||
);
|
||||
}),
|
||||
React.DOM.br(),
|
||||
"Closing",
|
||||
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([...selectedMode.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) {
|
||||
selectedOps.swap(d => d.set('active', key)
|
||||
.update('operations', ops => ops.add(key)));
|
||||
} else {
|
||||
selectedOps.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'), ...selectedMode.deref(), 'operations'], Immutable.List()))
|
||||
.map(op => [VALUATION_JOURNALS.getIn([selectedMode.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([...selectedMode.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([selectedMode.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(selectedMode.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<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; })
|
||||
)
|
||||
);
|
||||
}));
|
||||
function format(val, def) {
|
||||
if (!val) { return def === undefined ? '' : def; }
|
||||
if (val % 1 === 0) { return val; }
|
||||
return val.toFixed(2);
|
||||
}
|
||||
})();
|
||||
1343
static/js/valuation-data.js
Normal file
1343
static/js/valuation-data.js
Normal file
File diff suppressed because it is too large
Load Diff
176
static/js/valuation-journal.js
Normal file
176
static/js/valuation-journal.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/* global Immutable, React */
|
||||
/* global createAtom, findAncestor */
|
||||
/* global VALUATION_{STANDARDS,METHODS,JOURNALS,ENTRIES,REVIEWS} */
|
||||
(function () {
|
||||
'use strict';
|
||||
// NOTE: used by valuation cheat_sheet.rst
|
||||
|
||||
const selectedMode = createAtom()
|
||||
const selectedOp = createAtom();
|
||||
|
||||
const entries = VALUATION_ENTRIES.concat(VALUATION_REVIEWS);
|
||||
|
||||
function watch (next) {
|
||||
React.render(
|
||||
React.createElement(Controls, { entryKey: next }),
|
||||
document.getElementById('journaling-entries-controls'));
|
||||
React.render(
|
||||
React.createElement(FormatEntry, { entryKey: next }),
|
||||
document.querySelector('.journal-entries'));
|
||||
}
|
||||
|
||||
selectedOp.addWatch('chart', (k, m, prev, next) => watch([next, ...selectedMode.deref()]));
|
||||
selectedMode.addWatch('chart', (k, m, prev, next) => watch([selectedOp.deref(), ...next]));
|
||||
|
||||
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);
|
||||
|
||||
selectedMode.reset(['continental', 'periodic']);
|
||||
selectedOp.reset('initial_inventory');
|
||||
});
|
||||
|
||||
const Controls = React.createClass({
|
||||
render: function () {
|
||||
const key = this.props.entryKey;
|
||||
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') === key[1],
|
||||
onChange: function (e) {
|
||||
const newValue = item.get('name');
|
||||
selectedMode.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') === key[2],
|
||||
onChange: e => selectedMode.swap(vals => [vals[0], item.get('name')]),
|
||||
}),
|
||||
' ',
|
||||
item.get('text')
|
||||
);
|
||||
}),
|
||||
React.DOM.br(),
|
||||
React.DOM.b(null, "Activate operations to see the impact:"),
|
||||
VALUATION_ENTRIES.map(function (item, index) {
|
||||
return React.DOM.label(
|
||||
{ key: index },
|
||||
React.DOM.input({
|
||||
type: 'radio',
|
||||
checked: index === key[0],
|
||||
onChange: e => selectedOp.reset(index),
|
||||
}),
|
||||
' ',
|
||||
item.get('title')
|
||||
);
|
||||
}),
|
||||
React.DOM.br(),
|
||||
"Closing",
|
||||
VALUATION_REVIEWS.map(function (item, index) {
|
||||
return React.DOM.label(
|
||||
{ key: index },
|
||||
React.DOM.input({
|
||||
type: 'radio',
|
||||
checked: index === key[0],
|
||||
onChange: e => selectedOp.reset(index),
|
||||
}),
|
||||
' ',
|
||||
item.get('title')
|
||||
);
|
||||
}),
|
||||
React.DOM.br(),
|
||||
);
|
||||
}
|
||||
});
|
||||
const FormatEntry = React.createClass({
|
||||
render: function () {
|
||||
const entry = entries.getIn(this.props.entryKey);
|
||||
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,
|
||||
// Use `journal_operations' if it's a review. See `valuation-data.js'.
|
||||
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) {
|
||||
const standard = this.props.entryKey[1];
|
||||
if (!entry) {
|
||||
return React.DOM.tr(
|
||||
{ key: 'spacer-' + index },
|
||||
React.DOM.td({ colSpan: 3 }, "\u00A0")
|
||||
);
|
||||
}
|
||||
const journalEntry = VALUATION_JOURNALS.getIn([standard, ...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);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user