19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:47 +01:00
parent accf5918df
commit 6e65e8c877
688 changed files with 225434 additions and 199401 deletions

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class ProductDocument extends models.ServerModel {
_name = "product.document";
}

View file

@ -0,0 +1,13 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { fields } from "@web/../tests/web_test_helpers";
export class ResFake extends mailModels.ResFake {
duration = fields.Float({ string: "duration" });
_views = {
form: /* xml */ `
<form>
<field name="duration" widget="mrp_timer" readonly="1"/>
</form>`,
};
}

View file

@ -0,0 +1,145 @@
import {
click,
contains,
openView,
registerArchs,
start,
startServer
} from "@mail/../tests/mail_test_helpers";
import { defineMrpModels } from "@mrp/../tests/mrp_test_helpers";
import { describe, test } from "@odoo/hoot";
import { inputFiles } from "@web/../tests/utils";
import { asyncStep, getService, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
describe.current.tags("desktop");
defineMrpModels();
const newArchs = {
"product.document,false,kanban": `<kanban js_class="product_documents_kanban" create="false"><templates><t t-name="card">
<field name="name"/>
</t></templates></kanban>`,
};
test("MRP documents kanban basic rendering", async () => {
const pyEnv = await startServer();
const irAttachment = pyEnv["ir.attachment"].create({
mimetype: "image/png",
name: "test.png",
});
pyEnv["product.document"].create([
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
{ name: "test2" },
{ name: "test3" },
]);
registerArchs(newArchs);
await start();
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
await contains("button[name='product_upload_document']");
await contains(".o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)", { count: 3 });
// check control panel buttons
await contains(".o_control_panel_main_buttons .btn-primary", { text: "Upload" });
});
test("mrp: upload multiple files", async () => {
const pyEnv = await startServer();
const irAttachment = pyEnv["ir.attachment"].create({
mimetype: "image/png",
name: "test.png",
});
const text1 = new File(["hello, world"], "text1.txt", { type: "text/plain" });
const text2 = new File(["hello, world"], "text2.txt", { type: "text/plain" });
const text3 = new File(["hello, world"], "text3.txt", { type: "text/plain" });
pyEnv["product.document"].create([
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
{ name: "test2" },
{ name: "test3" },
]);
registerArchs(newArchs);
await start();
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
getService("file_upload").bus.addEventListener("FILE_UPLOAD_ADDED", () => asyncStep("xhrSend"));
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text1]);
await waitForSteps(["xhrSend"]);
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text2, text3]);
await waitForSteps(["xhrSend"]);
});
test("mrp: click on image opens attachment viewer", async () => {
const newArchs = {
"product.document,false,kanban": `
<kanban js_class="product_documents_kanban" create="false">
<templates>
<t t-name="card">
<div class="o_kanban_previewer" t-if="record.ir_attachment_id.raw_value">
<field name="ir_attachment_id" invisible="1"/>
<img t-attf-src="/web/image/#{record.ir_attachment_id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/>
</div>
<field name="name"/>
<field name="mimetype"/>
</t>
</templates>
</kanban>`,
};
const pyEnv = await startServer();
const irAttachment = pyEnv["ir.attachment"].create({
mimetype: "image/png",
name: "test.png",
});
pyEnv["product.document"].create([
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
{ name: "test2" },
{ name: "test3" },
]);
registerArchs(newArchs);
await start();
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
await click(".o_kanban_previewer");
await contains(".o-FileViewer");
await click(".o-FileViewer-headerButton .fa-times");
await contains(".o-FileViewer", { count: 0 });
});
test("mrp: upload progress bars", async () => {
const pyEnv = await startServer();
const irAttachment = pyEnv["ir.attachment"].create({
mimetype: "image/png",
name: "test.png",
});
const text1 = new File(["hello, world"], "text1.txt", { type: "text/plain" });
pyEnv["product.document"].create([
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
{ name: "test2" },
{ name: "test3" },
]);
registerArchs(newArchs);
await start();
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
let xhr;
patchWithCleanup(fileUploadService, {
createXhr() {
xhr = super.createXhr(...arguments);
xhr.send = () => {};
return xhr;
},
});
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text1]);
const progressEvent = new Event("progress", { bubbles: true });
progressEvent.loaded = 250000000;
progressEvent.total = 500000000;
progressEvent.lengthComputable = true;
xhr.upload.dispatchEvent(progressEvent);
await contains(".o_file_upload_progress_text_left", { text: "Uploading... (50%)" });
progressEvent.loaded = 350000000;
xhr.upload.dispatchEvent(progressEvent);
await contains(".o_file_upload_progress_text_right", { text: "(350/500MB)" });
});

