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)