Merge branch 'master' into edge
diff --git a/hr/doctype/leave_application/leave_application_calendar.js b/hr/doctype/leave_application/leave_application_calendar.js
index eebd559..398664e 100644
--- a/hr/doctype/leave_application/leave_application_calendar.js
+++ b/hr/doctype/leave_application/leave_application_calendar.js
@@ -6,5 +6,12 @@
"title": "title",
"status": "status",
},
+ options: {
+ header: {
+ left: 'prev,next today',
+ center: 'title',
+ right: 'month'
+ }
+ },
get_events_method: "hr.doctype.leave_application.leave_application.get_events"
})
\ No newline at end of file
diff --git a/selling/doctype/lead/get_leads.py b/selling/doctype/lead/get_leads.py
index 5b127e7..b0e6484 100644
--- a/selling/doctype/lead/get_leads.py
+++ b/selling/doctype/lead/get_leads.py
@@ -64,7 +64,7 @@
if mail.from_email == self.settings.email_id:
return
- add_sales_communication(mail.subject, mail.content, mail.form_email,
+ add_sales_communication(mail.mail.get("subject", "[No Subject]"), mail.content, mail.from_email,
mail.from_real_name, mail=mail, date=mail.date)
def get_leads():
diff --git a/selling/doctype/sales_common/sales_common.py b/selling/doctype/sales_common/sales_common.py
index 797462a..47d139f 100644
--- a/selling/doctype/sales_common/sales_common.py
+++ b/selling/doctype/sales_common/sales_common.py
@@ -384,36 +384,38 @@
def get_item_list(self, obj, is_stopped=0):
"""get item list"""
il = []
- for d in getlist(obj.doclist,obj.fname):
- reserved_wh, reserved_qty = '', 0 # used for delivery note
- qty = flt(d.qty)
- if is_stopped:
- qty = flt(d.qty) > flt(d.delivered_qty) and flt(flt(d.qty) - flt(d.delivered_qty)) or 0
+ for d in getlist(obj.doclist, obj.fname):
+ reserved_warehouse = ""
+ reserved_qty_for_main_item = 0
+
+ if obj.doc.doctype == "Sales Order":
+ reserved_warehouse = d.reserved_warehouse
+ if flt(d.qty) > flt(d.delivered_qty):
+ reserved_qty_for_main_item = flt(d.qty) - flt(d.delivered_qty)
- if d.prevdoc_doctype == 'Sales Order':
- # used in delivery note to reduce reserved_qty
- # Eg.: if SO qty is 10 and there is tolerance of 20%, then it will allow DN of 12.
- # But in this case reserved qty should only be reduced by 10 and not 12.
+ if obj.doc.doctype == "Delivery Note" and d.prevdoc_doctype == 'Sales Order':
+ # if SO qty is 10 and there is tolerance of 20%, then it will allow DN of 12.
+ # But in this case reserved qty should only be reduced by 10 and not 12
+
+ already_delivered_qty = self.get_already_delivered_qty(obj.doc.name,
+ d.prevdoc_docname, d.prevdoc_detail_docname)
+ so_qty, reserved_warehouse = self.get_so_qty_and_warehouse(d.prevdoc_detail_docname)
+
+ if already_delivered_qty + d.qty > so_qty:
+ reserved_qty_for_main_item = -(so_qty - already_delivered_qty)
+ else:
+ reserved_qty_for_main_item = -flt(d.qty)
- tot_qty, max_qty, tot_amt, max_amt, reserved_wh = self.get_curr_and_ref_doc_details(d.doctype, 'prevdoc_detail_docname', d.prevdoc_detail_docname, obj.doc.name, obj.doc.doctype)
- if((flt(tot_qty) + flt(qty) > flt(max_qty))):
- reserved_qty = -(flt(max_qty)-flt(tot_qty))
- else:
- reserved_qty = - flt(qty)
-
- if obj.doc.doctype == 'Sales Order':
- reserved_wh = d.reserved_warehouse
-
if self.has_sales_bom(d.item_code):
for p in getlist(obj.doclist, 'packing_details'):
if p.parent_detail_docname == d.name and p.parent_item == d.item_code:
# the packing details table's qty is already multiplied with parent's qty
il.append({
'warehouse': p.warehouse,
- 'reserved_warehouse': reserved_wh,
+ 'reserved_warehouse': reserved_warehouse,
'item_code': p.item_code,
'qty': flt(p.qty),
- 'reserved_qty': (flt(p.qty)/qty)*(reserved_qty),
+ 'reserved_qty': (flt(p.qty)/flt(d.qty)) * reserved_qty_for_main_item,
'uom': p.uom,
'batch_no': cstr(p.batch_no).strip(),
'serial_no': cstr(p.serial_no).strip(),
@@ -422,10 +424,10 @@
else:
il.append({
'warehouse': d.warehouse,
- 'reserved_warehouse': reserved_wh,
+ 'reserved_warehouse': reserved_warehouse,
'item_code': d.item_code,
- 'qty': qty,
- 'reserved_qty': reserved_qty,
+ 'qty': d.qty,
+ 'reserved_qty': reserved_qty_for_main_item,
'uom': d.stock_uom,
'batch_no': cstr(d.batch_no).strip(),
'serial_no': cstr(d.serial_no).strip(),
@@ -433,27 +435,20 @@
})
return il
+ def get_already_delivered_qty(self, dn, so, so_detail):
+ qty = webnotes.conn.sql("""select sum(qty) from `tabDelivery Note Item`
+ where prevdoc_detail_docname = %s and docstatus = 1
+ and prevdoc_doctype = 'Sales Order' and prevdoc_docname = %s
+ and parent != %s""", (so_detail, so, dn))
+ return qty and flt(qty[0][0]) or 0.0
- def get_curr_and_ref_doc_details(self, curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name, curr_parent_doctype):
- """ Get qty, amount already billed or delivered against curr line item for current doctype
- For Eg: SO-RV get total qty, amount from SO and also total qty, amount against that SO in RV
- """
- #Get total qty, amt of current doctype (eg RV) except for qty, amt of this transaction
- if curr_parent_doctype == 'Installation Note':
- curr_det = webnotes.conn.sql("select sum(qty) from `tab%s` where %s = '%s' and docstatus = 1 and parent != '%s'"% (curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name))
- qty, amt = curr_det and flt(curr_det[0][0]) or 0, 0
- else:
- curr_det = webnotes.conn.sql("select sum(qty), sum(amount) from `tab%s` where %s = '%s' and docstatus = 1 and parent != '%s'"% (curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name))
- qty, amt = curr_det and flt(curr_det[0][0]) or 0, curr_det and flt(curr_det[0][1]) or 0
+ def get_so_qty_and_warehouse(self, so_detail):
+ so_item = webnotes.conn.sql("""select qty, reserved_warehouse from `tabSales Order Item`
+ where name = %s and docstatus = 1""", so_detail, as_dict=1)
+ so_qty = so_item and flt(so_item[0]["qty"]) or 0.0
+ so_warehouse = so_item and so_item[0]["reserved_warehouse"] or ""
+ return so_qty, so_warehouse
- # get total qty of ref doctype
- so_det = webnotes.conn.sql("select qty, amount, reserved_warehouse from `tabSales Order Item` where name = '%s' and docstatus = 1"% ref_tab_dn)
- max_qty, max_amt, res_wh = so_det and flt(so_det[0][0]) or 0, so_det and flt(so_det[0][1]) or 0, so_det and cstr(so_det[0][2]) or ''
- return qty, max_qty, amt, max_amt, res_wh
-
-
- # Make Packing List from Sales BOM
- # =======================================================================
def has_sales_bom(self, item_code):
return webnotes.conn.sql("select name from `tabSales BOM` where new_item_code=%s and docstatus != 2", item_code)
diff --git a/selling/doctype/sales_order/sales_order.py b/selling/doctype/sales_order/sales_order.py
index cebd7e0..8205afb 100644
--- a/selling/doctype/sales_order/sales_order.py
+++ b/selling/doctype/sales_order/sales_order.py
@@ -318,28 +318,28 @@
def stop_sales_order(self):
self.check_modified_date()
- self.update_stock_ledger(update_stock = -1,clear = 1)
+ self.update_stock_ledger(update_stock = -1,is_stopped = 1)
webnotes.conn.set(self.doc, 'status', 'Stopped')
msgprint("""%s: %s has been Stopped. To make transactions against this Sales Order
you need to Unstop it.""" % (self.doc.doctype, self.doc.name))
def unstop_sales_order(self):
self.check_modified_date()
- self.update_stock_ledger(update_stock = 1,clear = 1)
+ self.update_stock_ledger(update_stock = 1,is_stopped = 1)
webnotes.conn.set(self.doc, 'status', 'Submitted')
msgprint("%s: %s has been Unstopped" % (self.doc.doctype, self.doc.name))
- def update_stock_ledger(self, update_stock, clear = 0):
- for d in self.get_item_list(clear):
+ def update_stock_ledger(self, update_stock, is_stopped = 0):
+ for d in self.get_item_list(is_stopped):
if webnotes.conn.get_value("Item", d['item_code'], "is_stock_item") == "Yes":
if not d['reserved_warehouse']:
msgprint("""Please enter Reserved Warehouse for item %s
as it is stock Item""" % d['item_code'], raise_exception=1)
-
+
args = {
"item_code": d['item_code'],
- "reserved_qty": flt(update_stock) * flt(d['qty']),
+ "reserved_qty": flt(update_stock) * flt(d['reserved_qty']),
"posting_date": self.doc.transaction_date,
"voucher_type": self.doc.doctype,
"voucher_no": self.doc.name,
@@ -348,8 +348,8 @@
get_obj('Warehouse', d['reserved_warehouse']).update_bin(args)
- def get_item_list(self, clear):
- return get_obj('Sales Common').get_item_list( self, clear)
+ def get_item_list(self, is_stopped):
+ return get_obj('Sales Common').get_item_list( self, is_stopped)
def on_update(self):
pass
\ No newline at end of file
diff --git a/selling/doctype/sales_order/test_sales_order.py b/selling/doctype/sales_order/test_sales_order.py
new file mode 100644
index 0000000..5d820fe
--- /dev/null
+++ b/selling/doctype/sales_order/test_sales_order.py
@@ -0,0 +1,253 @@
+import webnotes
+from webnotes.utils import flt
+import unittest
+
+class TestSalesOrder(unittest.TestCase):
+ def create_so(self, so_doclist = None):
+ if not so_doclist:
+ so_doclist =test_records[0]
+
+ w = webnotes.bean(copy=so_doclist)
+ w.insert()
+ w.submit()
+ return w
+
+ def create_dn_against_so(self, so, delivered_qty=0):
+ from stock.doctype.delivery_note.test_delivery_note import test_records as dn_test_records
+ dn = webnotes.bean(webnotes.copy_doclist(dn_test_records[0]))
+ dn.doclist[1].item_code = so.doclist[1].item_code
+ dn.doclist[1].prevdoc_doctype = "Sales Order"
+ dn.doclist[1].prevdoc_docname = so.doc.name
+ dn.doclist[1].prevdoc_detail_docname = so.doclist[1].name
+ if delivered_qty:
+ dn.doclist[1].qty = delivered_qty
+ dn.insert()
+ dn.submit()
+ return dn
+
+ def get_bin_reserved_qty(self, item_code, warehouse):
+ return flt(webnotes.conn.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
+ "reserved_qty"))
+
+ def delete_bin(self, item_code, warehouse):
+ bin = webnotes.conn.exists({"doctype": "Bin", "item_code": item_code,
+ "warehouse": warehouse})
+ if bin:
+ webnotes.delete_doc("Bin", bin[0][0])
+
+ def check_reserved_qty(self, item_code, warehouse, qty):
+ bin_reserved_qty = self.get_bin_reserved_qty(item_code, warehouse)
+ self.assertEqual(bin_reserved_qty, qty)
+
+ def test_reserved_qty_for_so(self):
+ # reset bin
+ self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"])
+
+ # submit
+ so = self.create_so()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0)
+
+ # cancel
+ so.cancel()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0)
+
+
+ def test_reserved_qty_for_partial_delivery(self):
+ # reset bin
+ self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"])
+
+ # submit so
+ so = self.create_so()
+
+ # allow negative stock
+ webnotes.conn.set_default("allow_negative_stock", 1)
+
+ # submit dn
+ dn = self.create_dn_against_so(so)
+
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 6.0)
+
+ # stop so
+ so.load_from_db()
+ so.obj.stop_sales_order()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0)
+
+ # unstop so
+ so.load_from_db()
+ so.obj.unstop_sales_order()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 6.0)
+
+ # cancel dn
+ dn.cancel()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0)
+
+ def test_reserved_qty_for_over_delivery(self):
+ # reset bin
+ self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"])
+
+ # submit so
+ so = self.create_so()
+
+ # allow negative stock
+ webnotes.conn.set_default("allow_negative_stock", 1)
+
+ # set over-delivery tolerance
+ webnotes.conn.set_value('Item', so.doclist[1].item_code, 'tolerance', 50)
+
+ # submit dn
+ dn = self.create_dn_against_so(so, 15)
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0)
+
+ # cancel dn
+ dn.cancel()
+ self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0)
+
+ def test_reserved_qty_for_so_with_packing_list(self):
+ from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records
+
+ # change item in test so record
+ test_record = test_records[0][:]
+ test_record[1]["item_code"] = "_Test Sales BOM Item"
+
+ # reset bin
+ self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"])
+ self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"])
+
+ # submit
+ so = self.create_so(test_record)
+
+
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 50.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 20.0)
+
+ # cancel
+ so.cancel()
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+
+ def test_reserved_qty_for_partial_delivery_with_packing_list(self):
+ from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records
+
+ # change item in test so record
+
+ test_record = webnotes.copy_doclist(test_records[0])
+ test_record[1]["item_code"] = "_Test Sales BOM Item"
+
+ # reset bin
+ self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"])
+ self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"])
+
+ # submit
+ so = self.create_so(test_record)
+
+ # allow negative stock
+ webnotes.conn.set_default("allow_negative_stock", 1)
+
+ # submit dn
+ dn = self.create_dn_against_so(so)
+
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 30.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 12.0)
+
+ # stop so
+ so.load_from_db()
+ so.obj.stop_sales_order()
+
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+
+ # unstop so
+ so.load_from_db()
+ so.obj.unstop_sales_order()
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 30.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 12.0)
+
+ # cancel dn
+ dn.cancel()
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 50.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 20.0)
+
+ def test_reserved_qty_for_over_delivery_with_packing_list(self):
+ from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records
+
+ # change item in test so record
+ test_record = webnotes.copy_doclist(test_records[0])
+ test_record[1]["item_code"] = "_Test Sales BOM Item"
+
+ # reset bin
+ self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"])
+ self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"])
+
+ # submit
+ so = self.create_so(test_record)
+
+ # allow negative stock
+ webnotes.conn.set_default("allow_negative_stock", 1)
+
+ # set over-delivery tolerance
+ webnotes.conn.set_value('Item', so.doclist[1].item_code, 'tolerance', 50)
+
+ # submit dn
+ dn = self.create_dn_against_so(so, 15)
+
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 0.0)
+
+ # cancel dn
+ dn.cancel()
+ self.check_reserved_qty(sbom_test_records[0][1]["item_code"],
+ so.doclist[1].reserved_warehouse, 50.0)
+ self.check_reserved_qty(sbom_test_records[0][2]["item_code"],
+ so.doclist[1].reserved_warehouse, 20.0)
+
+test_dependencies = ["Sales BOM"]
+
+test_records = [
+ [
+ {
+ "company": "_Test Company",
+ "conversion_rate": 1.0,
+ "currency": "INR",
+ "customer": "_Test Customer",
+ "customer_name": "_Test Customer",
+ "customer_group": "_Test Customer Group",
+ "doctype": "Sales Order",
+ "fiscal_year": "_Test Fiscal Year 2013",
+ "order_type": "Sales",
+ "delivery_date": "2013-02-23",
+ "plc_conversion_rate": 1.0,
+ "price_list_currency": "INR",
+ "price_list_name": "_Test Price List",
+ "territory": "_Test Territory",
+ "transaction_date": "2013-02-21",
+ "grand_total": 500.0,
+ "grand_total_export": 500.0,
+ },
+ {
+ "description": "CPU",
+ "doctype": "Sales Order Item",
+ "item_code": "_Test Item Home Desktop 100",
+ "item_name": "CPU",
+ "parentfield": "sales_order_details",
+ "qty": 10.0,
+ "basic_rate": 50.0,
+ "export_rate": 50.0,
+ "amount": 500.0,
+ "reserved_warehouse": "_Test Warehouse",
+ }
+ ],
+]
\ No newline at end of file
diff --git a/stock/doctype/delivery_note/delivery_note.py b/stock/doctype/delivery_note/delivery_note.py
index b8d20fb..f54edf2 100644
--- a/stock/doctype/delivery_note/delivery_note.py
+++ b/stock/doctype/delivery_note/delivery_note.py
@@ -319,9 +319,9 @@
webnotes.msgprint("%s Packing Slip(s) Cancelled" % res[0][1])
- def update_stock_ledger(self, update_stock, is_stopped = 0):
+ def update_stock_ledger(self, update_stock):
self.values = []
- for d in self.get_item_list(is_stopped):
+ for d in self.get_item_list():
if webnotes.conn.get_value("Item", d['item_code'], "is_stock_item") == "Yes":
if not d['warehouse']:
msgprint("Please enter Warehouse for item %s as it is stock item"
@@ -344,8 +344,8 @@
get_obj('Stock Ledger', 'Stock Ledger').update_stock(self.values)
- def get_item_list(self, is_stopped):
- return get_obj('Sales Common').get_item_list(self, is_stopped)
+ def get_item_list(self):
+ return get_obj('Sales Common').get_item_list(self)
def make_sl_entry(self, d, wh, qty, in_value, update_stock):
diff --git a/stock/doctype/delivery_note/test_delivery_note.py b/stock/doctype/delivery_note/test_delivery_note.py
new file mode 100644
index 0000000..4666360
--- /dev/null
+++ b/stock/doctype/delivery_note/test_delivery_note.py
@@ -0,0 +1,36 @@
+test_records = [
+ [
+ {
+ "company": "_Test Company",
+ "conversion_rate": 1.0,
+ "currency": "INR",
+ "customer": "_Test Customer",
+ "customer_name": "_Test Customer",
+ "doctype": "Delivery Note",
+ "fiscal_year": "_Test Fiscal Year 2013",
+ "plc_conversion_rate": 1.0,
+ "posting_date": "2013-02-21",
+ "posting_time": "9:00:00",
+ "price_list_currency": "INR",
+ "price_list_name": "_Test Price List",
+ "status": "Draft",
+ "territory": "_Test Territory",
+ "grand_total": 500.0,
+ "grand_total_export": 500.0,
+ },
+ {
+ "description": "CPU",
+ "doctype": "Delivery Note Item",
+ "item_code": "_Test Item Home Desktop 100",
+ "item_name": "CPU",
+ "parentfield": "delivery_note_details",
+ "qty": 4.0,
+ "basic_rate": 50.0,
+ "export_rate": 50.0,
+ "amount": 500.0,
+ "warehouse": "_Test Warehouse",
+ "stock_uom": "No."
+ }
+ ]
+
+]
\ No newline at end of file
diff --git a/stock/doctype/item/test_item.py b/stock/doctype/item/test_item.py
index 4238e14..853283e 100644
--- a/stock/doctype/item/test_item.py
+++ b/stock/doctype/item/test_item.py
@@ -145,4 +145,23 @@
"is_sub_contracted_item": "No",
"stock_uom": "_Test UOM"
}],
+ [{
+ "doctype": "Item",
+ "item_code": "_Test Sales BOM Item",
+ "item_name": "_Test Sales BOM Item",
+ "description": "_Test Sales BOM Item",
+ "item_group": "_Test Item Group Desktops",
+ "is_stock_item": "No",
+ "is_asset_item": "No",
+ "has_batch_no": "No",
+ "has_serial_no": "No",
+ "is_purchase_item": "Yes",
+ "is_sales_item": "Yes",
+ "is_service_item": "No",
+ "is_sample_item": "No",
+ "inspection_required": "No",
+ "is_pro_applicable": "No",
+ "is_sub_contracted_item": "No",
+ "stock_uom": "_Test UOM"
+ }],
]
\ No newline at end of file
diff --git a/stock/doctype/sales_bom/test_sales_bom.py b/stock/doctype/sales_bom/test_sales_bom.py
new file mode 100644
index 0000000..850616f
--- /dev/null
+++ b/stock/doctype/sales_bom/test_sales_bom.py
@@ -0,0 +1,20 @@
+test_records = [
+ [
+ {
+ "doctype": "Sales BOM",
+ "new_item_code": "_Test Sales BOM Item"
+ },
+ {
+ "doctype": "Sales BOM Item",
+ "item_code": "_Test Item",
+ "parentfield": "sales_bom_items",
+ "qty": 5.0
+ },
+ {
+ "doctype": "Sales BOM Item",
+ "item_code": "_Test Item Home Desktop 100",
+ "parentfield": "sales_bom_items",
+ "qty": 2.0
+ }
+ ],
+]
\ No newline at end of file