Merge branch 'frappe:develop' into develop
This commit is contained in:
commit
5b461bfa75
@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.add_custom_button(__("New Version"), function() {
|
||||||
|
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||||
|
frappe.set_route("Form", "BOM", new_bom.name);
|
||||||
|
});
|
||||||
|
|
||||||
if(frm.doc.docstatus==1) {
|
if(frm.doc.docstatus==1) {
|
||||||
frm.add_custom_button(__("Work Order"), function() {
|
frm.add_custom_button(__("Work Order"), function() {
|
||||||
frm.trigger("make_work_order");
|
frm.trigger("make_work_order");
|
||||||
|
@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list
|
|||||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
|
class BOMRecursionError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BOMTree:
|
class BOMTree:
|
||||||
"""Full tree representation of a BOM"""
|
"""Full tree representation of a BOM"""
|
||||||
|
|
||||||
@ -251,9 +255,8 @@ class BOM(WebsiteGenerator):
|
|||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
self.validate_bom_currency(item)
|
self.validate_bom_currency(item)
|
||||||
|
|
||||||
item.bom_no = ""
|
if item.do_not_explode:
|
||||||
if not item.do_not_explode:
|
item.bom_no = ""
|
||||||
item.bom_no = item.bom_no
|
|
||||||
|
|
||||||
ret = self.get_bom_material_detail(
|
ret = self.get_bom_material_detail(
|
||||||
{
|
{
|
||||||
@ -555,35 +558,27 @@ class BOM(WebsiteGenerator):
|
|||||||
"""Check whether recursion occurs in any bom"""
|
"""Check whether recursion occurs in any bom"""
|
||||||
|
|
||||||
def _throw_error(bom_name):
|
def _throw_error(bom_name):
|
||||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
|
frappe.throw(
|
||||||
|
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
|
||||||
|
exc=BOMRecursionError,
|
||||||
|
)
|
||||||
|
|
||||||
bom_list = self.traverse_tree()
|
bom_list = self.traverse_tree()
|
||||||
child_items = (
|
child_items = frappe.get_all(
|
||||||
frappe.get_all(
|
"BOM Item",
|
||||||
"BOM Item",
|
fields=["bom_no", "item_code"],
|
||||||
fields=["bom_no", "item_code"],
|
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
child_bom = {d.bom_no for d in child_items}
|
for item in child_items:
|
||||||
child_items_codes = {d.item_code for d in child_items}
|
if self.name == item.bom_no:
|
||||||
|
_throw_error(self.name)
|
||||||
|
if self.item == item.item_code and item.bom_no:
|
||||||
|
# Same item but with different BOM should not be allowed.
|
||||||
|
# Same item can appear recursively once as long as it doesn't have BOM.
|
||||||
|
_throw_error(item.bom_no)
|
||||||
|
|
||||||
if self.name in child_bom:
|
if self.name in {d.bom_no for d in self.items}:
|
||||||
_throw_error(self.name)
|
|
||||||
|
|
||||||
if self.item in child_items_codes:
|
|
||||||
_throw_error(self.item)
|
|
||||||
|
|
||||||
bom_nos = (
|
|
||||||
frappe.get_all(
|
|
||||||
"BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"}
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.name in {d.parent for d in bom_nos}:
|
|
||||||
_throw_error(self.name)
|
_throw_error(self.name)
|
||||||
|
|
||||||
def traverse_tree(self, bom_list=None):
|
def traverse_tree(self, bom_list=None):
|
||||||
|
@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase
|
|||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
|
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
@ -324,43 +324,36 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
def test_bom_recursion_1st_level(self):
|
def test_bom_recursion_1st_level(self):
|
||||||
"""BOM should not allow BOM item again in child"""
|
"""BOM should not allow BOM item again in child"""
|
||||||
item_code = "_Test BOM Recursion"
|
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item_code, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom = frappe.new_doc("BOM")
|
bom = frappe.new_doc("BOM")
|
||||||
bom.item = item_code
|
bom.item = item_code
|
||||||
bom.append("items", frappe._dict(item_code=item_code))
|
bom.append("items", frappe._dict(item_code=item_code))
|
||||||
with self.assertRaises(frappe.ValidationError) as err:
|
bom.save()
|
||||||
|
with self.assertRaises(BOMRecursionError):
|
||||||
|
bom.items[0].bom_no = bom.name
|
||||||
bom.save()
|
bom.save()
|
||||||
|
|
||||||
self.assertTrue("recursion" in str(err.exception).lower())
|
|
||||||
frappe.delete_doc("BOM", bom.name, ignore_missing=True)
|
|
||||||
|
|
||||||
def test_bom_recursion_transitive(self):
|
def test_bom_recursion_transitive(self):
|
||||||
item1 = "_Test BOM Recursion"
|
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||||
item2 = "_Test BOM Recursion 2"
|
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item1, {"is_stock_item": 1})
|
|
||||||
make_item(item2, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom1 = frappe.new_doc("BOM")
|
bom1 = frappe.new_doc("BOM")
|
||||||
bom1.item = item1
|
bom1.item = item1
|
||||||
bom1.append("items", frappe._dict(item_code=item2))
|
bom1.append("items", frappe._dict(item_code=item2))
|
||||||
bom1.save()
|
bom1.save()
|
||||||
bom1.submit()
|
|
||||||
|
|
||||||
bom2 = frappe.new_doc("BOM")
|
bom2 = frappe.new_doc("BOM")
|
||||||
bom2.item = item2
|
bom2.item = item2
|
||||||
bom2.append("items", frappe._dict(item_code=item1))
|
bom2.append("items", frappe._dict(item_code=item1))
|
||||||
|
bom2.save()
|
||||||
|
|
||||||
with self.assertRaises(frappe.ValidationError) as err:
|
bom2.items[0].bom_no = bom1.name
|
||||||
|
bom1.items[0].bom_no = bom2.name
|
||||||
|
|
||||||
|
with self.assertRaises(BOMRecursionError):
|
||||||
|
bom1.save()
|
||||||
bom2.save()
|
bom2.save()
|
||||||
bom2.submit()
|
|
||||||
|
|
||||||
self.assertTrue("recursion" in str(err.exception).lower())
|
|
||||||
|
|
||||||
bom1.cancel()
|
|
||||||
frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True)
|
|
||||||
frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True)
|
|
||||||
|
|
||||||
def test_bom_with_process_loss_item(self):
|
def test_bom_with_process_loss_item(self):
|
||||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||||
|
@ -34,7 +34,6 @@ def get_data(filters):
|
|||||||
if filters.get(field):
|
if filters.get(field):
|
||||||
query_filters[field] = ("in", filters.get(field))
|
query_filters[field] = ("in", filters.get(field))
|
||||||
|
|
||||||
|
|
||||||
query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
||||||
|
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
|
@ -35,6 +35,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
const input = this.scan_barcode_field.value;
|
const input = this.scan_barcode_field.value;
|
||||||
|
this.scan_barcode_field.set_value("");
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,51 +56,51 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = me.update_table(data);
|
me.update_table(data).then(row => {
|
||||||
if (row) {
|
row ? resolve(row) : reject();
|
||||||
resolve(row);
|
});
|
||||||
}
|
|
||||||
else {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update_table(data) {
|
update_table(data) {
|
||||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
return new Promise(resolve => {
|
||||||
|
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||||
|
|
||||||
const {item_code, barcode, batch_no, serial_no} = data;
|
const {item_code, barcode, batch_no, serial_no} = data;
|
||||||
|
|
||||||
let row = this.get_row_to_modify_on_scan(item_code, batch_no);
|
let row = this.get_row_to_modify_on_scan(item_code, batch_no);
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
if (this.dont_allow_new_row) {
|
if (this.dont_allow_new_row) {
|
||||||
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
|
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
|
||||||
|
this.clean_up();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new row if new item/batch is scanned
|
||||||
|
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
||||||
|
// trigger any row add triggers defined on child table.
|
||||||
|
this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.is_duplicate_serial_no(row, serial_no)) {
|
||||||
this.clean_up();
|
this.clean_up();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add new row if new item/batch is scanned
|
frappe.run_serially([
|
||||||
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
() => this.set_selector_trigger_flag(row, data),
|
||||||
// trigger any row add triggers defined on child table.
|
() => this.set_item(row, item_code).then(qty => {
|
||||||
this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
|
this.show_scan_message(row.idx, row.item_code, qty);
|
||||||
}
|
}),
|
||||||
|
() => this.set_serial_no(row, serial_no),
|
||||||
if (this.is_duplicate_serial_no(row, serial_no)) {
|
() => this.set_batch_no(row, batch_no),
|
||||||
this.clean_up();
|
() => this.set_barcode(row, barcode),
|
||||||
return;
|
() => this.clean_up(),
|
||||||
}
|
() => resolve(row)
|
||||||
|
]);
|
||||||
this.set_selector_trigger_flag(row, data);
|
|
||||||
this.set_item(row, item_code).then(qty => {
|
|
||||||
this.show_scan_message(row.idx, row.item_code, qty);
|
|
||||||
});
|
});
|
||||||
this.set_serial_no(row, serial_no);
|
|
||||||
this.set_batch_no(row, batch_no);
|
|
||||||
this.set_barcode(row, barcode);
|
|
||||||
this.clean_up();
|
|
||||||
return row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// batch and serial selector is reduandant when all info can be added by scan
|
// batch and serial selector is reduandant when all info can be added by scan
|
||||||
@ -117,25 +118,24 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
|
|
||||||
set_item(row, item_code) {
|
set_item(row, item_code) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const increment = (value = 1) => {
|
const increment = async (value = 1) => {
|
||||||
const item_data = {item_code: item_code};
|
const item_data = {item_code: item_code};
|
||||||
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
||||||
frappe.model.set_value(row.doctype, row.name, item_data);
|
await frappe.model.set_value(row.doctype, row.name, item_data);
|
||||||
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.prompt_qty) {
|
if (this.prompt_qty) {
|
||||||
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
||||||
increment(value);
|
increment(value).then((value) => resolve(value));
|
||||||
resolve(value);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
increment();
|
increment().then((value) => resolve(value));
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
set_serial_no(row, serial_no) {
|
async set_serial_no(row, serial_no) {
|
||||||
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
|
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
|
||||||
const existing_serial_nos = row[this.serial_no_field];
|
const existing_serial_nos = row[this.serial_no_field];
|
||||||
let new_serial_nos = "";
|
let new_serial_nos = "";
|
||||||
@ -145,19 +145,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
} else {
|
} else {
|
||||||
new_serial_nos = serial_no;
|
new_serial_nos = serial_no;
|
||||||
}
|
}
|
||||||
frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_batch_no(row, batch_no) {
|
async set_batch_no(row, batch_no) {
|
||||||
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
|
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
|
||||||
frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
|
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_barcode(row, barcode) {
|
async set_barcode(row, barcode) {
|
||||||
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
||||||
frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes
|
|
||||||
|
|
||||||
|
|
||||||
class TestSearch(unittest.TestCase):
|
|
||||||
# Search for the word "cond", part of the word "conduire" (Lead) in french.
|
|
||||||
def test_contact_search_in_foreign_language(self):
|
|
||||||
try:
|
|
||||||
frappe.local.lang_full_dict = None # reset cached translations
|
|
||||||
frappe.local.lang = "fr"
|
|
||||||
output = filter_dynamic_link_doctypes(
|
|
||||||
"DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"}
|
|
||||||
)
|
|
||||||
result = [["found" for x in y if x == "Lead"] for y in output]
|
|
||||||
self.assertTrue(["found"] in result)
|
|
||||||
finally:
|
|
||||||
frappe.local.lang = "en"
|
|
Loading…
Reference in New Issue
Block a user