[enhancement] make service type product bundle:
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json
index 3f4e296..a1be948 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 8c95a45..fdf6b76 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -13,17 +13,9 @@
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 @@
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 8c5fe12..39b17f3 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 @@
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 b92e934..109034d 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -39,9 +39,8 @@
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 59e58b0..d4d5f92 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 @@
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 @@
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 @@
dn.cancel()
self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10)
-
+
# cancel
so.load_from_db()
so.cancel()
@@ -108,9 +108,9 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 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 f299c4a..0fb01ac 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -12,6 +12,23 @@
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")