1533 lines
59 KiB
JavaScript
1533 lines
59 KiB
JavaScript
|
/** @odoo-module */
|
||
|
|
||
|
import { browser } from "@web/core/browser/browser";
|
||
|
import {
|
||
|
click,
|
||
|
editInput,
|
||
|
editSelect,
|
||
|
editSelectMenu,
|
||
|
getFixture,
|
||
|
nextTick,
|
||
|
patchWithCleanup,
|
||
|
} from "@web/../tests/helpers/utils";
|
||
|
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||
|
import { registry } from "@web/core/registry";
|
||
|
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
|
||
|
import { ImportDataProgress } from "../src/import_data_progress/import_data_progress";
|
||
|
import { ImportAction } from "../src/import_action/import_action";
|
||
|
import { ImportBlockUI } from "../src/import_block_ui";
|
||
|
import { useEffect } from "@odoo/owl";
|
||
|
|
||
|
const serviceRegistry = registry.category("services");
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
//#region Helpers
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
let serverData;
|
||
|
let target;
|
||
|
let totalRows;
|
||
|
|
||
|
function registerFakeHTTPService(validate = (route, params) => {}) {
|
||
|
const fakeHTTPService = {
|
||
|
start() {
|
||
|
return {
|
||
|
post: (route, params) => {
|
||
|
validate(route, params);
|
||
|
const file = {
|
||
|
id: 10,
|
||
|
name: params.ufile[0].name,
|
||
|
mimetype: "text/plain",
|
||
|
};
|
||
|
return JSON.stringify([file]);
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
};
|
||
|
serviceRegistry.add("http", fakeHTTPService);
|
||
|
}
|
||
|
|
||
|
function getFieldsTree(serverData) {
|
||
|
const fields = Object.entries(serverData.models.partner.fields);
|
||
|
fields.forEach(([k, v]) => {
|
||
|
v.id = k;
|
||
|
v.fields = [];
|
||
|
});
|
||
|
const mappedFields = fields.map((e) => e[1]);
|
||
|
return mappedFields.filter((e) => ["id", "__last_update", "name"].includes(e.id) === false);
|
||
|
}
|
||
|
|
||
|
function getMatches(serverData, headers) {
|
||
|
// basic implementation for testing purposes which matches if the first line is the
|
||
|
// name of a field, or corresponds to the string value of a field from serverData
|
||
|
const matches = [];
|
||
|
for (const header of headers) {
|
||
|
if (serverData.models.partner.fields[header]) {
|
||
|
matches.push([header]);
|
||
|
}
|
||
|
const serverDataIndex = Object.values(serverData.models.partner.fields).findIndex(
|
||
|
(e) => e.string === header
|
||
|
);
|
||
|
if (serverDataIndex !== -1) {
|
||
|
matches.push([Object.keys(serverData.models.partner.fields)[serverDataIndex]]);
|
||
|
}
|
||
|
}
|
||
|
return Object.assign({}, matches);
|
||
|
}
|
||
|
|
||
|
async function executeImport(data, shouldWait = false) {
|
||
|
const res = {
|
||
|
ids: [],
|
||
|
};
|
||
|
const matching = data[1].filter((f) => f !== false);
|
||
|
if (matching.length) {
|
||
|
res.ids.push(1);
|
||
|
} else {
|
||
|
res.messages = [
|
||
|
{
|
||
|
type: "error",
|
||
|
not_matching_error: true,
|
||
|
message: "You must configure at least one field to import",
|
||
|
},
|
||
|
];
|
||
|
}
|
||
|
if (data[3].skip + 1 < (data[3].has_headers ? totalRows - 1 : totalRows)) {
|
||
|
res.nextrow = data[3].skip + data[3].limit;
|
||
|
}
|
||
|
if (shouldWait) {
|
||
|
// make sure the progress bar is shown
|
||
|
await nextTick();
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
function parsePreview(opts) {
|
||
|
const fakePreviewData = [
|
||
|
["Foo", "Deco addict", "Azure Interior", "Brandon Freeman"],
|
||
|
["Bar", "1", "1", "0"],
|
||
|
["Display name", "Azure Interior"],
|
||
|
];
|
||
|
const headers = opts.has_headers && fakePreviewData.map((col) => col[0]);
|
||
|
totalRows = [...fakePreviewData].sort((a, b) => (a.length > b.length ? -1 : 1))[0].length;
|
||
|
|
||
|
return Promise.resolve({
|
||
|
advanced_mode: opts.advanced,
|
||
|
batch: false,
|
||
|
fields: getFieldsTree(serverData),
|
||
|
file_length: opts.has_headers ? totalRows - 1 : totalRows,
|
||
|
header_types: false,
|
||
|
headers: headers,
|
||
|
matches: opts.has_headers && getMatches(serverData, headers),
|
||
|
options: {
|
||
|
...opts,
|
||
|
sheet: opts.sheet.length ? opts.sheet : "Template",
|
||
|
sheets: ["Template", "Template 2"],
|
||
|
},
|
||
|
preview: opts.has_headers
|
||
|
? fakePreviewData.map((col) => col.shift() && col)
|
||
|
: [...fakePreviewData],
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function customParsePreview(opts, { fields, headers, rowCount, matches, preview }) {
|
||
|
totalRows = rowCount;
|
||
|
|
||
|
return Promise.resolve({
|
||
|
advanced_mode: opts.advanced,
|
||
|
batch: false,
|
||
|
fields: fields,
|
||
|
file_length: opts.has_headers ? totalRows - 1 : totalRows,
|
||
|
header_types: false,
|
||
|
headers: headers,
|
||
|
matches: matches,
|
||
|
options: {
|
||
|
...opts,
|
||
|
sheet: opts.sheet.length ? opts.sheet : "Template",
|
||
|
sheets: ["Template", "Template 2"],
|
||
|
},
|
||
|
preview: preview,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// since executing a real import would be difficult, this method simply returns
|
||
|
// some error messages to help testing the UI
|
||
|
function executeFailingImport(field, isMultiline, field_path = "") {
|
||
|
let moreInfo = [];
|
||
|
if (serverData.models.partner.fields[field].type === "selection") {
|
||
|
moreInfo = serverData.models.partner.fields[field].selection;
|
||
|
}
|
||
|
return {
|
||
|
ids: false,
|
||
|
messages: isMultiline
|
||
|
? [
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Invalid value",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 0, to: 0 },
|
||
|
value: "Invalid value",
|
||
|
priority: "info",
|
||
|
},
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Duplicate value",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 1, to: 1 },
|
||
|
priority: "error",
|
||
|
},
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Wrong values",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 2, to: 3 },
|
||
|
priority: "warning",
|
||
|
},
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Bad value here",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 4, to: 4 },
|
||
|
value: "Bad value",
|
||
|
priority: "warning",
|
||
|
},
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Duplicate value",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 5, to: 5 },
|
||
|
priority: "error",
|
||
|
},
|
||
|
]
|
||
|
: [
|
||
|
{
|
||
|
field,
|
||
|
field_name: serverData.models.partner.fields[field].string,
|
||
|
field_path,
|
||
|
message: "Incorrect value",
|
||
|
moreInfo,
|
||
|
record: 0,
|
||
|
rows: { from: 0, to: 0 },
|
||
|
},
|
||
|
],
|
||
|
name: ["Some invalid content", "Wrong content", "Bad content"],
|
||
|
nextrow: 0,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
async function createImportAction(customRouter = {}) {
|
||
|
const router = {
|
||
|
"/web/dataset/call_kw/partner/get_import_templates": (route, args) => Promise.resolve([]),
|
||
|
"/web/dataset/call_kw/base_import.import/parse_preview": (route, args) =>
|
||
|
parsePreview(args[1]),
|
||
|
"/web/dataset/call_kw/base_import.import/execute_import": (route, args) =>
|
||
|
executeImport(args),
|
||
|
"/web/dataset/call_kw/base_import.import/create": (route, args) => Promise.resolve(11),
|
||
|
"base_import.import/get_fields": (route, args) =>
|
||
|
Promise.resolve(serverData.models.partner.fields),
|
||
|
};
|
||
|
|
||
|
for (const key in customRouter) {
|
||
|
router["/web/dataset/call_kw/" + key] = customRouter[key];
|
||
|
}
|
||
|
|
||
|
const webClient = await createWebClient({
|
||
|
serverData,
|
||
|
mockRPC: function (route, { args }) {
|
||
|
if (route in router) {
|
||
|
return router[route](route.replace("/web/dataset/call_kw/", ""), args);
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await doAction(webClient, 1);
|
||
|
}
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
//#endregion
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Tests
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
QUnit.module("Base Import Tests", (hooks) => {
|
||
|
hooks.beforeEach(async () => {
|
||
|
target = getFixture();
|
||
|
serverData = {
|
||
|
actions: {
|
||
|
1: {
|
||
|
name: "Import Data",
|
||
|
tag: "import",
|
||
|
target: "current",
|
||
|
type: "ir.actions.client",
|
||
|
params: {
|
||
|
model: "partner",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
models: {
|
||
|
partner: {
|
||
|
fields: {
|
||
|
display_name: { string: "Display name", type: "char" },
|
||
|
foo: { string: "Foo", type: "char" },
|
||
|
bar: { string: "Bar", type: "boolean", model_name: "partner" },
|
||
|
selection: {
|
||
|
string: "Selection",
|
||
|
type: "selection",
|
||
|
selection: [
|
||
|
["item_1", "First Item"],
|
||
|
["item_2", "Second item"],
|
||
|
],
|
||
|
model_name: "partner",
|
||
|
},
|
||
|
many2many_field: {
|
||
|
string: "Many2Many",
|
||
|
type: "many2many",
|
||
|
relation: "partner",
|
||
|
comodel_name: "comodel.test",
|
||
|
},
|
||
|
},
|
||
|
records: [],
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
target = getFixture();
|
||
|
});
|
||
|
|
||
|
QUnit.module("ImportAction");
|
||
|
|
||
|
QUnit.test("Import view: UI before file upload", async function (assert) {
|
||
|
const templateURL = "/myTemplateURL.xlsx";
|
||
|
|
||
|
await createImportAction({
|
||
|
"partner/get_import_templates": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return Promise.resolve([
|
||
|
{
|
||
|
label: "Some Import Template",
|
||
|
template: templateURL,
|
||
|
},
|
||
|
]);
|
||
|
},
|
||
|
"base_import.import/create": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return Promise.resolve(11);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
assert.containsOnce(target, ".o_import_action", "import view is displayed");
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_nocontent_help .btn-outline-primary").textContent,
|
||
|
" Some Import Template"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_nocontent_help .btn-outline-primary").href,
|
||
|
window.location.origin + templateURL,
|
||
|
"button has the right download url"
|
||
|
);
|
||
|
assert.verifySteps(["partner/get_import_templates", "base_import.import/create"]);
|
||
|
// Contains invisible mobile buttons
|
||
|
assert.containsN(
|
||
|
target,
|
||
|
".o_control_panel button",
|
||
|
5,
|
||
|
"only two buttons are visible by default"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: import a file with multiple sheets", async function (assert) {
|
||
|
registerFakeHTTPService((route, params) => {
|
||
|
assert.strictEqual(route, "/base_import/set_file");
|
||
|
assert.strictEqual(
|
||
|
params.ufile[0].name,
|
||
|
"fake_file.xlsx",
|
||
|
"file is correctly uploaded to the server"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"partner/get_import_templates": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return Promise.resolve([]);
|
||
|
},
|
||
|
"base_import.import/parse_preview": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return parsePreview(args[1]);
|
||
|
},
|
||
|
"base_import.import/create": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return Promise.resolve(11);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
assert.verifySteps([
|
||
|
"partner/get_import_templates",
|
||
|
"base_import.import/create",
|
||
|
"base_import.import/parse_preview",
|
||
|
]);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_action .o_import_data_sidepanel",
|
||
|
"side panel is visible"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_action .o_import_data_content",
|
||
|
"content panel is visible"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_sidepanel .fst-italic.truncate").textContent,
|
||
|
"fake_file",
|
||
|
"filename is shown and can be truncated"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_sidepanel .fst-italic:not(.truncate)").textContent,
|
||
|
".xlsx",
|
||
|
"file extension is displayed on its own"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_sidepanel [name=o_import_sheet]")
|
||
|
.selectedOptions[0].textContent,
|
||
|
"Template",
|
||
|
"first sheet is selected by default"
|
||
|
);
|
||
|
|
||
|
assert.containsN(
|
||
|
target,
|
||
|
".o_import_data_content tbody > tr",
|
||
|
3,
|
||
|
"recognized values are displayed in the view"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
|
||
|
.textContent,
|
||
|
"Foo",
|
||
|
"column title is shown"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tr:first-child td span:nth-child(2)")
|
||
|
.textContent,
|
||
|
"Deco addict",
|
||
|
"first example is shown"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tr:first-child td span:nth-child(2)")
|
||
|
.dataset.tooltipInfo,
|
||
|
'{"lines":["Deco addict","Azure Interior","Brandon Freeman"]}',
|
||
|
"tooltip contains other examples"
|
||
|
);
|
||
|
assert.containsNone(
|
||
|
target,
|
||
|
".o_import_data_content tbody td:nth-child(3) .alert-info",
|
||
|
"no comments are shown"
|
||
|
);
|
||
|
|
||
|
// Select a field already selected for another column
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Display name");
|
||
|
assert.containsN(
|
||
|
target,
|
||
|
".o_import_data_content tbody td:nth-child(3) .alert-info",
|
||
|
2,
|
||
|
"two comments are shown"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tbody td:nth-child(3) .alert-info")
|
||
|
.textContent,
|
||
|
"This column will be concatenated in field Display name"
|
||
|
);
|
||
|
|
||
|
// Preview the second sheet
|
||
|
await editSelect(target, ".o_import_data_sidepanel [name=o_import_sheet]", "Template 2");
|
||
|
assert.verifySteps(
|
||
|
["base_import.import/parse_preview"],
|
||
|
"changing sheet has sent a new parse_preview request"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_sidepanel [name=o_import_sheet]")
|
||
|
.selectedOptions[0].textContent,
|
||
|
"Template 2",
|
||
|
"second sheet is now selected"
|
||
|
);
|
||
|
assert.containsNone(
|
||
|
target,
|
||
|
".o_import_data_content tbody td:nth-child(3) .alert-info",
|
||
|
"no comments are shown"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: default import options are correctly", async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": async (route, args) => {
|
||
|
return parsePreview(args[1]);
|
||
|
},
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
assert.step("execute_import");
|
||
|
assert.deepEqual(
|
||
|
args[3],
|
||
|
{
|
||
|
advanced: true,
|
||
|
date_format: "",
|
||
|
datetime_format: "",
|
||
|
encoding: "",
|
||
|
fallback_values: {},
|
||
|
float_decimal_separator: ".",
|
||
|
float_thousand_separator: ",",
|
||
|
has_headers: true,
|
||
|
import_set_empty_fields: [],
|
||
|
import_skip_records: [],
|
||
|
keep_matches: false,
|
||
|
limit: 2000,
|
||
|
name_create_enabled_fields: {},
|
||
|
quoting: '"',
|
||
|
separator: "",
|
||
|
sheet: "Template",
|
||
|
sheets: ["Template", "Template 2"],
|
||
|
skip: 0,
|
||
|
tracking_disable: true,
|
||
|
},
|
||
|
"options are defaulted as expected"
|
||
|
);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
);
|
||
|
assert.verifySteps(["execute_import"]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: import a CSV file with one sheet", async function (assert) {
|
||
|
registerFakeHTTPService((route, params) => {
|
||
|
assert.strictEqual(route, "/base_import/set_file");
|
||
|
assert.strictEqual(
|
||
|
params.ufile[0].name,
|
||
|
"fake_file.csv",
|
||
|
"file is correctly uploaded to the server"
|
||
|
);
|
||
|
});
|
||
|
await createImportAction();
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_data_sidepanel .o_import_formatting",
|
||
|
"formatting options are present in the side panel"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_action .o_import_data_content",
|
||
|
"content panel is visible"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: additional options in debug", async function (assert) {
|
||
|
patchWithCleanup(odoo, { debug: true });
|
||
|
registerFakeHTTPService();
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": (route, args) => {
|
||
|
assert.strictEqual(
|
||
|
args[1].advanced,
|
||
|
true,
|
||
|
"in debug, advanced_mode is set in parse_preview"
|
||
|
);
|
||
|
return parsePreview(args[1]);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
|
||
|
await nextTick();
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_data_sidepanel .o_import_debug_options",
|
||
|
"additional options are present in the side panel in debug mode"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test(
|
||
|
"Import view: execute import with option 'use first row as headers'",
|
||
|
async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
const notificationMock = (message) => {
|
||
|
assert.step(message);
|
||
|
return () => {};
|
||
|
};
|
||
|
registry
|
||
|
.category("services")
|
||
|
.add("notification", makeFakeNotificationService(notificationMock), {
|
||
|
force: true,
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": async (route, args) => {
|
||
|
assert.step(route);
|
||
|
return parsePreview(args[1]);
|
||
|
},
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
assert.step(route);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(
|
||
|
target,
|
||
|
".o_control_panel_main_buttons .d-none input[type='file']",
|
||
|
file
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_sidepanel input[type=checkbox]").checked,
|
||
|
true,
|
||
|
"by default, the checkbox is enabled"
|
||
|
);
|
||
|
assert.verifySteps(["base_import.import/parse_preview"]);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
|
||
|
.textContent,
|
||
|
"Foo",
|
||
|
"first row is used as column title"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .o_select_menu").textContent,
|
||
|
"Foo",
|
||
|
"as the column header could match with a database field, it is selected by default"
|
||
|
);
|
||
|
|
||
|
await click(target.querySelector(".o_import_data_sidepanel input[type=checkbox]"));
|
||
|
assert.verifySteps(["base_import.import/parse_preview"]);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
|
||
|
.textContent,
|
||
|
"Foo, Deco addict, Azure Interior, Brandon Freeman",
|
||
|
"column title is shown as a list of rows elements"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .o_select_menu").textContent,
|
||
|
"To import, select a field...",
|
||
|
"as the column couldn't match with the database, user must make a choice"
|
||
|
);
|
||
|
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
);
|
||
|
assert.containsNone(
|
||
|
target,
|
||
|
".o_notification_body",
|
||
|
"should not display a notification"
|
||
|
);
|
||
|
assert.verifySteps(["base_import.import/execute_import"]);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_data_content .alert-info",
|
||
|
"if no fields were selected to match, the import fails with a message"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_data_content .alert-danger",
|
||
|
"an error is also displayed"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"You must configure at least one field to import"
|
||
|
);
|
||
|
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Display name");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
);
|
||
|
assert.verifySteps([
|
||
|
"base_import.import/execute_import",
|
||
|
"1 records successfully imported",
|
||
|
]);
|
||
|
}
|
||
|
);
|
||
|
|
||
|
QUnit.test("Import view: import data that don't match (selection)", async function (assert) {
|
||
|
serverData.models.partner.fields.selection.required = true;
|
||
|
let shouldFail = true;
|
||
|
|
||
|
registerFakeHTTPService();
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
if (shouldFail) {
|
||
|
shouldFail = false;
|
||
|
return executeFailingImport(args[1][0]);
|
||
|
}
|
||
|
assert.deepEqual(
|
||
|
args[3].fallback_values,
|
||
|
{
|
||
|
selection: {
|
||
|
fallback_value: "item_2",
|
||
|
field_model: "partner",
|
||
|
field_type: "selection",
|
||
|
},
|
||
|
},
|
||
|
"selected fallback value has been given to the request"
|
||
|
);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
// For this test, we force the display of an error message if this field is set
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Selection");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"The file contains blocking errors (see below)",
|
||
|
"a message is shown if the import was blocked"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report p").textContent,
|
||
|
"Incorrect value",
|
||
|
"the message is displayed in the view"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_field_selection",
|
||
|
"an action can be set when the column cannot match a field"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_field_selection select").textContent,
|
||
|
"Prevent importSet to: First ItemSet to: Second item",
|
||
|
"'skip' option is not available, since the field is required"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_field_selection select").selectedOptions[0].textContent,
|
||
|
"Prevent import",
|
||
|
"prevent option is selected by default"
|
||
|
);
|
||
|
editSelect(target, ".o_import_field_selection select", "item_2");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid.",
|
||
|
"import is now successful"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_field_selection",
|
||
|
"options are still present to change the action to do when the column don't match"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: import data that don't match (boolean)", async function (assert) {
|
||
|
let shouldFail = true;
|
||
|
|
||
|
registerFakeHTTPService();
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
if (shouldFail) {
|
||
|
shouldFail = false;
|
||
|
return executeFailingImport(args[1][0]);
|
||
|
}
|
||
|
assert.deepEqual(
|
||
|
args[3].fallback_values,
|
||
|
{
|
||
|
bar: {
|
||
|
fallback_value: "false",
|
||
|
field_model: "partner",
|
||
|
field_type: "boolean",
|
||
|
},
|
||
|
},
|
||
|
"selected fallback value has been given to the request"
|
||
|
);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
// For this test, we force the display of an error message if this field is set
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Bar");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"The file contains blocking errors (see below)",
|
||
|
"a message is shown if the import was blocked"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_field_boolean select").textContent,
|
||
|
"Prevent importSet to: FalseSet to: TrueSkip record",
|
||
|
"options are 'prevent', choose a default boolean value or 'skip'"
|
||
|
);
|
||
|
editSelect(target, ".o_import_field_boolean select", "false");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid.",
|
||
|
"import is now successful"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: import data that don't match (many2many)", async function (assert) {
|
||
|
let executeCount = 0;
|
||
|
|
||
|
registerFakeHTTPService();
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
executeCount++;
|
||
|
if (executeCount === 1) {
|
||
|
return executeFailingImport(args[1][0]);
|
||
|
}
|
||
|
if (executeCount === 2) {
|
||
|
assert.deepEqual(
|
||
|
args[3].name_create_enabled_fields,
|
||
|
{
|
||
|
many2many_field: true,
|
||
|
},
|
||
|
"selected fallback value has been given to the request"
|
||
|
);
|
||
|
} else {
|
||
|
assert.deepEqual(
|
||
|
args[3].name_create_enabled_fields,
|
||
|
{},
|
||
|
"selected fallback value has been given to the request"
|
||
|
);
|
||
|
assert.deepEqual(
|
||
|
args[3].import_skip_records,
|
||
|
["many2many_field"],
|
||
|
"selected fallback value has been given to the request"
|
||
|
);
|
||
|
}
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
// For this test, we force the display of an error message if this field is set
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Many2Many");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"The file contains blocking errors (see below)",
|
||
|
"a message is shown if the import was blocked"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_field_many2many select").textContent,
|
||
|
"Prevent importSet value as emptySkip recordCreate new values",
|
||
|
"options are 'prevent', choose a default boolean value or 'skip'"
|
||
|
);
|
||
|
editSelect(target, ".o_import_field_many2many select", "name_create_enabled_fields");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid.",
|
||
|
"import is now successful"
|
||
|
);
|
||
|
editSelect(target, ".o_import_field_many2many select", "import_skip_records");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid.",
|
||
|
"import is still successful"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: import messages are grouped and sorted", async function (assert) {
|
||
|
const fakeHTTPService = {
|
||
|
start() {
|
||
|
return {
|
||
|
post: (route, params) => {
|
||
|
const file = {
|
||
|
id: 10,
|
||
|
name: params.ufile[0].name,
|
||
|
mimetype: "text/plain",
|
||
|
};
|
||
|
return JSON.stringify([file]);
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
};
|
||
|
serviceRegistry.add("http", fakeHTTPService);
|
||
|
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
const webClient = await createWebClient({
|
||
|
serverData,
|
||
|
mockRPC: function (route, { args }) {
|
||
|
if (route === "/web/dataset/call_kw/partner/get_import_templates") {
|
||
|
return Promise.resolve([]);
|
||
|
}
|
||
|
if (route === "/web/dataset/call_kw/base_import.import/parse_preview") {
|
||
|
return parsePreview(args[1]);
|
||
|
}
|
||
|
if (route === "/web/dataset/call_kw/base_import.import/get_fields") {
|
||
|
assert.step(route);
|
||
|
return Promise.resolve(serverData.models.partner.fields);
|
||
|
}
|
||
|
if (route === "/web/dataset/call_kw/base_import.import/execute_import") {
|
||
|
return executeFailingImport(args[1][0], true);
|
||
|
}
|
||
|
if (route === "/web/dataset/call_kw/base_import.import/create") {
|
||
|
return Promise.resolve(11);
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await doAction(webClient, 1);
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(1)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"The file contains blocking errors (see below)",
|
||
|
"a message is shown if the import was blocked"
|
||
|
);
|
||
|
// Check that errors have been sorted and grouped
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report p").textContent.trim().toLowerCase(),
|
||
|
"multiple errors occurred in field foo:"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report li:first-child").textContent.trim(),
|
||
|
"Duplicate value at multiple rows"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report li:nth-child(2)").textContent.trim(),
|
||
|
"Wrong values at multiple rows"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report li:nth-child(3)").textContent.trim(),
|
||
|
"Bad value at row 5"
|
||
|
);
|
||
|
assert.containsN(target, ".o_import_report li", 3, "only 3 errors are visible by default");
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report_count").textContent.trim(),
|
||
|
"1 more"
|
||
|
);
|
||
|
|
||
|
await click(target, ".o_import_report_count");
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_report_count + li").textContent.trim(),
|
||
|
"Invalid value at row 1 (Some invalid content)"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: test import in batches", async function (assert) {
|
||
|
let executeImportCount = 0;
|
||
|
registerFakeHTTPService();
|
||
|
|
||
|
patchWithCleanup(ImportAction.prototype, {
|
||
|
get isBatched() {
|
||
|
// make sure the UI displays the batched import options
|
||
|
return true;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
assert.deepEqual(
|
||
|
args[1],
|
||
|
["foo", "bar", "display_name"],
|
||
|
"param contains the list of matching fields"
|
||
|
);
|
||
|
assert.deepEqual(
|
||
|
args[2],
|
||
|
["foo", "bar", "display name"],
|
||
|
"param contains the list of associated columns"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
args[3].limit,
|
||
|
1,
|
||
|
"limit option is equal to the value set in the view"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
args[3].skip,
|
||
|
executeImportCount * args[3].limit,
|
||
|
"skip option increments at each import"
|
||
|
);
|
||
|
executeImportCount++;
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("input#o_import_batch_limit").value,
|
||
|
"2000",
|
||
|
"by default, the batch limit is set to 2000 rows"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("input#o_import_row_start").value,
|
||
|
"1",
|
||
|
"by default, the import starts at line 1"
|
||
|
);
|
||
|
|
||
|
await editInput(target, "input#o_import_batch_limit", 1);
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid.",
|
||
|
"a message is shown if the import test was successfull"
|
||
|
);
|
||
|
assert.strictEqual(executeImportCount, 3, "execute_import was called 3 times");
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: execute and pause import in batches", async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
|
||
|
patchWithCleanup(ImportAction.prototype, {
|
||
|
get isBatched() {
|
||
|
// make sure the UI displays the batched import options
|
||
|
return true;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(ImportBlockUI.prototype, {
|
||
|
setup() {
|
||
|
super.setup();
|
||
|
if (this.props.message === "Importing") {
|
||
|
assert.step("Block UI received the right text");
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(ImportDataProgress.prototype, {
|
||
|
setup() {
|
||
|
super.setup();
|
||
|
useEffect(
|
||
|
() => {
|
||
|
if (this.props.importProgress.step === 1) {
|
||
|
// Trigger a pause at this step to resume later from the view
|
||
|
assert.step("pause triggered during step 2");
|
||
|
this.interrupt();
|
||
|
}
|
||
|
},
|
||
|
() => [this.props.importProgress.step]
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
this.props.totalSteps,
|
||
|
3,
|
||
|
"progress bar receives the number of steps"
|
||
|
);
|
||
|
assert.deepEqual(
|
||
|
this.props.importProgress,
|
||
|
{
|
||
|
value: 0,
|
||
|
step: 1,
|
||
|
},
|
||
|
"progress status has been given to the progress bar"
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => executeImport(args, true),
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await editInput(target, "input#o_import_batch_limit", 1);
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
);
|
||
|
// Since a nextTick is added to each batch, we must wait twice before the end of the second batch
|
||
|
await nextTick();
|
||
|
await nextTick();
|
||
|
assert.verifySteps(["Block UI received the right text", "pause triggered during step 2"]);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_data_content div .alert-warning",
|
||
|
"a message is shown to indicate the user to resume from the third row"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-warning b:first-child").textContent,
|
||
|
"Click 'Resume' to proceed with the import, resuming at line 2.",
|
||
|
"a message is shown to indicate the user to resume from the third row"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
.textContent,
|
||
|
"Resume",
|
||
|
"button contains the right text"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("input#o_import_row_start").value,
|
||
|
"2",
|
||
|
"the import will resume at line 2"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_notification_body").textContent,
|
||
|
"1 records successfully imported",
|
||
|
"display a notification with the quantity of imported values"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: test and pause import in batches", async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
|
||
|
patchWithCleanup(ImportAction.prototype, {
|
||
|
get isBatched() {
|
||
|
// make sure the UI displays the batched import options
|
||
|
return true;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(ImportBlockUI.prototype, {
|
||
|
setup() {
|
||
|
super.setup();
|
||
|
if (this.props.message === "Testing") {
|
||
|
assert.step("Block UI received the right text");
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
patchWithCleanup(ImportDataProgress.prototype, {
|
||
|
setup() {
|
||
|
super.setup();
|
||
|
useEffect(
|
||
|
() => {
|
||
|
if (this.props.importProgress.step === 1) {
|
||
|
// Trigger a pause at this step to resume later from the view
|
||
|
assert.step("pause triggered during step 2");
|
||
|
this.interrupt();
|
||
|
}
|
||
|
},
|
||
|
() => [this.props.importProgress.step]
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
this.props.totalSteps,
|
||
|
3,
|
||
|
"progress bar receives the number of steps"
|
||
|
);
|
||
|
assert.deepEqual(
|
||
|
this.props.importProgress,
|
||
|
{
|
||
|
value: 0,
|
||
|
step: 1,
|
||
|
},
|
||
|
"progress status has been given to the progress bar"
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/execute_import": (route, args) => executeImport(args, true),
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await editInput(target, "input#o_import_batch_limit", 1);
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
// Since a nextTick is added to each batch, we must wait twice before the end of the second batch
|
||
|
await nextTick();
|
||
|
await nextTick();
|
||
|
assert.verifySteps(["Block UI received the right text", "pause triggered during step 2"]);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-info").textContent,
|
||
|
"Everything seems valid."
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
.textContent,
|
||
|
"Import",
|
||
|
"after testing, 'Resume' text is not shown"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("input#o_import_row_start").value,
|
||
|
"1",
|
||
|
"the import will resume at line 1"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test(
|
||
|
"Import view: relational fields correctly mapped on preview",
|
||
|
async function (assert) {
|
||
|
assert.expect(4);
|
||
|
|
||
|
registerFakeHTTPService();
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": async (route, args) => {
|
||
|
return customParsePreview(args[1], {
|
||
|
fields: [
|
||
|
{ id: "id", name: "id", string: "External ID", fields: [], type: "id" },
|
||
|
{
|
||
|
id: "display_name",
|
||
|
name: "display_name",
|
||
|
string: "Display Name",
|
||
|
fields: [],
|
||
|
type: "id",
|
||
|
},
|
||
|
{
|
||
|
id: "many2many_field",
|
||
|
name: "many2many_field",
|
||
|
string: "Many2Many",
|
||
|
fields: [
|
||
|
{
|
||
|
id: "id",
|
||
|
name: "id",
|
||
|
string: "External ID",
|
||
|
fields: [],
|
||
|
type: "id",
|
||
|
},
|
||
|
],
|
||
|
type: "id",
|
||
|
},
|
||
|
],
|
||
|
headers: ["id", "display_name", "many2many_field/id"],
|
||
|
rowCount: 5,
|
||
|
matches: {
|
||
|
0: ["id"],
|
||
|
1: ["display_name"],
|
||
|
2: ["many2many_field", "id"],
|
||
|
},
|
||
|
preview: [
|
||
|
["0", "1", "2"],
|
||
|
["Name 1", "Name 2", "Name 3"],
|
||
|
["", "1", "2"],
|
||
|
],
|
||
|
});
|
||
|
},
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
assert.deepEqual(
|
||
|
args[1],
|
||
|
["id", "display_name", "many2many_field/id"],
|
||
|
"The proper arguments are given for the import"
|
||
|
);
|
||
|
assert.deepEqual(
|
||
|
args[2],
|
||
|
["id", "display_name", "many2many_field/id"],
|
||
|
"The proper arguments are given for the import"
|
||
|
);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
|
||
|
await editInput(
|
||
|
target,
|
||
|
".o_control_panel_main_buttons .d-none input[type='file']",
|
||
|
file
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(
|
||
|
"tr:nth-child(3) .o_import_file_column_cell span.text-truncate"
|
||
|
).innerText,
|
||
|
"many2many_field/id",
|
||
|
"The third row should be the relational field"
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("tr:nth-child(3) .o_select_menu_toggler_slot span").innerText,
|
||
|
"Many2Many / External ID",
|
||
|
"The relational field should be selected by default and the name should be the full path."
|
||
|
);
|
||
|
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
|
||
|
QUnit.test("Import view: import errors with relational fields", async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": async (route, args) => {
|
||
|
return customParsePreview(args[1], {
|
||
|
fields: [
|
||
|
{ id: "id", name: "id", string: "External ID", fields: [], type: "id" },
|
||
|
{
|
||
|
id: "display_name",
|
||
|
name: "display_name",
|
||
|
string: "Display Name",
|
||
|
fields: [],
|
||
|
type: "id",
|
||
|
},
|
||
|
{
|
||
|
id: "many2many_field",
|
||
|
name: "many2many_field",
|
||
|
string: "Many2Many",
|
||
|
fields: [
|
||
|
{
|
||
|
id: "id",
|
||
|
name: "id",
|
||
|
string: "External ID",
|
||
|
fields: [],
|
||
|
type: "id",
|
||
|
},
|
||
|
],
|
||
|
type: "id",
|
||
|
},
|
||
|
],
|
||
|
headers: ["id", "display_name", "many2many_field/id"],
|
||
|
rowCount: 5,
|
||
|
matches: {
|
||
|
0: ["id"],
|
||
|
1: ["display_name"],
|
||
|
2: ["many2many_field", "id"],
|
||
|
},
|
||
|
preview: [
|
||
|
["0", "1", "2"],
|
||
|
["Name 1", "Name 2", "Name 3"],
|
||
|
["", "1", "2"],
|
||
|
],
|
||
|
});
|
||
|
},
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
return executeFailingImport(args[1][0], true, ["many2many_field", "id"]);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
// For this test, we force the display of an error message if this field is set
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Many2Many");
|
||
|
await click(
|
||
|
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_data_content .alert-danger").textContent,
|
||
|
"The file contains blocking errors (see below)",
|
||
|
"A message is shown if the import was blocked"
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("tr:nth-child(3) .o_import_file_column_cell span.text-truncate")
|
||
|
.innerText,
|
||
|
"many2many_field/id",
|
||
|
"The third row should be the relational field"
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("tr:nth-child(3) .o_select_menu_toggler_slot span").innerText,
|
||
|
"Many2Many / External ID",
|
||
|
"The relational field is properly mapped"
|
||
|
);
|
||
|
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
"tr:nth-child(3) .o_import_report.alert",
|
||
|
"The relational field should have error messages on his row"
|
||
|
);
|
||
|
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("tr:nth-child(3) .o_import_report.alert p b").innerText,
|
||
|
"Many2Many / External ID",
|
||
|
"The error should contain the full path of the relational field"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import view: date format should be converted to strftime", async function (assert) {
|
||
|
assert.expect(5);
|
||
|
registerFakeHTTPService();
|
||
|
let parseCount = 0;
|
||
|
await createImportAction({
|
||
|
"base_import.import/parse_preview": async (route, args) => {
|
||
|
parseCount++;
|
||
|
const response = await parsePreview(args[1]);
|
||
|
if (parseCount === 2) {
|
||
|
assert.strictEqual(
|
||
|
response.options.date_format,
|
||
|
"%Y%m%d",
|
||
|
"server sends back a strftime formatted date"
|
||
|
);
|
||
|
}
|
||
|
return response;
|
||
|
},
|
||
|
"base_import.import/execute_import": (route, args) => {
|
||
|
assert.step("execute_import");
|
||
|
assert.strictEqual(
|
||
|
args[3].date_format,
|
||
|
"%Y%m%d",
|
||
|
"date is converted to strftime as expected during the import"
|
||
|
);
|
||
|
return executeImport(args);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await editInput(target, ".o_import_date_format#date_format-3", "YYYYMMDD");
|
||
|
|
||
|
// Parse the file again with the updated date format to check that
|
||
|
// the format is correctly formatted in the UI
|
||
|
await click(target.querySelector(".o_import_formatting button"));
|
||
|
await click(
|
||
|
$(target).find(
|
||
|
".o_control_panel_main_buttons > div:visible > button:contains(Import)"
|
||
|
)[0]
|
||
|
);
|
||
|
assert.verifySteps(["execute_import"]);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_date_format").value,
|
||
|
"YYYYMMDD",
|
||
|
"UI displays the human formatted date"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Import action: field selection has a clear button", async function (assert) {
|
||
|
registerFakeHTTPService();
|
||
|
patchWithCleanup(browser, {
|
||
|
setTimeout: (fn) => fn(),
|
||
|
});
|
||
|
|
||
|
await createImportAction();
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
|
||
|
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Bar");
|
||
|
assert.containsN(
|
||
|
target,
|
||
|
".o_select_menu_toggler_clear",
|
||
|
2,
|
||
|
"clear button is present for each field to unselect it"
|
||
|
);
|
||
|
|
||
|
await click(target.querySelector(".o_select_menu_toggler_clear"));
|
||
|
assert.strictEqual(
|
||
|
target.querySelector("tr:nth-child(2) .o_select_menu").textContent,
|
||
|
"To import, select a field...",
|
||
|
"field has been unselected"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test(
|
||
|
"Import a CSV: formatting options for date and datetime options",
|
||
|
async function (assert) {
|
||
|
registerFakeHTTPService((route, params) => {
|
||
|
assert.strictEqual(route, "/base_import/set_file");
|
||
|
assert.strictEqual(
|
||
|
params.ufile[0].name,
|
||
|
"fake_file.csv",
|
||
|
"file is correctly uploaded to the server"
|
||
|
);
|
||
|
});
|
||
|
await createImportAction();
|
||
|
|
||
|
// Set and trigger the change of a file for the input
|
||
|
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
|
||
|
await editInput(target, ".o_control_panel_collapsed_create input[type='file']", file);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_date_format").list.id,
|
||
|
"list-3",
|
||
|
"a datalist is associated to the date input"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target.querySelector(".o_import_date_format").previousElementSibling,
|
||
|
"sup",
|
||
|
"a tooltip is displayed on the label of the option"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_date_format").list.options[0].value,
|
||
|
"YYYY-MM-DD",
|
||
|
"an option for datetime has the right value"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_datetime_format").list.id,
|
||
|
"list-4",
|
||
|
"a datalist is associated to the datetime input"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target.querySelector(".o_import_datetime_format").previousElementSibling,
|
||
|
"sup",
|
||
|
"a tooltip is displayed on the label of the option"
|
||
|
);
|
||
|
assert.strictEqual(
|
||
|
target.querySelector(".o_import_datetime_format").list.options[0].value,
|
||
|
"YYYY-MM-DD HH:mm:SS",
|
||
|
"an option for datetime has the right value"
|
||
|
);
|
||
|
assert.containsOnce(
|
||
|
target,
|
||
|
".o_import_action .o_import_data_content",
|
||
|
"content panel is visible"
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
});
|