Merge pull request #16745 from nabinhait/manage-returned-qty
fix: Consider returned qty while making invoice from DN / PR
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8911ddf..b1a851a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -201,7 +201,8 @@
get_query: function() {
var filters = {
docstatus: 1,
- company: me.frm.doc.company
+ company: me.frm.doc.company,
+ is_return: 0
};
if(me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 50996ed..1eb2b09 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -391,7 +391,7 @@
return invoiced_qty_map
-def get_returned_qty_map(sales_orders):
+def get_returned_qty_map_against_so(sales_orders):
"""returns a map: {so_detail: returned_qty}"""
returned_qty_map = {}
@@ -403,12 +403,25 @@
return returned_qty_map
+def get_returned_qty_map_against_dn(delivery_note):
+ """returns a map: {so_detail: returned_qty}"""
+ returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.item_code, sum(abs(dn_item.qty)) as qty
+ from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
+ where dn.name = dn_item.parent
+ and dn.docstatus = 1
+ and dn.is_return = 1
+ and dn.return_against = %s
+ group by dn_item.item_code
+ """, delivery_note))
+
+ return returned_qty_map
+
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
doc = frappe.get_doc('Delivery Note', source_name)
sales_orders = [d.against_sales_order for d in doc.items]
- returned_qty_map = get_returned_qty_map(sales_orders)
-
+ returned_qty_map_against_so = get_returned_qty_map_against_so(sales_orders)
+ returned_qty_map_against_dn = get_returned_qty_map_against_dn(source_name)
invoiced_qty_map = get_invoiced_qty_map(source_name)
def set_missing_values(source, target):
@@ -428,13 +441,26 @@
target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address))
def update_item(source_doc, target_doc, source_parent):
- target_doc.qty = (source_doc.qty -
- invoiced_qty_map.get(source_doc.name, 0) - returned_qty_map.get(source_doc.so_detail, 0))
+ target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ if not source_doc.so_detail:
+ returned_qty_map_against_dn[source_doc.item_code] = returned_qty
if source_doc.serial_no and source_parent.per_billed > 0:
target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code,
target_doc.qty, source_parent.name)
+ def get_pending_qty(item_row):
+ pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - returned_qty_map_against_so.get(item_row.so_detail, 0)
+ returned_qty = flt(returned_qty_map_against_dn.get(item_row.item_code, 0))
+ if not item_row.so_detail:
+ if returned_qty >= pending_qty:
+ pending_qty = 0
+ returned_qty -= pending_qty
+ else:
+ pending_qty -= returned_qty
+ returned_qty = 0
+ return pending_qty, returned_qty
+
doc = get_mapped_doc("Delivery Note", source_name, {
"Delivery Note": {
"doctype": "Sales Invoice",
@@ -453,7 +479,7 @@
"cost_center": "cost_center"
},
"postprocess": update_item,
- "filter": lambda d: abs(d.qty) - abs(invoiced_qty_map.get(d.name, 0))<=0
+ "filter": lambda d: get_pending_qty(d)[0]<=0
},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0c5a71c..5c03121 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -636,7 +636,7 @@
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
set_perpetual_inventory(0, company)
-
+
def test_make_sales_invoice_from_dn_for_returned_qty(self):
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
@@ -655,6 +655,33 @@
si = make_sales_invoice(dn.name)
self.assertEquals(si.items[0].qty, 1)
+ def test_make_sales_invoice_from_dn_with_returned_qty_against_dn(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+ dn = create_delivery_note(qty=8, do_not_submit=True)
+ dn.append("items", {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": 100,
+ "conversion_factor": 1.0,
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ })
+ dn.submit()
+
+ si1 = make_sales_invoice(dn.name)
+ si1.items[0].qty = 4
+ si1.items.pop(1)
+ si1.save()
+ si1.submit()
+
+ create_delivery_note(is_return=1, return_against=dn.name, qty=-2)
+
+ si2 = make_sales_invoice(dn.name)
+ self.assertEquals(si2.items[0].qty, 2)
+ self.assertEquals(si2.items[1].qty, 1)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index f006d00..cb51e22 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -125,7 +125,7 @@
else:
self.status = "Completed"
-
+
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO
@@ -311,7 +311,7 @@
"\n".join(warehouse_with_no_account))
return process_gl_map(gl_entries)
-
+
def get_asset_gl_entry(self, gl_entries):
for d in self.get("items"):
if d.is_fixed_asset:
@@ -405,6 +405,11 @@
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
+ doc = frappe.get_doc('Purchase Receipt', source_name)
+ purchase_orders = [d.purchase_order for d in doc.items]
+ returned_qty_map_against_po = get_returned_qty_map_against_po(purchase_orders)
+ returned_qty_map_against_pr = get_returned_qty_map_against_pr(source_name)
+
invoiced_qty_map = get_invoiced_qty_map(source_name)
def set_missing_values(source, target):
@@ -417,7 +422,23 @@
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
- target_doc.qty = source_doc.qty - invoiced_qty_map.get(source_doc.name, 0)
+ target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ if not source_doc.purchase_order_item:
+ returned_qty_map_against_pr[source_doc.item_code] = returned_qty
+
+ def get_pending_qty(item_row):
+ pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) \
+ - returned_qty_map_against_po.get(item_row.purchase_order_item, 0)
+ returned_qty = flt(returned_qty_map_against_pr.get(item_row.item_code, 0))
+ if not item_row.purchase_order_item:
+ if returned_qty >= pending_qty:
+ pending_qty = 0
+ returned_qty -= pending_qty
+ else:
+ pending_qty -= returned_qty
+ returned_qty = 0
+ return pending_qty, returned_qty
+
doclist = get_mapped_doc("Purchase Receipt", source_name, {
"Purchase Receipt": {
@@ -440,7 +461,7 @@
"asset": "asset",
},
"postprocess": update_item,
- "filter": lambda d: abs(d.qty) - abs(invoiced_qty_map.get(d.name, 0))<=0
+ "filter": lambda d: get_pending_qty(d)[0]<=0
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
@@ -462,6 +483,31 @@
return invoiced_qty_map
+def get_returned_qty_map_against_po(purchase_orders):
+ """returns a map: {so_detail: returned_qty}"""
+ returned_qty_map = {}
+
+ for name, returned_qty in frappe.get_all('Purchase Order Item', fields = ["name", "returned_qty"],
+ filters = {'parent': ('in', purchase_orders), 'docstatus': 1}, as_list=1):
+ if not returned_qty_map.get(name):
+ returned_qty_map[name] = 0
+ returned_qty_map[name] += returned_qty
+
+ return returned_qty_map
+
+def get_returned_qty_map_against_pr(purchase_receipt):
+ """returns a map: {so_detail: returned_qty}"""
+ returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.item_code, sum(abs(pr_item.qty)) as qty
+ from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
+ where pr.name = pr_item.parent
+ and pr.docstatus = 1
+ and pr.is_return = 1
+ and pr.return_against = %s
+ group by pr_item.item_code
+ """, purchase_receipt))
+
+ return returned_qty_map
+
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 472083b..c5ae838 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -404,6 +404,44 @@
set_perpetual_inventory(0, pr.company)
+ def test_make_purchase_invoice_from_pr_for_returned_qty(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
+
+ po = create_purchase_order()
+ pr = create_pr_against_po(po.name)
+
+ pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True)
+ pr1.items[0].purchase_order = po.name
+ pr1.items[0].purchase_order_item = po.items[0].name
+ pr1.submit()
+
+ pi = make_purchase_invoice(pr.name)
+ self.assertEquals(pi.items[0].qty, 3)
+
+ def test_make_purchase_invoice_from_dn_with_returned_qty_against_dn(self):
+ pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
+ pr1.append("items", {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "received_qty": 1,
+ "rate": 100,
+ "conversion_factor": 1.0,
+ })
+ pr1.submit()
+
+ pi1 = make_purchase_invoice(pr1.name)
+ pi1.items[0].qty = 4
+ pi1.items.pop(1)
+ pi1.save()
+ pi1.submit()
+
+ make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2)
+
+ pi2 = make_purchase_invoice(pr1.name)
+ self.assertEquals(pi2.items[0].qty, 2)
+ self.assertEquals(pi2.items[1].qty, 1)
+
def get_gl_entries(voucher_type, voucher_no):
return frappe.db.sql("""select account, debit, credit, cost_center
from `tabGL Entry` where voucher_type=%s and voucher_no=%s