View file

@ -1,245 +0,0 @@
/** @odoo-module **/
import testUtils from 'web.test_utils';
import { registry } from "@web/core/registry";
import {
click,
getFixture,
nextTick,
} from '@web/../tests/helpers/utils';
import { setupViewRegistries } from "@web/../tests/views/helpers";
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch([
'mrp.document',
]);
const serviceRegistry = registry.category("services");
let target;
let pyEnv;
QUnit.module('Views', {}, function () {
QUnit.module('MrpDocumentsKanbanView', {
beforeEach: async function () {
serviceRegistry.add("file_upload", fileUploadService);
this.ORIGINAL_CREATE_XHR = fileUploadService.createXhr;
this.patchDocumentXHR = (mockedXHRs, customSend) => {
fileUploadService.createXhr = () => {
const xhr = new window.EventTarget();
Object.assign(xhr, {
upload: new window.EventTarget(),
open() {},
send(data) { customSend && customSend(data); },
});
mockedXHRs.push(xhr);
return xhr;
};
};
pyEnv = await startServer();
const irAttachment = pyEnv['ir.attachment'].create({
mimetype: 'image/png',
name: 'test.png',
})
pyEnv['mrp.document'].create([
{name: 'test1', priority: 2, ir_attachment_id: irAttachment},
{name: 'test2', priority: 1},
{name: 'test3', priority: 3},
]);
target = getFixture();
setupViewRegistries();
},
afterEach() {
fileUploadService.createXhr = this.ORIGINAL_CREATE_XHR;
},
}, function () {
QUnit.test('MRP documents kanban basic rendering', async function (assert) {
assert.expect(4);
const views = {
'mrp.document,false,kanban':
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
<div>
<field name="name"/>
</div>
</t></templates></kanban>`
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mrp.document',
views: [[false, 'kanban']],
});
assert.ok(target.querySelector('.o_mrp_documents_kanban_upload'),
"should have upload button in kanban buttons");
assert.containsN(target, '.o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)', 3,
"should have 3 records in the renderer");
// check control panel buttons
assert.containsN(target, '.o_cp_buttons .btn-primary', 1,
"should have only 1 primary button i.e. Upload button");
assert.equal(target.querySelector(".o_cp_buttons .btn-primary").innerText.trim().toUpperCase(), 'UPLOAD',
"should have a primary 'Upload' button");
});
QUnit.test('mrp: upload multiple files', async function (assert) {
assert.expect(4);
const file1 = await testUtils.file.createFile({
name: 'text1.txt',
content: 'hello, world',
contentType: 'text/plain',
});
const file2 = await testUtils.file.createFile({
name: 'text2.txt',
content: 'hello, world',
contentType: 'text/plain',
});
const file3 = await testUtils.file.createFile({
name: 'text3.txt',
content: 'hello, world',
contentType: 'text/plain',
});
const mockedXHRs = [];
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
const views = {
'mrp.document,false,kanban':
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
<div>
<field name="name"/>
</div>
</t></templates></kanban>`
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mrp.document',
views: [[false, 'kanban']],
});
const fileInput = target.querySelector(".o_input_file");
let dataTransfer = new DataTransfer();
dataTransfer.items.add(file1);
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
assert.verifySteps(['xhrSend']);
dataTransfer = new DataTransfer();
dataTransfer.items.add(file2);
dataTransfer.items.add(file3);
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
assert.verifySteps(['xhrSend']);
});
QUnit.test('mrp: upload progress bars', async function (assert) {
assert.expect(4);
const file1 = await testUtils.file.createFile({
name: 'text1.txt',
content: 'hello, world',
contentType: 'text/plain',
});
const mockedXHRs = [];
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
const views = {
'mrp.document,false,kanban':
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
<div>
<field name="name"/>
</div>
</t></templates></kanban>`
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mrp.document',
views: [[false, 'kanban']],
});
const fileInput = target.querySelector(".o_input_file");
let dataTransfer = new DataTransfer();
dataTransfer.items.add(file1);
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
assert.verifySteps(['xhrSend']);
const progressEvent = new Event('progress', { bubbles: true });
progressEvent.loaded = 250000000;
progressEvent.total = 500000000;
progressEvent.lengthComputable = true;
mockedXHRs[0].upload.dispatchEvent(progressEvent);
await nextTick();
assert.strictEqual(
target.querySelector('.o_file_upload_progress_text_left').innerText,
"Uploading... (50%)",
"the current upload progress should be at 50%"
);
progressEvent.loaded = 350000000;
mockedXHRs[0].upload.dispatchEvent(progressEvent);
await nextTick();
assert.strictEqual(
target.querySelector('.o_file_upload_progress_text_right').innerText,
"(350/500MB)",
"the current upload progress should be at (350/500Mb)"
);
});
QUnit.test("mrp: click on image opens attachment viewer", async function (assert) {
assert.expect(4);
const views = {
'mrp.document,false,kanban':
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
<div class="o_kanban_image" t-if="record.ir_attachment_id.raw_value">
<div class="o_kanban_previewer">
<field name="ir_attachment_id" invisible="1"/>
<img t-attf-src="/web/image/#{record.ir_attachment_id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/>
</div>
</div>
<div>
<field name="name"/>
</div>
</t></templates></kanban>`
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'mrp.document',
views: [[false, 'kanban']],
});
assert.containsOnce(target, ".o_kanban_previewer");
await click(target.querySelector(".o_kanban_previewer"));
await nextTick();
assert.containsOnce(target, '.o_AttachmentViewer',
"should have a document preview");
assert.containsOnce(target, '.o_AttachmentViewer_headerItemButtonClose',
"should have a close button");
await click(target, '.o_AttachmentViewer_headerItemButtonClose');
assert.containsNone(target, '.o_AttachmentViewer',
"should not have a document preview");
});
});
});

