Merge pull request #3739 from nabinhait/packed-items

Packed items in sales invoice
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 12e199f..f581faa 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -4,7 +4,7 @@
 from __future__ import unicode_literals
 import frappe
 import frappe.defaults
-from frappe.utils import cint, cstr, flt
+from frappe.utils import cint, flt
 from frappe import _, msgprint, throw
 from erpnext.accounts.party import get_party_account, get_due_date
 from erpnext.controllers.stock_controller import update_gl_entries_after
@@ -66,6 +66,7 @@
 		self.validate_c_form()
 		self.validate_time_logs_are_submitted()
 		self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
+		self.update_packing_list()
 
 	def on_submit(self):
 		super(SalesInvoice, self).on_submit()
@@ -363,6 +364,13 @@
 			d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0
 			d.projected_qty = bin and flt(bin[0]['projected_qty']) or 0
 
+	def update_packing_list(self):
+		if cint(self.update_stock) == 1:
+			from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+			make_packing_list(self, 'items')
+		else:
+			self.set('packed_items', [])
+
 
 	def get_warehouse(self):
 		user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`
@@ -381,20 +389,6 @@
 		return warehouse
 
 	def on_update(self):
-		if cint(self.update_stock) == 1:
-			# Set default warehouse from POS Profile
-			if cint(self.is_pos) == 1:
-				w = self.get_warehouse()
-				if w:
-					for d in self.get('items'):
-						if not d.warehouse:
-							d.warehouse = cstr(w)
-
-			from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
-			make_packing_list(self, 'items')
-		else:
-			self.set('packed_items', [])
-
 		if cint(self.is_pos) == 1:
 			if flt(self.paid_amount) == 0:
 				if self.cash_bank_account:
@@ -424,8 +418,9 @@
 	def update_stock_ledger(self):
 		sl_entries = []
 		for d in self.get_item_list():
-			if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 \
-				and d.warehouse:
+			if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and d.warehouse and flt(d['qty']):
+				self.update_reserved_qty(d)
+				
 				incoming_rate = 0
 				if cint(self.is_return) and self.return_against and self.docstatus==1:
 					incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code,
diff --git a/erpnext/change_log/current/packing_list.md b/erpnext/change_log/current/packing_list.md
new file mode 100644
index 0000000..d4793be
--- /dev/null
+++ b/erpnext/change_log/current/packing_list.md
@@ -0,0 +1,2 @@
+- Fixed logic of reserved qty calculation while delivered product bundle via Sales Invoice
+- Delete stock ledger entries on cancellation of Sales Invoice while product bundle delivered
\ No newline at end of file
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9a0aedf..a1468e1 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -174,12 +174,14 @@
 				if flt(d.qty) > flt(d.delivered_qty):
 					reserved_qty_for_main_item = flt(d.qty) - flt(d.delivered_qty)
 
-			elif self.doctype == "Delivery Note" and d.against_sales_order and not self.is_return:
+			elif (((self.doctype == "Delivery Note" and d.against_sales_order) 
+					or (self.doctype == "Sales Invoice" and d.sales_order and self.update_stock)) 
+					and not self.is_return):
 				# 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(self.name,
-					d.against_sales_order, d.so_detail)
+					d.against_sales_order if self.doctype=="Delivery Note" else d.sales_order, d.so_detail)
 				so_qty, reserved_warehouse = self.get_so_qty_and_warehouse(d.so_detail)
 
 				if already_delivered_qty + d.qty > so_qty:
@@ -221,12 +223,21 @@
 		return frappe.db.sql("""select name from `tabProduct Bundle`
 			where new_item_code=%s and docstatus != 2""", item_code)
 
-	def get_already_delivered_qty(self, dn, so, so_detail):
-		qty = frappe.db.sql("""select sum(qty) from `tabDelivery Note Item`
+	def get_already_delivered_qty(self, current_docname, so, so_detail):
+		delivered_via_dn = frappe.db.sql("""select sum(qty) from `tabDelivery Note Item`
 			where so_detail = %s and docstatus = 1
 			and against_sales_order = %s
-			and parent != %s""", (so_detail, so, dn))
-		return qty and flt(qty[0][0]) or 0.0
+			and parent != %s""", (so_detail, so, current_docname))
+			
+		delivered_via_si = frappe.db.sql("""select sum(qty) from `tabSales Invoice Item`
+			where so_detail = %s and docstatus = 1
+			and sales_order = %s
+			and parent != %s""", (so_detail, so, current_docname))
+			
+		total_delivered_qty = (flt(delivered_via_dn[0][0]) if delivered_via_dn else 0) \
+			+ (flt(delivered_via_si[0][0]) if delivered_via_si else 0)
+		
+		return total_delivered_qty
 
 	def get_so_qty_and_warehouse(self, so_detail):
 		so_item = frappe.db.sql("""select qty, warehouse from `tabSales Order Item`
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index a47314b..cebeaf5 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -9,6 +9,8 @@
 
 from erpnext.controllers.accounts_controller import AccountsController
 from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map
+from erpnext.stock.utils import update_bin
+
 
 class StockController(AccountsController):
 	def make_gl_entries(self, repost_future_gle=True):
@@ -227,6 +229,23 @@
 			incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
 
 		return incoming_rate
+		
+	def update_reserved_qty(self, d):
+		if d['reserved_qty'] < 0 :
+			# Reduce reserved qty from reserved warehouse mentioned in so
+			if not d["reserved_warehouse"]:
+				frappe.throw(_("Reserved Warehouse is missing in Sales Order"))
+
+			args = {
+				"item_code": d['item_code'],
+				"warehouse": d["reserved_warehouse"],
+				"voucher_type": self.doctype,
+				"voucher_no": self.name,
+				"reserved_qty": (self.docstatus==1 and 1 or -1)*flt(d['reserved_qty']),
+				"posting_date": self.posting_date,
+				"is_amended": self.amended_from and 'Yes' or 'No'
+			}
+			update_bin(args)
 
 def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
 		warehouse_account=None):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 648d5b7..ef08098 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -180,3 +180,4 @@
 erpnext.patches.v5_1.rename_roles
 erpnext.patches.v5_1.default_bom
 execute:frappe.delete_doc("DocType", "Party Type")
+erpnext.patches.v5_4.fix_reserved_qty_and_sle_for_packed_items
diff --git a/erpnext/patches/v5_4/__init__.py b/erpnext/patches/v5_4/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/patches/v5_4/__init__.py
diff --git a/erpnext/patches/v5_4/fix_reserved_qty_and_sle_for_packed_items.py b/erpnext/patches/v5_4/fix_reserved_qty_and_sle_for_packed_items.py
new file mode 100644
index 0000000..0d46d99
--- /dev/null
+++ b/erpnext/patches/v5_4/fix_reserved_qty_and_sle_for_packed_items.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from erpnext.utilities.repost_stock import update_bin_qty, get_reserved_qty, repost_actual_qty
+
+def execute():
+	cancelled_invoices = frappe.db.sql_list("""select name from `tabSales Invoice` 
+		where docstatus = 2 and ifnull(update_stock, 0) = 1""")
+
+	if cancelled_invoices:
+		frappe.db.sql("""delete from `tabStock Ledger Entry` 
+			where voucher_type = 'Sales Invoice' and voucher_no in (%s)""" 
+			% (', '.join(['%s']*len(cancelled_invoices))), tuple(cancelled_invoices))
+			
+	for item_code, warehouse in frappe.db.sql("select item_code, warehouse from tabBin where ifnull(reserved_qty, 0) < 0"):
+		
+		repost_actual_qty(item_code, warehouse)
+		
+		update_bin_qty(item_code, warehouse, {
+			"reserved_qty": get_reserved_qty(item_code, warehouse)
+		})
+	
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index bf7505b..5e11962 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -9,7 +9,6 @@
 from frappe import msgprint, _
 import frappe.defaults
 from frappe.model.mapper import get_mapped_doc
-from erpnext.stock.utils import update_bin
 from erpnext.controllers.selling_controller import SellingController
 
 form_grid_templates = {
@@ -262,23 +261,6 @@
 
 		self.make_sl_entries(sl_entries)
 
-	def update_reserved_qty(self, d):
-		if d['reserved_qty'] < 0 :
-			# Reduce reserved qty from reserved warehouse mentioned in so
-			if not d["reserved_warehouse"]:
-				frappe.throw(_("Reserved Warehouse is missing in Sales Order"))
-
-			args = {
-				"item_code": d['item_code'],
-				"warehouse": d["reserved_warehouse"],
-				"voucher_type": self.doctype,
-				"voucher_no": self.name,
-				"reserved_qty": (self.docstatus==1 and 1 or -1)*flt(d['reserved_qty']),
-				"posting_date": self.posting_date,
-				"is_amended": self.amended_from and 'Yes' or 'No'
-			}
-			update_bin(args)
-
 def get_list_context(context=None):
 	from erpnext.controllers.website_list_for_contact import get_list_context
 	list_context = get_list_context(context)