diff --git a/erpnext/accounts/doctype/pricing_rule/test_records.json b/erpnext/accounts/doctype/pricing_rule/test_records.json deleted file mode 100644 index 706d54cf2e..0000000000 --- a/erpnext/accounts/doctype/pricing_rule/test_records.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "doctype": "Pricing Rule", - "name": "_Test Pricing Rule 1" - } -] diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8328fc7c89..7112678e6f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -197,27 +197,14 @@ class BOM(Document): if self.with_operations and cstr(m.operation_no) not in self.op: frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no)) - if m.bom: - self.validate_bom_no(m.item_code, m.bom_no, m.idx) + if m.bom_no: + validate_bom_no(m.item_code, m.bom_no) if flt(m.qty) <= 0: frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) self.check_if_item_repeated(m.item_code, m.operation_no, check_list) - def validate_bom_no(self, item, bom_no, idx): - """Validate BOM No of sub-contracted items""" - bom = frappe.get_doc("BOM", bom_no) - if not bom.is_active: - frappe.throw(_("BOM {0} must be active").format(bom_no)) - if not bom.docstatus!=1: - frappe.throw(_("BOM {0} must be submitted").format(bom_no)) - - bom = frappe.db.sql("""select name from `tabBOM` where name = %s and item = %s - and is_active=1 and docstatus=1""", - (bom_no, item), as_dict =1) - if not bom: - frappe.throw(_("BOM {0} for Item {1} in row {2} is inactive or not submitted").format(bom_no, item, idx)) def check_if_item_repeated(self, item, op, check_list): if [cstr(item), cstr(op)] in check_list: @@ -425,3 +412,16 @@ def get_bom_items(bom, qty=1, fetch_exploded=1): items = get_bom_items_as_dict(bom, qty, fetch_exploded).values() items.sort(lambda a, b: a.item_code > b.item_code and 1 or -1) return items + +def validate_bom_no(item, bom_no): + """Validate BOM No of sub-contracted items""" + bom = frappe.get_doc("BOM", bom_no) + if not bom.is_active: + frappe.throw(_("BOM {0} must be active").format(bom_no)) + if not bom.docstatus!=1: + if not getattr(frappe.flags, "in_test", False): + frappe.throw(_("BOM {0} must be submitted").format(bom_no)) + if item and not (bom.item == item or \ + bom.item == frappe.db.get_value("Item", item, "variant_of")): + frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) + diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 17c28d5d84..26aa6d3b16 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -2,98 +2,128 @@ { "bom_materials": [ { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Serialized Item With Series", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Serialized Item With Series", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { - "amount": 2000.0, - "doctype": "BOM Item", - "item_code": "_Test Item 2", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "doctype": "BOM Item", + "item_code": "_Test Item 2", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test Item Home Desktop Manufactured", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test Item Home Desktop Manufactured", "quantity": 1.0 - }, + }, { "bom_materials": [ { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { - "amount": 2000.0, - "doctype": "BOM Item", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "doctype": "BOM Item", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test FG Item", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test FG Item", "quantity": 1.0 }, { "bom_operations": [ { - "operation_no": "1", - "opn_description": "_Test", - "workstation": "_Test Workstation 1", + "operation_no": "1", + "opn_description": "_Test", + "workstation": "_Test Workstation 1", "time_in_min": 60, "operating_cost": 100 } - ], + ], "bom_materials": [ { "operation_no": 1, - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { "operation_no": 1, - "amount": 2000.0, - "bom_no": "BOM/_Test Item Home Desktop Manufactured/001", - "doctype": "BOM Item", - "item_code": "_Test Item Home Desktop Manufactured", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "bom_no": "BOM/_Test Item Home Desktop Manufactured/001", + "doctype": "BOM Item", + "item_code": "_Test Item Home Desktop Manufactured", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test FG Item 2", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test FG Item 2", + "quantity": 1.0, + "with_operations": 1 + }, + { + "bom_operations": [ + { + "operation_no": "1", + "opn_description": "_Test", + "workstation": "_Test Workstation 1", + "time_in_min": 60, + "operating_cost": 140 + } + ], + "bom_materials": [ + { + "operation_no": 1, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 3000.0, + "stock_uom": "_Test UOM" + } + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test Variant Item", "quantity": 1.0, "with_operations": 1 } -] \ No newline at end of file +] diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 309f47c388..f6af357c37 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -7,12 +7,12 @@ import frappe from frappe.utils import flt, nowdate from frappe import _ from frappe.model.document import Document +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class ProductionOrder(Document): - def validate(self): if self.docstatus == 0: self.status = "Draft" @@ -21,7 +21,9 @@ class ProductionOrder(Document): validate_status(self.status, ["Draft", "Submitted", "Stopped", "In Process", "Completed", "Cancelled"]) - self.validate_bom_no() + if self.bom_no: + validate_bom_no(self.production_item, self.bom_no) + self.validate_sales_order() self.validate_warehouse() self.set_fixed_cost() @@ -29,14 +31,6 @@ class ProductionOrder(Document): from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) - def validate_bom_no(self): - if self.bom_no: - bom = frappe.db.sql("""select name from `tabBOM` where name=%s and docstatus=1 - and is_active=1 and item=%s""" - , (self.bom_no, self.production_item), as_dict =1) - if not bom: - frappe.throw(_("BOM {0} is not active or not submitted").format(self.bom_no)) - def validate_sales_order(self): if self.sales_order: so = frappe.db.sql("""select name, delivery_date from `tabSales Order` diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 4085d988e9..01c4e4f643 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -264,6 +264,7 @@ "is_sales_item": "Yes", "is_service_item": "No", "is_stock_item": "Yes", + "is_manufactured_item": "Yes", "is_sub_contracted_item": "Yes", "item_code": "_Test Variant Item", "item_group": "_Test Item Group Desktops", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2d8c269bd..028fff9fc5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -12,6 +12,7 @@ from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle from erpnext.controllers.queries import get_match_cond from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class NotUpdateStockError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass @@ -293,10 +294,8 @@ class StockEntry(StockController): def validate_bom(self): for d in self.get('mtn_details'): - if d.bom_no and not frappe.db.sql("""select name from `tabBOM` - where item = %s and name = %s and docstatus = 1 and is_active = 1""", - (d.item_code, d.bom_no)): - frappe.throw(_("BOM {0} is not submitted or inactive BOM for Item {1}").format(d.bom_no, d.item_code)) + if d.bom_no: + validate_bom_no(d.item_code, d.bom_no) def validate_finished_goods(self): """validation: finished good quantity should be same as manufacturing quantity""" @@ -497,13 +496,20 @@ class StockEntry(StockController): # add raw materials to Stock Entry Detail table self.add_to_stock_entry_detail(item_dict) - # add finished good item to Stock Entry Detail table -- along with bom_no - if self.production_order and self.purpose == "Manufacture": - item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name", - "description", "stock_uom", "expense_account", "buying_cost_center"], as_dict=1) + if self.bom_no: + if self.production_order: + item_code = pro_obj.production_item + to_warehouse = pro_obj.fg_warehouse + else: + item_code = frappe.db.get_value("BOM", self.bom_no, "item") + to_warehouse = "" + + item = frappe.db.get_value("Item", item_code, ["item_name", + "description", "stock_uom", "expense_account", "buying_cost_center", "name"], as_dict=1) + self.add_to_stock_entry_detail({ - cstr(pro_obj.production_item): { - "to_warehouse": pro_obj.fg_warehouse, + item.name: { + "to_warehouse": to_warehouse, "from_warehouse": "", "qty": self.fg_completed_qty, "item_name": item.item_name, @@ -512,27 +518,7 @@ class StockEntry(StockController): "expense_account": item.expense_account, "cost_center": item.buying_cost_center, } - }, bom_no=pro_obj.bom_no) - - elif self.purpose in ["Material Receipt", "Repack"]: - if self.purpose=="Material Receipt": - self.from_warehouse = "" - - item = frappe.db.sql("""select name, item_name, description, - stock_uom, expense_account, buying_cost_center from `tabItem` - where name=(select item from tabBOM where name=%s)""", - self.bom_no, as_dict=1) - self.add_to_stock_entry_detail({ - item[0]["name"] : { - "qty": self.fg_completed_qty, - "item_name": item[0].item_name, - "description": item[0]["description"], - "stock_uom": item[0]["stock_uom"], - "from_warehouse": "", - "expense_account": item[0].expense_account, - "cost_center": item[0].buying_cost_center, - } - }, bom_no=self.bom_no) + }, bom_no = self.bom_no) self.get_stock_and_rate() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c2dcdc10ca..03eb9fe253 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -858,6 +858,29 @@ class TestStockEntry(unittest.TestCase): fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0] self.assertEqual(fg_rate, 100.00) + def test_variant_production_order(self): + bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", + "is_default": 1, "docstatus": 1}) + + production_order = frappe.new_doc("Production Order") + production_order.update({ + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test Variant Item-S", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "Nos", + "wip_warehouse": "_Test Warehouse - _TC" + }) + production_order.insert() + production_order.submit() + + from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry + + stock_entry = frappe.get_doc(make_stock_entry(production_order.name, "Manufacture", 1)) + stock_entry.insert() + self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.mtn_details]) + def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) se.get("mtn_details")[0].item_code = item_code or "_Test Serialized Item With Series"