diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8ab79e68be..619a415c8b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -31,7 +31,7 @@ class BOMTree: # specifying the attributes to save resources # ref: https://docs.python.org/3/reference/datamodel.html#slots - __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + __slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"] def __init__( self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 @@ -50,9 +50,10 @@ class BOMTree: def __create_tree(self): bom = frappe.get_cached_doc("BOM", self.name) self.item_code = bom.item + self.bom_qty = bom.quantity for item in bom.get("items", []): - qty = item.qty / bom.quantity # quantity per unit + qty = item.stock_qty / bom.quantity # quantity per unit exploded_qty = self.exploded_qty * qty if item.bom_no: child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d60feb2b39..01bf2e4315 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -6,7 +6,7 @@ from collections import deque from functools import partial import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import cstr, flt from erpnext.controllers.tests.test_subcontracting_controller import ( @@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"] class TestBOM(FrappeTestCase): + @timeout def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict @@ -37,6 +38,7 @@ class TestBOM(FrappeTestCase): self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 2) + @timeout def test_get_items_exploded(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict @@ -49,11 +51,13 @@ class TestBOM(FrappeTestCase): self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 3) + @timeout def test_get_items_list(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3) + @timeout def test_default_bom(self): def _get_default_bom_in_item(): return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom")) @@ -71,6 +75,7 @@ class TestBOM(FrappeTestCase): self.assertTrue(_get_default_bom_in_item(), bom.name) + @timeout def test_update_bom_cost_in_all_boms(self): # get current rate for '_Test Item 2' bom_rates = frappe.db.get_values( @@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase): ): self.assertEqual(d.base_rate, rm_base_rate + 10) + @timeout def test_bom_cost(self): bom = frappe.copy_doc(test_records[2]) bom.insert() @@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + @timeout def test_bom_cost_with_batch_size(self): bom = frappe.copy_doc(test_records[2]) bom.docstatus = 0 @@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.operating_cost, op_cost / 2) bom.delete() + @timeout def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): @@ -181,6 +189,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.base_raw_material_cost, 27000) self.assertEqual(bom.base_total_cost, 33000) + @timeout def test_bom_cost_multi_uom_based_on_valuation_rate(self): bom = frappe.copy_doc(test_records[2]) bom.set_rate_of_sub_assembly_item_based_on_bom = 0 @@ -202,6 +211,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.items[0].rate, 20) + @timeout def test_bom_cost_with_fg_based_operating_cost(self): bom = frappe.copy_doc(test_records[4]) bom.insert() @@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + @timeout def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" set_backflush_based_on("Material Transferred for Subcontract") @@ -310,6 +321,7 @@ class TestBOM(FrappeTestCase): supplied_items = sorted([d.rm_item_code for d in sco.supplied_items]) self.assertEqual(bom_items, supplied_items) + @timeout def test_bom_tree_representation(self): bom_tree = { "Assembly": { @@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) + @timeout def test_generated_variant_bom(self): from erpnext.controllers.item_variant import create_variant @@ -375,6 +388,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(reqd_item.qty, created_item.qty) self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) + @timeout def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" item_code = make_item(properties={"is_stock_item": 1}).name @@ -387,6 +401,7 @@ class TestBOM(FrappeTestCase): bom.items[0].bom_no = bom.name bom.save() + @timeout def test_bom_recursion_transitive(self): item1 = make_item(properties={"is_stock_item": 1}).name item2 = make_item(properties={"is_stock_item": 1}).name @@ -408,6 +423,7 @@ class TestBOM(FrappeTestCase): bom1.save() bom2.save() + @timeout def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() @@ -421,6 +437,7 @@ class TestBOM(FrappeTestCase): # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) + @timeout def test_bom_item_query(self): query = partial( item_query, @@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase): ) self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") + @timeout def test_exclude_exploded_items_from_bom(self): bom_no = get_default_bom() new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no)) @@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase): new_bom.delete() + @timeout def test_valid_transfer_defaults(self): bom_with_op = frappe.db.get_value( "BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1} @@ -489,11 +508,13 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.transfer_material_against, "Work Order") bom.delete() + @timeout def test_bom_name_length(self): """test >140 char names""" bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}} create_nested_bom(bom_tree, prefix="") + @timeout def test_version_index(self): bom = frappe.new_doc("BOM") @@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase): msg=f"Incorrect index for {existing_boms}", ) + @timeout def test_bom_versioning(self): bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}} bom = create_nested_bom(bom_tree, prefix="") @@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase): self.assertNotEqual(amendment.name, version.name) self.assertEqual(int(version.name.split("-")[-1]), 2) + @timeout def test_clear_inpection_quality(self): bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) @@ -565,6 +588,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.quality_inspection_template, None) + @timeout def test_bom_pricing_based_on_lpp(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.items[0].rate, 42) + @timeout def test_set_default_bom_for_item_having_single_bom(self): from erpnext.stock.doctype.item.test_item import make_item @@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase): bom.reload() self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name) + @timeout def test_exploded_items_rate(self): rm_item = make_item( properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} @@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + @timeout def test_bom_cost_update_flag(self): rm_item = make_item( properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 5dd557f8ab..2026f62914 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( update_cost_in_all_boms_in_test, @@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase): def tearDown(self): frappe.db.rollback() + @timeout def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" @@ -33,6 +34,7 @@ class TestBOMUpdateTool(FrappeTestCase): self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1})) self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1})) + @timeout def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: item_doc = create_item(item, valuation_rate=100) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ae9e9c6962..66b871c746 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -682,7 +682,7 @@ class WorkOrder(Document): for node in bom_traversal: if node.is_bom: - operations.extend(_get_operations(node.name, qty=node.exploded_qty)) + operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty)) bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 07ee2890c4..fcdf245659 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad return existing_address if out: - return min(out, key=lambda x: x[1])[0] # find min by sort_key + return max(out, key=lambda x: x[1])[0] # find max by sort_key else: return None diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index 29e056e34f..fd2fe300fa 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -11,6 +11,7 @@ from frappe.utils import random_string from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( get_charts_for_country, ) +from erpnext.setup.doctype.company.company import get_default_company_address test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"] test_dependencies = ["Fiscal Year"] @@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase): self.assertTrue(lft >= min_lft) self.assertTrue(rgt <= max_rgt) + def test_primary_address(self): + company = "_Test Company" + + secondary = frappe.get_doc( + { + "address_title": "Non Primary", + "doctype": "Address", + "address_type": "Billing", + "address_line1": "Something", + "city": "Mumbai", + "state": "Maharashtra", + "country": "India", + "is_primary_address": 1, + "pincode": "400098", + "links": [ + { + "link_doctype": "Company", + "link_name": company, + } + ], + } + ) + secondary.insert() + self.addCleanup(secondary.delete) + + primary = frappe.copy_doc(secondary) + primary.is_primary_address = 1 + primary.insert() + self.addCleanup(primary.delete) + + self.assertEqual(get_default_company_address(company), primary.name) + def get_no_of_children(self, company): def get_no_of_children(companies, no_of_children): children = [] diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c06700a99a..05a37ee4c4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -377,7 +377,9 @@ class Item(Document): "" if item_barcode.barcode_type not in options else item_barcode.barcode_type ) if item_barcode.barcode_type: - barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper()) + barcode_type = convert_erpnext_to_barcodenumber( + item_barcode.barcode_type.upper(), item_barcode.barcode + ) if barcode_type in barcodenumber.barcodes(): if not barcodenumber.check_code(barcode_type, item_barcode.barcode): frappe.throw( @@ -982,20 +984,29 @@ class Item(Document): ) -def convert_erpnext_to_barcodenumber(erpnext_number): +def convert_erpnext_to_barcodenumber(erpnext_number, barcode): + if erpnext_number == "EAN": + ean_type = { + 8: "EAN8", + 13: "EAN13", + } + barcode_length = len(barcode) + if barcode_length in ean_type: + return ean_type[barcode_length] + + return erpnext_number + convert = { "UPC-A": "UPCA", "CODE-39": "CODE39", - "EAN": "EAN13", - "EAN-12": "EAN", - "EAN-8": "EAN8", "ISBN-10": "ISBN10", "ISBN-13": "ISBN13", } + if erpnext_number in convert: return convert[erpnext_number] - else: - return erpnext_number + + return erpnext_number def make_item_price(item, price_list_name, item_price): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 67ed90d4e7..0c6dc77635 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -581,8 +581,9 @@ class TestItem(FrappeTestCase): }, {"barcode": "72527273070", "barcode_type": "UPC-A"}, {"barcode": "123456", "barcode_type": "CODE-39"}, - {"barcode": "401268452363", "barcode_type": "EAN-12"}, - {"barcode": "90311017", "barcode_type": "EAN-8"}, + {"barcode": "401268452363", "barcode_type": "EAN"}, + {"barcode": "90311017", "barcode_type": "EAN"}, + {"barcode": "73513537", "barcode_type": "EAN"}, {"barcode": "0123456789012", "barcode_type": "GS1"}, {"barcode": "2211564566668", "barcode_type": "GTIN"}, {"barcode": "0256480249", "barcode_type": "ISBN"},