From 862a2eb975517c4fc78e8298a04076d72df6cec1 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 3 Aug 2015 11:58:23 +0530 Subject: [PATCH] [enhancement] make service type product bundle: --- .../product_bundle/product_bundle.json | 92 +++++++++++++++++-- .../doctype/product_bundle/product_bundle.py | 13 +-- .../product_bundle/test_product_bundle.py | 19 +++- .../doctype/sales_order/sales_order.py | 5 +- .../doctype/sales_order/test_sales_order.py | 86 ++++++++++------- erpnext/stock/doctype/item/test_item.py | 17 ++++ 6 files changed, 174 insertions(+), 58 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index 3f4e2965d6..a1be948691 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -1,21 +1,41 @@ { + "allow_copy": 0, "allow_import": 1, + "allow_rename": 0, "creation": "2013-06-20 11:53:21", + "custom": 0, "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", "docstatus": 0, "doctype": "DocType", - "document_type": "Master", + "document_type": "", "fields": [ { + "allow_on_submit": 0, "fieldname": "basic_section", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { - "description": "The Item that represents the Package. This Item must have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\"", + "allow_on_submit": 0, + "description": "", "fieldname": "new_item_code", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Parent Item", "no_copy": 1, @@ -23,30 +43,67 @@ "oldfieldtype": "Data", "options": "Item", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, "description": "List items that form the package.", "fieldname": "item_section", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, "fieldname": "items", "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Items", + "no_copy": 0, "oldfieldname": "sales_bom_items", "oldfieldtype": "Table", "options": "Product Bundle Item", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-sitemap", "idx": 1, + "in_create": 0, + "in_dialog": 0, "is_submittable": 0, - "modified": "2015-07-13 05:28:28.140327", + "issingle": 0, + "istable": 0, + "modified": "2015-08-03 11:23:26.263254", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", @@ -54,14 +111,20 @@ "permissions": [ { "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 @@ -69,31 +132,44 @@ { "amend": 0, "apply_user_permissions": 1, + "cancel": 0, "create": 0, "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock User", + "set_user_permissions": 0, + "share": 0, "submit": 0, "write": 0 }, { "amend": 0, "apply_user_permissions": 1, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Sales User", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 8c95a45bf3..fdf6b76db0 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -13,17 +13,9 @@ class ProductBundle(Document): self.name = self.new_item_code def validate(self): - self.validate_main_item() - from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") - def validate_main_item(self): - """main item must have Is Stock Item as No and Is Sales Item as Yes""" - if not frappe.db.sql("""select name from tabItem where name=%s and - is_stock_item = 0 and is_sales_item = 1""", self.new_item_code): - frappe.throw(_("Parent Item {0} must be not Stock Item and must be a Sales Item").format(self.new_item_code)) - def get_item_details(self, name): det = frappe.db.sql("""select description, stock_uom from `tabItem` where name = %s""", name) @@ -36,8 +28,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql("""select name, item_name, description from tabItem - where is_stock_item=0 and is_sales_item=1 - and name not in (select name from `tabProduct Bundle`) and %s like %s - %s limit %s, %s""" % (searchfield, "%s", + where name not in (select name from `tabProduct Bundle`) + and %s like %s %s limit %s, %s""" % (searchfield, "%s", get_match_cond(doctype),"%s", "%s"), ("%%%s%%" % txt, start, page_len)) diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index 8c5fe12e1b..39b17f368d 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -5,4 +5,21 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Product Bundle') \ No newline at end of file +test_records = frappe.get_test_records('Product Bundle') + +def make_product_bundle(parent, items): + if frappe.db.exists("Product Bundle", parent): + return frappe.get_doc("Product Bundle", parent) + + product_bundle = frappe.get_doc({ + "doctype": "Product Bundle", + "parent_item": parent, + "new_item_code": parent + }) + + for item in items: + product_bundle.append("items", {"item_code": item, "qty": 1}) + + product_bundle.insert() + + return product_bundle diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b92e934fa3..109034d14c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -39,9 +39,8 @@ class SalesOrder(SellingController): for d in self.get('items'): check_list.append(cstr(d.item_code)) - if (frappe.db.get_value("Item", d.item_code, "is_stock_item")==1 or - self.has_product_bundle(d.item_code)) and not d.warehouse: - frappe.throw(_("Reserved warehouse required for stock item {0}").format(d.item_code)) + if frappe.db.get_value("Item", d.item_code, "is_stock_item") and not d.warehouse: + frappe.throw(_("Delivery warehouse required for stock item {0}").format(d.item_code)) # used for production plan d.transaction_date = self.transaction_date diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 59e58b0c78..d4d5f92b59 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -80,7 +80,7 @@ class TestSalesOrder(unittest.TestCase): def test_reserved_qty_for_partial_delivery(self): existing_reserved_qty = get_reserved_qty() - + so = make_sales_order() self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) @@ -91,7 +91,7 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() so.stop_sales_order() self.assertEqual(get_reserved_qty(), existing_reserved_qty) - + # unstop so so.load_from_db() so.unstop_sales_order() @@ -99,7 +99,7 @@ class TestSalesOrder(unittest.TestCase): dn.cancel() self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) - + # cancel so.load_from_db() so.cancel() @@ -108,9 +108,9 @@ class TestSalesOrder(unittest.TestCase): def test_reserved_qty_for_over_delivery(self): # set over-delivery tolerance frappe.db.set_value('Item', "_Test Item", 'tolerance', 50) - + existing_reserved_qty = get_reserved_qty() - + so = make_sales_order() self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) @@ -124,39 +124,39 @@ class TestSalesOrder(unittest.TestCase): def test_reserved_qty_for_partial_delivery_with_packing_list(self): existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") - + so = make_sales_order(item_code="_Test Product Bundle Item") - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) - + dn = create_dn_against_so(so.name) - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10) # stop so so.load_from_db() so.stop_sales_order() - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2) # unstop so so.load_from_db() so.unstop_sales_order() - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) - + so.load_from_db() so.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) @@ -165,25 +165,25 @@ class TestSalesOrder(unittest.TestCase): def test_reserved_qty_for_over_delivery_with_packing_list(self): # set over-delivery tolerance frappe.db.set_value('Item', "_Test Product Bundle Item", 'tolerance', 50) - + existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") - + so = make_sales_order(item_code="_Test Product Bundle Item") - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) - + dn = create_dn_against_so(so.name, 15) - + self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2) - + dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) def test_warehouse_user(self): @@ -201,7 +201,7 @@ class TestSalesOrder(unittest.TestCase): frappe.set_user("test@example.com") - so = make_sales_order(company="_Test Company 1", + so = make_sales_order(company="_Test Company 1", warehouse="_Test Warehouse 2 - _TC1", do_not_save=True) so.conversion_rate = 0.02 so.plc_conversion_rate = 0.02 @@ -216,14 +216,30 @@ class TestSalesOrder(unittest.TestCase): def test_block_delivery_note_against_cancelled_sales_order(self): so = make_sales_order() - + dn = make_delivery_note(so.name) dn.insert() - + so.cancel() - + self.assertRaises(frappe.CancelledLinkError, dn.submit) - + + def test_service_type_product_bundle(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + make_item("_Test Service Product Bundle", {"is_stock_item": 0, "is_sales_item": 1}) + make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0, "is_sales_item": 1}) + make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0, "is_sales_item": 1}) + + make_product_bundle("_Test Service Product Bundle", + ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"]) + + so = make_sales_order(item_code = "_Test Service Product Bundle") + + self.assertTrue("_Test Service Product Bundle Item 1" in [d.item_code for d in so.packed_items]) + self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) @@ -246,12 +262,12 @@ def make_sales_order(**args): so.insert() if not args.do_not_submit: so.submit() - + return so - + def create_dn_against_so(so, delivered_qty=0): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - + dn = make_delivery_note(so) dn.get("items")[0].qty = delivered_qty or 5 dn.insert() @@ -261,5 +277,5 @@ def create_dn_against_so(so, delivered_qty=0): def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty")) - -test_dependencies = ["Currency Exchange"] \ No newline at end of file + +test_dependencies = ["Currency Exchange"] diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index f299c4a75c..0fb01ac117 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -12,6 +12,23 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry test_ignore = ["BOM"] test_dependencies = ["Warehouse"] +def make_item(item_code, properties=None): + if frappe.db.exists("Item", item_code): + return frappe.get_doc("Item", item_code) + + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products" + }) + + if properties: + item.update(properties) + item.insert() + return item + class TestItem(unittest.TestCase): def get_item(self, idx): item_code = test_records[idx].get("item_code")