View file

@ -0,0 +1,14 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels } from "@web/../tests/web_test_helpers";
import { ProductDocument } from "@mrp/../tests/mock_server/mock_models/product_document";
import { ResFake } from "@mrp/../tests/mock_server/mock_models/res_fake";
export function defineMrpModels() {
return defineModels(mrpModels);
}
export const mrpModels = {
...mailModels,
ProductDocument,
ResFake,
};

View file

@ -1,42 +0,0 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Mrp", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
foo: {
fields: {
duration: { string: "Duration", type: "float" },
},
records: [{ id: 1, duration: 150.5 }],
},
},
};
setupViewRegistries();
});
QUnit.module("MrpTimer");
QUnit.test("ensure the rendering is based on minutes and seconds", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "foo",
resId: 1,
arch: '<form><field name="duration" widget="mrp_timer" readonly="1"/></form>',
});
assert.strictEqual(
target.querySelector(".o_field_mrp_timer").textContent,
"150:30",
"should not contain hours and be correctly set base on minutes seconds"
);
});
});

View file

@ -0,0 +1,14 @@
import { openFormView, start, startServer } from "@mail/../tests/mail_test_helpers";
import { defineMrpModels } from "@mrp/../tests/mrp_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
describe.current.tags("desktop");
defineMrpModels();
test("ensure the rendering is based on minutes and seconds", async () => {
const pyEnv = await startServer();
const fakeId = pyEnv["res.fake"].create({ duration: 150.5 });
await start();
await openFormView("res.fake", fakeId);
expect(".o_field_mrp_timer").toHaveText("150:30");
});

View file

@ -1,70 +1,34 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
tour.register('test_mrp_manual_consumption', {test: true}, [
{
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("5.00")',
run: () => {},
},
{
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last',
run: 'click',
},
{
trigger: 'div[name="quantity_done"] input',
run: 'text 6.0'
},
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
},
{
trigger: "input[id='qty_producing']",
run: 'text 8.0',
},
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
},
{
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("6.00")',
run: () => {},
},
{
trigger: 'button[name=button_mark_done]',
run: 'click',
},
{
trigger: 'button[name=action_confirm]',
extra_trigger: '.o_technical_modal',
run: 'click',
},
{
trigger: 'button[name=action_backorder]',
run: 'click',
},
{
trigger: "input[id='qty_producing']",
run: 'text 2.0',
},
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
},
{
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("2.00")',
run: () => {},
},
{
trigger: 'button[name=button_mark_done]',
run: 'click',
},
{
trigger: 'button[name=action_confirm]',
extra_trigger: '.o_technical_modal',
run: 'click',
},
...tour.stepUtils.saveForm(),
]);
import { registry } from "@web/core/registry";
import { stepUtils } from '@web_tour/tour_utils';
registry.category("web_tour.tours").add('test_mrp_manual_consumption_02', {
steps: () => [
{
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last:contains("0.00")',
},
{
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last',
run: 'click',
},
{
trigger: 'div[name="quantity"] input',
run: "edit 16.0 && click body",
},
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
run: "click",
},
{
trigger: "input[id='qty_producing_0']",
run: "edit 8.0 && click body",
},
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
run: "click",
},
{
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last:contains("16.00")',
},
...stepUtils.saveForm(),
]});

View file

