Odoo18-Base/addons/web/static/tests/views/fields/monetary_field_tests.js
2025-03-10 11:12:23 +07:00

1033 lines
35 KiB
JavaScript

/** @odoo-module **/
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
addRow,
click,
clickSave,
editInput,
getFixture,
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { session } from "@web/session";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
partner: {
fields: {
int_field: {
string: "int_field",
type: "integer",
sortable: true,
searchable: true,
},
float_field: {
string: "float_field",
type: "float",
digits: [16, 1],
searchable: true,
},
p: {
string: "one2many field",
type: "one2many",
relation: "partner",
searchable: true,
},
currency_id: {
string: "Currency",
type: "many2one",
relation: "currency",
searchable: true,
},
monetary_field: {
string: "Monetary Field",
type: "monetary",
},
},
records: [
{ id: 1, int_field: 10, float_field: 0.44444 },
{ id: 2, int_field: 0, float_field: 0, currency_id: 2 },
{ id: 3, int_field: 80, float_field: -3.89859 },
{ id: 4, int_field: false, float_field: false },
{ id: 5, int_field: -4, float_field: 9.1, currency_id: 1 },
{ id: 6, float_field: 3.9, monetary_field: 4.2, currency_id: 1 },
],
},
currency: {
fields: {
digits: { string: "Digits" },
symbol: { string: "Currency Sumbol", type: "char", searchable: true },
position: { string: "Currency Position", type: "char", searchable: true },
},
records: [
{ id: 1, display_name: "$", symbol: "$", position: "before" },
{ id: 2, display_name: "€", symbol: "€", position: "after" },
{
id: 3,
display_name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
],
},
},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("MonetaryField");
QUnit.test("basic flow in form view - float field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
assert.containsOnce(
target,
".o_field_monetary > div.text-nowrap",
"should have o_horizontal class"
);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"9.10",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.childNodes[0].textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_monetary input", "108.2458938598598");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The new value should be rounded properly after the blur"
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The new value should be rounded properly."
);
});
QUnit.test("basic flow in form view - monetary field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"9.10",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.childNodes[0].textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_monetary input", "108.2458938598598");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The new value should be rounded properly after the blur"
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The new value should be rounded properly."
);
});
QUnit.test("rounding using formula in form view - float field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
// Test computation and rounding
await editInput(target, ".o_field_monetary input", "=100/3");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"33.33",
"The new value should be calculated and rounded properly."
);
});
QUnit.test("rounding using formula in form view - monetary field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
// Test computation and rounding
await editInput(target, ".o_field_monetary input", "=100/3");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"33.33",
"The new value should be calculated and rounded properly."
);
});
QUnit.test("with currency digits != 2 - float field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
},
});
serverData.models.partner.records = [
{
id: 1,
float_field: 99.1234,
monetary_field: 99.1234,
currency_id: 3,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1234",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.children[0].textContent,
"Bs.F",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_widget input", "99.111111111");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1111",
"The value should should be formatted on blur."
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1111",
"The new value should be rounded properly."
);
});
QUnit.test("with currency digits != 2 - monetary field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
},
});
serverData.models.partner.records = [
{
id: 1,
float_field: 99.1234,
monetary_field: 99.1234,
currency_id: 3,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1234",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.children[0].textContent,
"Bs.F",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_widget input", "99.111111111");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1111",
"The value should should be formatted on blur."
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"99.1111",
"The new value should be rounded properly."
);
});
QUnit.test("basic flow in editable list view - float field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
{
id: 2,
float_field: 15.3,
monetary_field: 15.3,
currency_id: 2,
},
{
id: 3,
float_field: false,
monetary_field: false,
currency_id: 1,
},
{
id: 4,
float_field: 5.0,
monetary_field: 5.0,
},
];
await makeView({
type: "list",
serverData,
resModel: "partner",
arch: `
<tree editable="bottom">
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</tree>`,
});
const dollarValues = Array.from(target.querySelectorAll("td")).filter((x) =>
x.textContent.includes("$")
);
assert.strictEqual(dollarValues.length, 2, "Only 2 line has dollar as a currency.");
const euroValues = Array.from(target.querySelectorAll("td")).filter((x) =>
x.textContent.includes("€")
);
assert.strictEqual(euroValues.length, 1, "Only 1 line has euro as a currency.");
const noCurrencyValues = Array.from(target.querySelectorAll("td.o_data_cell")).filter(
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
);
assert.strictEqual(noCurrencyValues.length, 1, "Only 1 line has no currency.");
// switch to edit mode
const dollarCell = target.querySelectorAll("td.o_field_cell")[0];
await click(dollarCell);
assert.strictEqual(
dollarCell.children.length,
1,
"The cell td should only contain the special div of monetary widget."
);
assert.containsOnce(
target,
".o_field_widget input",
"The view should have 1 input for editable monetary float."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"9.10",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.childNodes[0].textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_widget input", "108.2458938598598");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The typed value should be correctly displayed and formatted on blur"
);
await clickSave(target);
assert.strictEqual(
dollarCell.textContent,
"$\u00a0108.25",
"The new value should be correct"
);
});
QUnit.test("basic flow in editable list view - monetary field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 9.1,
monetary_field: 9.1,
currency_id: 1,
},
{
id: 2,
float_field: 15.3,
monetary_field: 15.3,
currency_id: 2,
},
{
id: 3,
float_field: false,
monetary_field: false,
currency_id: 1,
},
{
id: 4,
float_field: 5.0,
monetary_field: 5.0,
},
];
await makeView({
type: "list",
serverData,
resModel: "partner",
arch: `
<tree editable="bottom">
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
</tree>`,
});
const dollarValues = Array.from(target.querySelectorAll("td")).filter((x) =>
x.textContent.includes("$")
);
assert.strictEqual(dollarValues.length, 2, "Only 2 line has dollar as a currency.");
const euroValues = Array.from(target.querySelectorAll("td")).filter((x) =>
x.textContent.includes("€")
);
assert.strictEqual(euroValues.length, 1, "Only 1 line has euro as a currency.");
const noCurrencyValues = Array.from(target.querySelectorAll("td.o_data_cell")).filter(
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
);
assert.strictEqual(noCurrencyValues.length, 1, "Only 1 line has no currency.");
// switch to edit mode
const dollarCell = target.querySelectorAll("td.o_field_cell")[0];
await click(dollarCell);
assert.strictEqual(
dollarCell.children.length,
1,
"The cell td should only contain the special div of monetary widget."
);
assert.containsOnce(
target,
".o_field_widget input",
"The view should have 1 input for editable monetary float."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"9.10",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget input").parentNode.childNodes[0].textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_widget input", "108.2458938598598");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"108.25",
"The typed value should be correctly displayed and formatted on blur"
);
await clickSave(target);
assert.strictEqual(
dollarCell.textContent,
"$\u00a0108.25",
"The new value should be correct"
);
});
QUnit.test("changing currency updates the field - float field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 4.2,
monetary_field: 4.2,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="float_field" widget="monetary"/>
<field name="currency_id"/>
</form>`,
});
// replace bottom with new helpers when they exist
await click(target, ".o_field_many2one_selection input");
const euroM2OListItem = Array.from(
target.querySelectorAll(".o_field_many2one_selection li")
).filter((li) => li.textContent === "€")[0];
await click(euroM2OListItem);
assert.strictEqual(
target.querySelector(".o_field_monetary div :first-child").textContent +
target.querySelector(".o_field_monetary div :last-child").value,
"€4.20",
"The value should be formatted with new currency on blur."
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_monetary input").value,
"4.20",
"The new value should still be correct."
);
});
QUnit.test("changing currency updates the field - monetary field", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: 4.2,
monetary_field: 4.2,
currency_id: 1,
},
];
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="monetary_field"/>
<field name="currency_id"/>
</form>`,
});
// replace bottom with new helpers when they exist
await click(target, ".o_field_many2one_selection input");
const euroM2OListItem = Array.from(
target.querySelectorAll(".o_field_many2one_selection li")
).filter((li) => li.textContent === "€")[0];
await click(euroM2OListItem);
assert.strictEqual(
target.querySelector(".o_field_monetary div :first-child").textContent +
target.querySelector(".o_field_monetary div :last-child").value,
"€4.20",
"The value should be formatted with new currency on blur."
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_monetary input").value,
"4.20",
"The new value should still be correct."
);
});
QUnit.test("MonetaryField with monetary field given in options", async function (assert) {
serverData.models.partner.fields.float_field.type = "monetary";
serverData.models.partner.fields.company_currency_id = {
string: "Company Currency",
type: "many2one",
relation: "currency",
};
serverData.models.partner.records[4].company_currency_id = 2;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form edit="0">
<sheet>
<field name="float_field" options="{'currency_field': 'company_currency_id'}"/>
<field name="company_currency_id"/>
</sheet>
</form>`,
resId: 5,
});
assert.strictEqual(
target.querySelector(".o_field_monetary").textContent,
"9.10\u00a0€",
"field monetary should be formatted with correct currency"
);
});
QUnit.test("should keep the focus when being edited in x2many lists", async function (assert) {
serverData.models.partner.fields.currency_id.default = 1;
serverData.models.partner.fields.m2m = {
string: "m2m",
type: "many2many",
relation: "partner",
default: [[6, false, [2]]],
};
serverData.views = {
"partner,false,list": `
<tree editable="bottom">
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</tree>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<field name="p"/>
<field name="m2m"/>
</sheet>
</form>`,
});
// test the monetary field inside the one2many
await addRow(target.querySelector(".o_field_widget[name=p]"));
await editInput(target, ".o_field_widget[name=float_field] input", "22");
assert.strictEqual(
document.activeElement,
target.querySelector(".o_field_widget[name=float_field] input"),
"the focus should still be on the input"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
"22.00",
"the value should have been formatted on field change"
);
await click(target);
assert.strictEqual(
target.querySelector(".o_field_widget[name=p] .o_field_widget[name=float_field] span")
.innerHTML,
"$&nbsp;22.00"
);
// test the monetary field inside the many2many
await click(target.querySelector(".o_field_widget[name=m2m] .o_data_cell"));
await editInput(target, ".o_field_widget[name=float_field] input", "22");
assert.strictEqual(
document.activeElement,
target.querySelector(".o_field_widget[name=float_field] input"),
"the focus should still be on the input"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
"22.00",
"the value should have been formatted on field change"
);
await click(target);
assert.strictEqual(
target.querySelector(".o_field_widget[name=m2m] .o_field_widget[name=float_field] span")
.innerHTML,
"22.00&nbsp;€"
);
});
QUnit.test("MonetaryField with currency set by an onchange", async function (assert) {
// this test ensures that the monetary field can be re-rendered with and
// without currency (which can happen as the currency can be set by an
// onchange)
serverData.models.partner.onchanges = {
int_field: function (obj) {
obj.currency_id = obj.int_field ? 2 : null;
},
};
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree editable="top">
<field name="int_field"/>
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</tree>`,
});
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(
target,
".o_selected_row .o_field_widget[name=float_field] input",
"monetary field should have been rendered correctly (without currency)"
);
assert.containsNone(
target,
".o_selected_row .o_field_widget[name=float_field] span",
"monetary field should have been rendered correctly (without currency)"
);
// set a value for int_field -> should set the currency and re-render float_field
await editInput(target, ".o_field_widget[name=int_field] input", "7");
assert.containsOnce(
target,
".o_selected_row .o_field_widget[name=float_field] input",
"monetary field should have been re-rendered correctly (with currency)"
);
assert.strictEqual(
target.querySelector(".o_selected_row .o_field_widget[name=float_field] span")
.innerText,
"€",
"monetary field should have been re-rendered correctly (with currency)"
);
await click(target.querySelector(".o_field_widget[name=float_field] input"));
assert.strictEqual(
document.activeElement,
target.querySelector(".o_field_widget[name=float_field] input"),
"focus should be on the float_field field's input"
);
// unset the value of int_field -> should unset the currency and re-render float_field
await click(target.querySelector(".o_field_widget[name=int_field]"));
await editInput(target, ".o_field_widget[name=int_field] input", "0");
assert.containsOnce(
target,
".o_selected_row .o_field_widget[name=float_field] input",
"monetary field should have been re-rendered correctly (without currency)"
);
assert.containsNone(
target,
".o_selected_row .o_field_widget[name=float_field] span",
"monetary field should have been re-rendered correctly (without currency)"
);
await click(target.querySelector(".o_field_widget[name=float_field] input"));
assert.strictEqual(
document.activeElement,
target.querySelector(".o_field_widget[name=float_field] input"),
"focus should be on the float_field field's input"
);
});
QUnit.test("float widget on monetary field", async function (assert) {
serverData.models.partner.fields.monetary = { string: "Monetary", type: "monetary" };
serverData.models.partner.records[0].monetary = 9.99;
serverData.models.partner.records[0].currency_id = 1;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form edit="0">
<sheet>
<field name="monetary" widget="float"/>
<field name="currency_id" invisible="1"/>
</sheet>
</form>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name=monetary]").textContent,
"9.99",
"value should be correctly formatted (with the float formatter)"
);
});
QUnit.test("float field with monetary widget and decimal precision", async function (assert) {
serverData.models.partner.records = [
{
id: 1,
float_field: -8.89859,
currency_id: 1,
},
];
patchWithCleanup(session, {
currencies: {
...session.currencies,
1: {
name: "USD",
symbol: "$",
position: "before",
digits: [0, 4],
},
},
});
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<field name="float_field" widget="monetary" options="{'field_digits': True}"/>
<field name="currency_id" invisible="1"/>
</sheet>
</form>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
"-8.9",
"The input should be rendered without the currency symbol."
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").parentElement.firstChild
.textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
await editInput(target, ".o_field_monetary input", "109.2458938598598");
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
"109.2",
"The value should should be formatted on blur."
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"109.2",
"The new value should be rounded properly."
);
});
QUnit.test("MonetaryField without currency symbol", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 5,
serverData,
arch: `
<form>
<sheet>
<field name="float_field" widget="monetary" options="{'no_symbol': True}" />
<field name="currency_id" invisible="1" />
</sheet>
</form>`,
});
// Non-breaking space between the currency and the amount
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
"9.10",
"The currency symbol is not displayed"
);
});
QUnit.test("monetary field with placeholder", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="float_field" widget="monetary" placeholder="Placeholder"/>
<field name="currency_id" invisible="1"/>
</form>`,
});
const input = target.querySelector(".o_field_widget[name='float_field'] input");
input.value = "";
await triggerEvent(input, null, "input");
assert.strictEqual(
target.querySelector(".o_field_widget[name='float_field'] input").placeholder,
"Placeholder"
);
});
QUnit.test("required monetary field with zero value", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="monetary_field" required="1"/>
</form>`,
});
assert.containsOnce(target, ".o_form_editable");
assert.strictEqual(target.querySelector("[name=monetary_field] input").value, "0.00");
});
QUnit.test("uses 'currency_id' as currency field by default", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
</form>`,
resId: 6,
});
assert.containsOnce(target, ".o_form_editable");
assert.strictEqual(
target.querySelector(".o_field_widget[name=monetary_field] input").parentElement
.firstChild.textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
});
QUnit.test("automatically uses currency_field if defined", async function (assert) {
serverData.models.partner.fields.custom_currency_id = {
string: "Currency",
type: "many2one",
relation: "currency",
searchable: true,
};
serverData.models.partner.fields.monetary_field.currency_field = "custom_currency_id";
serverData.models.partner.records[5].custom_currency_id = 1;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="monetary_field"/>
<field name="custom_currency_id" invisible="1"/>
</form>`,
resId: 6,
});
assert.containsOnce(target, ".o_form_editable");
assert.strictEqual(
target.querySelector(".o_field_widget[name=monetary_field] input").parentElement
.firstChild.textContent,
"$",
"The input should be preceded by a span containing the currency symbol."
);
});
});