Files
odoo-docs/static/js/valuation-accounting.js
Lulu Grimalkin (lugr) 912dde6d26 [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>
2025-10-23 17:06:20 +00:00

263 lines
12 KiB
JavaScript

/* 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);
}
})();