@ -0,0 +1,25 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_manufacture_from_bom", {
steps: () => [
{
trigger: '[name="product_tmpl_id"]',
run: "click",
},
{
trigger: '.o_stat_text:contains("BoM Overview")',
run: "click",
},
{
trigger: '.fa-toggle-off',
run: "click",
},
{
trigger: 'button.btn-primary:contains("Manufacture")',
run: "click",
},
{
trigger: 'button[aria-checked="true"]:contains("Draft")',
},
],
});

View file

@ -0,0 +1,47 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('test_mrp_bom_product_catalog', {
steps: () => [
{
trigger: 'button[name=action_add_from_catalog]',
run: "click",
},
{
trigger: '.o_kanban_record:nth-child(1)',
run: "click",
},
{
trigger: '.o_product_added',
run: "click",
},
{
trigger: 'button:contains("Back to BoM")',
run: "click",
},
{
trigger: 'div.o_field_one2many:contains("Component")',
},
]});
registry.category("web_tour.tours").add('test_mrp_production_product_catalog', {
steps: () => [
{
trigger: 'button[name=action_add_from_catalog_raw]',
run: "click",
},
{
trigger: '.o_kanban_record:nth-child(1)',
run: "click",
},
{
trigger: '.o_product_added',
run: "click",
},
{
trigger: 'button:contains("Back to Production")',
run: "click",
},
{
trigger: 'div.o_field_widget:contains("WH/MO/")',
},
]});

View file

@ -0,0 +1,27 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("mrp_bom_report_tour", {
steps: () => [
{
content: "Check the current displayed variant",
trigger: ".o_mrp_bom_report_page h2 a:contains('[alpaca] Product Test Sync (L)')",
run: () => {},
},
{
content: "Open dropdown menu",
trigger: ".o-autocomplete--input",
run: "click",
},
{
content: "Select the other variant",
trigger: ".o-autocomplete--dropdown-menu.show li.o-autocomplete--dropdown-item:eq(1)",
run: "click",
},
{
content: "Ensure the second variant is displayed",
trigger: ".o_mrp_bom_report_page h2 a:contains('[zebra] Product Test Sync (S)')",
run: () => {},
},
],
});

View file

@ -0,0 +1,163 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('test_manufacturing_and_byproduct_sm_to_sml_synchronization', {
steps: () => [
{
trigger: ".btn-primary[name=action_confirm]",
run: "click",
},
{
trigger: ".o_data_row:has([name=quantity]:contains(5.00)) > td:contains(product2)",
run: "click",
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: "h4:contains('Components')",
run: "click",
},
{
trigger: ".modal .o_list_number:contains(5)",
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
trigger: ".o_data_row:has([name=quantity]:contains(5.00)) > td:contains(product2)",
run: "click",
},
{
trigger: ".o_field_widget[name=quantity] input",
run: 'edit 21',
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: "h4:contains('Components')",
run: "click",
},
{
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
run: "click",
},
{
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
run: 'edit 25',
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
trigger: ".o_data_row:has([name=product_uom_qty]:contains(5.00)) > td:contains(25)",
run: "click",
},
{
trigger: ".o_field_widget[name=quantity] input",
run: 'edit 7',
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: ".modal .o_data_row > td:contains('7')",
run: "click",
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
trigger: ".nav-link[name=finished_products]",
run: "click",
},
{
trigger: ".o_data_row:has([name=quantity]:contains(2.00)) > td:contains(product2)",
run: "click",
},
{
trigger: ".fa-list",
run: "click",
},
{
trigger: "h4:contains('Move Byproduct')",
run: "click",
},
{
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
run: "click",
},
{
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
run: 'edit 2',
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
trigger: ".o_data_row:has([name=quantity]:contains(2.00)) > td[name=product_id]:contains(product2)",
run: "dblclick",
},
{
trigger: ".o_field_widget[name=quantity] input",
run: 'edit 5',
},
{
trigger: ".fa-list",
run: "click",
},
{
trigger: "h4:contains('Move Byproduct')",
run: "click",
},
{
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
run: "click",
},
{
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
run: 'edit 7',
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
trigger: ".o_data_row:has([name=product_uom_qty]:contains(2.00)) > td:contains(10)",
run: "click",
},
{
trigger: ".o_field_widget[name=quantity] input",
run: 'edit 7',
},
{
trigger: ".fa-list",
run: "click",
},
{
trigger: ".o_list_footer .o_list_number > span:contains('7')",
run: "click",
},
{
content: "Click Save",
trigger: ".modal .modal-footer .o_form_button_save",
run: "click",
},
{
content: "wait for save completion",
trigger: ".o_form_readonly, .o_form_saved",
},
]
});