mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 23:39:28 +00:00
test: remove standalone serial and barcode tests
This commit is contained in:
@@ -1,176 +0,0 @@
|
||||
/* eslint-env node */
|
||||
const assert = require("node:assert/strict");
|
||||
const { readFileSync } = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const vm = require("node:vm");
|
||||
|
||||
function setupScanner() {
|
||||
const items = [];
|
||||
const item_requests = [];
|
||||
const alerts = [];
|
||||
let scan_result;
|
||||
const frm = {
|
||||
doctype: "Delivery Note",
|
||||
doc: { doctype: "Delivery Note", items },
|
||||
fields_dict: {
|
||||
scan_barcode: {
|
||||
value: "",
|
||||
set_value(value) {
|
||||
this.value = value;
|
||||
},
|
||||
},
|
||||
items: { grid: { doctype: "Delivery Note Item" } },
|
||||
},
|
||||
script_manager: { trigger() {} },
|
||||
};
|
||||
const frappe = {
|
||||
flags: {},
|
||||
meta: { has_field: (doctype, field) => field !== "last_scanned_warehouse" },
|
||||
utils: { get_link_title: () => "PHYSICAL-123", add_link_title() {} },
|
||||
call: async () => ({ message: scan_result }),
|
||||
show_alert: (alert) => alerts.push(alert),
|
||||
run_serially: (tasks) =>
|
||||
tasks.reduce((pending, task) => pending.then(task), Promise.resolve()),
|
||||
model: {
|
||||
add_child: (doc, doctype) => {
|
||||
const row = {
|
||||
doctype,
|
||||
name: `row-${items.length + 1}`,
|
||||
idx: items.length + 1,
|
||||
qty: 1,
|
||||
};
|
||||
items.push(row);
|
||||
return row;
|
||||
},
|
||||
set_value: async (doctype, name, field, value) => {
|
||||
const row = items.find((item) => item.name === name);
|
||||
const values = typeof field === "string" ? { [field]: value } : field;
|
||||
const item_changed =
|
||||
values.item_code && values.item_code !== row.item_code;
|
||||
// Frappe sets all supplied fields before running their change handlers.
|
||||
Object.assign(row, values);
|
||||
if (item_changed) {
|
||||
item_requests.push({ ...row });
|
||||
// Outward item selection auto-picks stock if the scanned references were absent.
|
||||
row.serial_no ||= "auto-picked-id";
|
||||
row.batch_no ||= "auto-picked-batch";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const context = {
|
||||
frappe,
|
||||
erpnext: { utils: {} },
|
||||
__: (text) => text,
|
||||
refresh_field() {},
|
||||
flt: Number,
|
||||
};
|
||||
vm.runInNewContext(
|
||||
readFileSync(path.join(__dirname, "../utils/barcode_scanner.js"), "utf8"),
|
||||
context
|
||||
);
|
||||
const scanner = new context.erpnext.utils.BarcodeScanner({ frm });
|
||||
const scan = (serial_no, item_code = "ITEM-A") => {
|
||||
scan_result = {
|
||||
item_code,
|
||||
serial_no,
|
||||
serial_number: "PHYSICAL-123",
|
||||
batch_no: `batch-${item_code}`,
|
||||
has_serial_no: 1,
|
||||
has_batch_no: 1,
|
||||
};
|
||||
frm.fields_dict.scan_barcode.value = "PHYSICAL-123";
|
||||
return scanner.process_scan();
|
||||
};
|
||||
return { scanner, scan, items, item_requests, alerts, frm, frappe };
|
||||
}
|
||||
|
||||
test("one scan sets its serial and batch before item auto-selection and adds one unit", async () => {
|
||||
const { scan, items, item_requests } = setupScanner();
|
||||
await scan("scanned-id");
|
||||
assert.equal(item_requests[0].serial_no, "scanned-id");
|
||||
assert.equal(item_requests[0].batch_no, "batch-ITEM-A");
|
||||
assert.equal(item_requests[0].qty, 1);
|
||||
assert.equal(items[0].serial_no, "scanned-id");
|
||||
assert.equal(items[0].qty, 1);
|
||||
});
|
||||
|
||||
test("rescanning the same serial does not append it or increase quantity", async () => {
|
||||
const { scan, items, alerts } = setupScanner();
|
||||
await scan("scanned-id");
|
||||
await assert.rejects(scan("scanned-id"));
|
||||
assert.equal(items.length, 1);
|
||||
assert.equal(items[0].serial_no, "scanned-id");
|
||||
assert.equal(items[0].qty, 1);
|
||||
assert.equal(alerts.at(-1).indicator, "orange");
|
||||
});
|
||||
|
||||
test("distinct serial IDs, including prefixes, each add one unit", async () => {
|
||||
const { scan, items } = setupScanner();
|
||||
await scan("scanned-id-long");
|
||||
await scan("scanned-id");
|
||||
assert.equal(items[0].serial_no, "scanned-id-long\nscanned-id");
|
||||
assert.equal(items[0].qty, 2);
|
||||
});
|
||||
|
||||
test("matching physical numbers on different items keep their separate serial IDs", async () => {
|
||||
const { scan, items } = setupScanner();
|
||||
await scan("item-a-id", "ITEM-A");
|
||||
await scan("item-b-id", "ITEM-B");
|
||||
assert.deepEqual(
|
||||
items.map((row) => [row.item_code, row.serial_no, row.qty]),
|
||||
[
|
||||
["ITEM-A", "item-a-id", 1],
|
||||
["ITEM-B", "item-b-id", 1],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("scanning into an empty default row starts with one unit", async () => {
|
||||
const { scan, items, frappe, frm } = setupScanner();
|
||||
frappe.model.add_child(frm.doc, "Delivery Note Item");
|
||||
await scan("scanned-id");
|
||||
assert.equal(items.length, 1);
|
||||
assert.equal(items[0].serial_no, "scanned-id");
|
||||
assert.equal(items[0].qty, 1);
|
||||
});
|
||||
|
||||
test("the scan dialog replaces auto-selected serials with its scanned list", async () => {
|
||||
const { scanner, items, frappe, frm } = setupScanner();
|
||||
frappe.ui = {
|
||||
Dialog: class {
|
||||
constructor({ fields }) {
|
||||
this.values = Object.fromEntries(
|
||||
fields.map((field) => [field.fieldname, field.default])
|
||||
);
|
||||
this.$wrapper = { find: () => ({ css() {} }) };
|
||||
}
|
||||
set_primary_action(label, action) {
|
||||
this.primary_action = action;
|
||||
}
|
||||
get_value(field) {
|
||||
return this.values[field];
|
||||
}
|
||||
show() {}
|
||||
hide() {}
|
||||
},
|
||||
};
|
||||
const row = frappe.model.add_child(frm.doc, "Delivery Note Item");
|
||||
Object.assign(row, {
|
||||
item_code: "ITEM-A",
|
||||
serial_no: "auto-picked-id",
|
||||
batch_no: "batch-ITEM-A",
|
||||
});
|
||||
scanner.prepare_item_for_scan(
|
||||
row,
|
||||
"ITEM-A",
|
||||
null,
|
||||
"batch-ITEM-A",
|
||||
"scanned-id"
|
||||
);
|
||||
await scanner.dialog.primary_action();
|
||||
assert.equal(items[0].serial_no, "scanned-id");
|
||||
assert.equal(items[0].qty, 1);
|
||||
assert.equal(items[0].has_item_scanned, 1);
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
/* eslint-env node */
|
||||
const assert = require("node:assert/strict");
|
||||
const { readFileSync } = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const vm = require("node:vm");
|
||||
|
||||
function setup(xcall) {
|
||||
class Control {
|
||||
get_model_value() {
|
||||
return this.doc.serial_no;
|
||||
}
|
||||
parse_validate_and_set_in_model(value) {
|
||||
this.doc.serial_no = value;
|
||||
}
|
||||
set_formatted_input(value) {
|
||||
this.display = value;
|
||||
}
|
||||
set_disp_area() {}
|
||||
}
|
||||
const titles = new Map();
|
||||
const handlers = {};
|
||||
const frappe = {
|
||||
xcall,
|
||||
ui: {
|
||||
form: {
|
||||
ControlSmallText: Control,
|
||||
ControlLink: Control,
|
||||
ControlText: Control,
|
||||
ControlLongText: Control,
|
||||
on: (doctype, events) => {
|
||||
handlers[doctype] = events;
|
||||
},
|
||||
},
|
||||
},
|
||||
utils: {
|
||||
get_link_title: (doctype, name) => titles.get(name),
|
||||
add_link_title: (doctype, name, value) => titles.set(name, value),
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(
|
||||
readFileSync(
|
||||
path.join(__dirname, "../utils/serial_batch_input.js"),
|
||||
"utf8"
|
||||
),
|
||||
{ frappe }
|
||||
);
|
||||
const control = new frappe.ui.form.ControlSmallText();
|
||||
control.frm = {
|
||||
doctype: "Purchase Receipt",
|
||||
doc: { doctype: "Purchase Receipt" },
|
||||
};
|
||||
control.df = { parent: "Purchase Receipt Item", fieldname: "serial_no" };
|
||||
control.doc = { item_code: "ITEM-B", serial_no: "existing-id" };
|
||||
return { control, handlers, titles, frappe };
|
||||
}
|
||||
|
||||
test("physical input resolves using the row's item, even when it resembles an existing ID", async () => {
|
||||
let request;
|
||||
const { control } = setup(async (method, args) => {
|
||||
request = args;
|
||||
return ["new-id"];
|
||||
});
|
||||
await control.parse_validate_and_set_in_model("existing-id", {});
|
||||
assert.equal(request.row.item_code, "ITEM-B");
|
||||
assert.deepEqual(Array.from(request.numbers), ["existing-id"]);
|
||||
assert.equal(control.doc.serial_no, "new-id");
|
||||
assert.equal(control.serial_number_text("new-id"), "existing-id");
|
||||
});
|
||||
|
||||
test("programmatic ID updates do not resolve the ID as a physical number", async () => {
|
||||
const { control } = setup(() => {
|
||||
throw new Error("Unexpected lookup");
|
||||
});
|
||||
await control.parse_validate_and_set_in_model("another-id", null);
|
||||
assert.equal(control.doc.serial_no, "another-id");
|
||||
});
|
||||
|
||||
test("saving waits for number resolution and stale responses cannot overwrite newer input", async () => {
|
||||
const responses = [];
|
||||
const { control, handlers } = setup(
|
||||
() => new Promise((resolve) => responses.push(resolve))
|
||||
);
|
||||
const first = control.parse_validate_and_set_in_model("first-number", {});
|
||||
const second = control.parse_validate_and_set_in_model("second-number", {});
|
||||
let saved = false;
|
||||
const saving = handlers[control.frm.doctype]
|
||||
.before_save(control.frm)
|
||||
.then(() => {
|
||||
saved = true;
|
||||
});
|
||||
assert.equal(saved, false);
|
||||
responses[1](["second-id"]);
|
||||
await second;
|
||||
responses[0](["first-id"]);
|
||||
await first;
|
||||
await saving;
|
||||
assert.equal(control.doc.serial_no, "second-id");
|
||||
assert.equal(saved, true);
|
||||
});
|
||||
|
||||
test("an ambiguous scan requires a selection and cancel leaves it unresolved", async () => {
|
||||
let dialog;
|
||||
const context = {
|
||||
erpnext: { utils: {} },
|
||||
__: (text) => text,
|
||||
frappe: {
|
||||
ui: {
|
||||
Dialog: class {
|
||||
constructor(options) {
|
||||
Object.assign(this, options);
|
||||
dialog = this;
|
||||
}
|
||||
show() {}
|
||||
hide() {
|
||||
this.onhide();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(
|
||||
readFileSync(path.join(__dirname, "../utils/barcode_scanner.js"), "utf8"),
|
||||
context
|
||||
);
|
||||
const scanner = Object.create(context.erpnext.utils.BarcodeScanner.prototype);
|
||||
const candidates = [
|
||||
{ item_code: "A", barcode: "123" },
|
||||
{ item_code: "A", serial_no: "id-a", serial_number: "123" },
|
||||
{ item_code: "B", serial_no: "id-b", serial_number: "123" },
|
||||
];
|
||||
const selection = scanner.select_scan_match(candidates);
|
||||
const item_field = dialog.fields[0];
|
||||
assert.equal(item_field.fieldtype, "Link");
|
||||
assert.equal(item_field.options, "Item");
|
||||
assert.deepEqual(Array.from(item_field.get_query().filters.name[1]), [
|
||||
"A",
|
||||
"B",
|
||||
]);
|
||||
dialog.primary_action({ item_code: "B" });
|
||||
assert.equal(await selection, candidates[2]);
|
||||
assert.equal(
|
||||
await scanner.select_scan_match(candidates.slice(0, 2)),
|
||||
candidates[1]
|
||||
);
|
||||
const cancelled = scanner.select_scan_match(candidates);
|
||||
dialog.hide();
|
||||
assert.equal(await cancelled, null);
|
||||
});
|
||||
|
||||
test("a programmatic update invalidates a pending keyboard lookup", async () => {
|
||||
let resolve;
|
||||
const { control } = setup(
|
||||
() =>
|
||||
new Promise((callback) => {
|
||||
resolve = callback;
|
||||
})
|
||||
);
|
||||
const typing = control.parse_validate_and_set_in_model("typed-number", {});
|
||||
await control.parse_validate_and_set_in_model("selected-id", null);
|
||||
resolve(["typed-id"]);
|
||||
await typing;
|
||||
assert.equal(control.doc.serial_no, "selected-id");
|
||||
});
|
||||
|
||||
test("typed batch input uses the physical label even when the control mapped it to an old ID", async () => {
|
||||
let request;
|
||||
const { control, frappe } = setup(async (method, args) => {
|
||||
request = args;
|
||||
return { batch_nos: ["item-b-batch-id"] };
|
||||
});
|
||||
const link = new frappe.ui.form.ControlLink();
|
||||
link.frm = control.frm;
|
||||
link.doc = control.doc;
|
||||
link.get_options = () => "Batch";
|
||||
link.get_label_value = () => "physical-batch-number";
|
||||
await link.parse_validate_and_set_in_model("item-a-batch-id", {}, undefined);
|
||||
assert.equal(request.item_code, "ITEM-B");
|
||||
assert.deepEqual(Array.from(request.batch_numbers), [
|
||||
"physical-batch-number",
|
||||
]);
|
||||
assert.equal(link.doc.serial_no, "item-b-batch-id");
|
||||
await link.parse_validate_and_set_in_model("programmatic-id", null);
|
||||
assert.equal(link.doc.serial_no, "programmatic-id");
|
||||
await link.parse_validate_and_set_in_model(
|
||||
"item-a-batch-id",
|
||||
null,
|
||||
"selected-physical-label"
|
||||
);
|
||||
assert.deepEqual(Array.from(request.batch_numbers), [
|
||||
"selected-physical-label",
|
||||
]);
|
||||
});
|
||||
|
||||
test("POS controls use their explicit item and form context", async () => {
|
||||
let request;
|
||||
const { control } = setup(async (method, args) => {
|
||||
request = args;
|
||||
return ["pos-serial-id"];
|
||||
});
|
||||
control.serial_batch_context = { frm: control.frm, row: control.doc };
|
||||
control.frm = undefined;
|
||||
await control.parse_validate_and_set_in_model("POS-PHYSICAL", {});
|
||||
assert.equal(request.row.item_code, "ITEM-B");
|
||||
assert.equal(control.doc.serial_no, "pos-serial-id");
|
||||
});
|
||||
|
||||
test("report numbers display physical labels and link to internal IDs", () => {
|
||||
let linked;
|
||||
const context = {
|
||||
frappe: {
|
||||
model: { can_read: () => true },
|
||||
form: {
|
||||
formatters: {
|
||||
Link: (id, df, options) => {
|
||||
linked = { id, options };
|
||||
return options.label;
|
||||
},
|
||||
},
|
||||
},
|
||||
utils: {
|
||||
escape_html: (value) =>
|
||||
value.replaceAll("<", "<").replaceAll(">", ">"),
|
||||
},
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(
|
||||
readFileSync(
|
||||
path.join(__dirname, "../utils/serial_batch_display.js"),
|
||||
"utf8"
|
||||
),
|
||||
context
|
||||
);
|
||||
const format = context.frappe.form.formatters.SerialBatchNumber;
|
||||
const field = { reference_field: "serial_no", options: "Serial No" };
|
||||
assert.equal(
|
||||
format("PHYSICAL-123", field, {}, { serial_no: "internal-id" }),
|
||||
"PHYSICAL-123"
|
||||
);
|
||||
assert.equal(linked.id, "internal-id");
|
||||
assert.equal(linked.options.label, "PHYSICAL-123");
|
||||
context.frappe.model.can_read = () => false;
|
||||
assert.equal(
|
||||
format("PHYSICAL-123", field, {}, { serial_no: "internal-id" }),
|
||||
"PHYSICAL-123"
|
||||
);
|
||||
assert.equal(
|
||||
format(
|
||||
"<serial>",
|
||||
field,
|
||||
{ for_print: true },
|
||||
{ serial_no: "internal-id" }
|
||||
),
|
||||
"<serial>"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user