diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 81fdbbefc35..90c67f1e521 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -74,7 +74,7 @@ def validate_returned_items(doc): for d in doc.get("items"): if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): if d.item_code not in valid_items: - frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + frappe.throw(_("Row # {0}: Returned Item {1} does not exist in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) @@ -266,6 +266,8 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.purchase_receipt_item = source_doc.name + elif doctype == "Purchase Invoice": target_doc.received_qty = -1* source_doc.received_qty target_doc.rejected_qty = -1* source_doc.rejected_qty @@ -282,6 +284,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.si_detail = source_doc.si_detail target_doc.expense_account = source_doc.expense_account + target_doc.dn_detail = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice": diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0f865760ca9..994d6ca5bee 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -652,6 +652,7 @@ erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.add_export_type_field_in_party_master erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 erpnext.patches.v12_0.create_irs_1099_field_united_states +erpnext.patches.v12_0.set_purchase_receipt_delivery_note_detail erpnext.patches.v12_0.add_permission_in_lower_deduction erpnext.patches.v12_0.set_permission_einvoicing erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py new file mode 100644 index 00000000000..52c9a2d7b3c --- /dev/null +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -0,0 +1,92 @@ +from __future__ import unicode_literals +import frappe +from collections import defaultdict + +def execute(): + + frappe.reload_doc('stock', 'doctype', 'delivery_note_item', force=True) + frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item', force=True) + + def map_rows(doc_row, return_doc_row, detail_field, doctype): + """Map rows after identifying similar ones.""" + + frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' + where name = '{return_doc_row_name}'""" \ + .format(doctype=doctype, + detail_field=detail_field, + doc_row_name=doc_row.get('name'), + return_doc_row_name=return_doc_row.get('name'))) #nosec + + def row_is_mappable(doc_row, return_doc_row, detail_field): + """Checks if two rows are similar enough to be mapped.""" + + if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field): + if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no: + return True + + elif doc_row.get('serial_no') and return_doc_row.get('serial_no'): + doc_sn = doc_row.serial_no.split('\n') + return_doc_sn = return_doc_row.serial_no.split('\n') + + if set(doc_sn) & set(return_doc_sn): + # if two rows have serial nos in common, map them + return True + + elif doc_row.rate == return_doc_row.rate: + return True + else: + return False + + def make_return_document_map(doctype, return_document_map): + """Returns a map of documents and it's return documents. + Format => { 'document' : ['return_document_1','return_document_2'] }""" + + return_against_documents = frappe.db.sql(""" + SELECT + return_against as document, name as return_document + FROM `tab{doctype}` + WHERE + is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec + + for entry in return_against_documents: + return_document_map[entry.document].append(entry.return_document) + + return return_document_map + + def set_document_detail_in_return_document(doctype): + """Map each row of the original document in the return document.""" + mapped = [] + return_document_map = defaultdict(list) + detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail" + + child_doc = frappe.scrub("{0} Item".format(doctype)) + frappe.reload_doc("stock", "doctype", child_doc) + + return_document_map = make_return_document_map(doctype, return_document_map) + + count = 0 + + #iterate through original documents and its return documents + for docname in return_document_map: + doc_items = frappe.get_cached_doc(doctype, docname).get("items") + for return_doc in return_document_map[docname]: + return_doc_items = frappe.get_cached_doc(doctype, return_doc).get("items") + + #iterate through return document items and original document items for mapping + for return_item in return_doc_items: + for doc_item in doc_items: + if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped: + map_rows(doc_item, return_item, detail_field, doctype) + mapped.append(doc_item.get('name')) + break + else: + continue + + # commit after every 100 sql updates + count += 1 + if count%100 == 0: + frappe.db.commit() + + set_document_detail_in_return_document("Purchase Receipt") + set_document_detail_in_return_document("Delivery Note") + frappe.db.commit() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c72bb893fff..f4334c4980a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -386,13 +386,12 @@ def get_invoiced_qty_map(delivery_note): def get_returned_qty_map(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 + returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, 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 @@ -411,7 +410,7 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("set_po_nos") if len(target.get("items")) == 0: - frappe.throw(_("All these items have already been invoiced")) + frappe.throw(_("All these items have already been Invoiced/Returned")) target.run_method("calculate_taxes_and_totals") @@ -436,9 +435,9 @@ def make_sales_invoice(source_name, target_doc=None): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = 0 - if returned_qty_map.get(item_row.item_code, 0) > 0: - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) - returned_qty_map[item_row.item_code] -= pending_qty + if returned_qty_map.get(item_row.name, 0) > 0: + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) + returned_qty_map[item_row.name] -= pending_qty if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index d7a93fb6917..cad822b369e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -623,6 +623,7 @@ class TestDeliveryNote(unittest.TestCase): dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True) dn1.items[0].against_sales_order = so.name dn1.items[0].so_detail = so.items[0].name + dn1.items[0].dn_detail = dn.items[0].name dn1.submit() si = make_sales_invoice(dn.name) @@ -649,7 +650,9 @@ class TestDeliveryNote(unittest.TestCase): si1.save() si1.submit() - create_delivery_note(is_return=1, return_against=dn.name, qty=-2) + dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() si2 = make_sales_invoice(dn.name) self.assertEquals(si2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a3386fce193..6e2adc3ac97 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -67,6 +67,7 @@ "so_detail", "against_sales_invoice", "si_detail", + "dn_detail", "section_break_40", "batch_no", "serial_no", @@ -715,12 +716,21 @@ "fieldname": "reason_for_return_section_break", "fieldtype": "Section Break", "label": "Reason For Return" + }, + { + "fieldname": "dn_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Against Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-06 14:18:33.131672", + "modified": "2020-04-28 14:18:33.131672", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f21c4ef8593..a150e097d42 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -495,7 +495,7 @@ def make_purchase_invoice(source_name, target_doc=None): def set_missing_values(source, target): if len(target.get("items")) == 0: - frappe.throw(_("All items have already been invoiced")) + frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 @@ -505,11 +505,11 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - returned_qty_map[source_doc.item_code] = returned_qty + returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: pending_qty = 0 @@ -567,13 +567,12 @@ def get_invoiced_qty_map(purchase_receipt): def get_returned_qty_map(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 + returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, 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 diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 09271cebfa8..65ff2de0b49 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -474,6 +474,7 @@ class TestPurchaseReceipt(unittest.TestCase): 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.items[0].purchase_receipt_item = pr.items[0].name pr1.submit() pi = make_purchase_invoice(pr.name) @@ -497,7 +498,9 @@ class TestPurchaseReceipt(unittest.TestCase): pi1.save() pi1.submit() - make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2) + pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True) + pr2.items[0].purchase_receipt_item = pr1.items[0].name + pr2.submit() pi2 = make_purchase_invoice(pr1.name) self.assertEquals(pi2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 095a712c1c1..3d46289d570 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -78,6 +78,7 @@ "stock_qty", "purchase_order_item", "material_request_item", + "purchase_receipt_item", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -817,10 +818,20 @@ "label": "Asset Category", "options": "Asset Category", "read_only": 1 + }, + { + "fieldname": "purchase_receipt_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Purchase Receipt Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, + "links": [], "modified": "2020-06-20 18:49:16.824489", "modified_by": "Administrator", "module": "Stock",