fix: Handle rows with same item code from Purchase Receipt to Invoice. (#20724)
* fix: Handle rows with same item code from Purchase Receipt to Invoice. * fix: Added patch, fixed tests, fixed delivery note behaviour * chore: Added comments amd fixed typo * fix: Added patch to patches.txt * fix: Patch fix and simplification, json timestamp updation. Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
parent
5210761e57
commit
5e3d8050aa
@ -74,7 +74,7 @@ def validate_returned_items(doc):
|
|||||||
for d in doc.get("items"):
|
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 and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0):
|
||||||
if d.item_code not in valid_items:
|
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))
|
.format(d.idx, d.item_code, doc.doctype, doc.return_against))
|
||||||
else:
|
else:
|
||||||
ref = valid_items.get(d.item_code, frappe._dict())
|
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 = source_doc.purchase_order
|
||||||
target_doc.purchase_order_item = source_doc.purchase_order_item
|
target_doc.purchase_order_item = source_doc.purchase_order_item
|
||||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||||
|
target_doc.purchase_receipt_item = source_doc.name
|
||||||
|
|
||||||
elif doctype == "Purchase Invoice":
|
elif doctype == "Purchase Invoice":
|
||||||
target_doc.received_qty = -1* source_doc.received_qty
|
target_doc.received_qty = -1* source_doc.received_qty
|
||||||
target_doc.rejected_qty = -1* source_doc.rejected_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.so_detail = source_doc.so_detail
|
||||||
target_doc.si_detail = source_doc.si_detail
|
target_doc.si_detail = source_doc.si_detail
|
||||||
target_doc.expense_account = source_doc.expense_account
|
target_doc.expense_account = source_doc.expense_account
|
||||||
|
target_doc.dn_detail = source_doc.name
|
||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
elif doctype == "Sales Invoice":
|
elif doctype == "Sales Invoice":
|
||||||
|
|||||||
@ -662,6 +662,7 @@ erpnext.patches.v12_0.create_irs_1099_field_united_states
|
|||||||
erpnext.patches.v12_0.move_bank_account_swift_number_to_bank
|
erpnext.patches.v12_0.move_bank_account_swift_number_to_bank
|
||||||
erpnext.patches.v12_0.rename_bank_reconciliation
|
erpnext.patches.v12_0.rename_bank_reconciliation
|
||||||
erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22
|
erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22
|
||||||
|
erpnext.patches.v12_0.set_purchase_receipt_delivery_note_detail
|
||||||
erpnext.patches.v12_0.add_permission_in_lower_deduction
|
erpnext.patches.v12_0.add_permission_in_lower_deduction
|
||||||
erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom
|
erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom
|
||||||
erpnext.patches.v12_0.rename_account_type_doctype
|
erpnext.patches.v12_0.rename_account_type_doctype
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
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)
|
||||||
|
|
||||||
|
#iterate through original documents and its return documents
|
||||||
|
for docname in return_document_map:
|
||||||
|
doc_items = frappe.get_doc(doctype, docname).get("items")
|
||||||
|
for return_doc in return_document_map[docname]:
|
||||||
|
return_doc_items = frappe.get_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
|
||||||
|
|
||||||
|
set_document_detail_in_return_document("Purchase Receipt")
|
||||||
|
set_document_detail_in_return_document("Delivery Note")
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -388,13 +388,12 @@ def get_invoiced_qty_map(delivery_note):
|
|||||||
|
|
||||||
def get_returned_qty_map(delivery_note):
|
def get_returned_qty_map(delivery_note):
|
||||||
"""returns a map: {so_detail: returned_qty}"""
|
"""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
|
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
|
||||||
where dn.name = dn_item.parent
|
where dn.name = dn_item.parent
|
||||||
and dn.docstatus = 1
|
and dn.docstatus = 1
|
||||||
and dn.is_return = 1
|
and dn.is_return = 1
|
||||||
and dn.return_against = %s
|
and dn.return_against = %s
|
||||||
group by dn_item.item_code
|
|
||||||
""", delivery_note))
|
""", delivery_note))
|
||||||
|
|
||||||
return returned_qty_map
|
return returned_qty_map
|
||||||
@ -413,7 +412,7 @@ def make_sales_invoice(source_name, target_doc=None):
|
|||||||
target.run_method("set_po_nos")
|
target.run_method("set_po_nos")
|
||||||
|
|
||||||
if len(target.get("items")) == 0:
|
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")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
@ -438,9 +437,9 @@ def make_sales_invoice(source_name, target_doc=None):
|
|||||||
pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
|
pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
|
||||||
|
|
||||||
returned_qty = 0
|
returned_qty = 0
|
||||||
if returned_qty_map.get(item_row.item_code, 0) > 0:
|
if returned_qty_map.get(item_row.name, 0) > 0:
|
||||||
returned_qty = flt(returned_qty_map.get(item_row.item_code, 0))
|
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
|
||||||
returned_qty_map[item_row.item_code] -= pending_qty
|
returned_qty_map[item_row.name] -= pending_qty
|
||||||
|
|
||||||
if returned_qty:
|
if returned_qty:
|
||||||
if returned_qty >= pending_qty:
|
if returned_qty >= pending_qty:
|
||||||
|
|||||||
@ -612,6 +612,7 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True)
|
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].against_sales_order = so.name
|
||||||
dn1.items[0].so_detail = so.items[0].name
|
dn1.items[0].so_detail = so.items[0].name
|
||||||
|
dn1.items[0].dn_detail = dn.items[0].name
|
||||||
dn1.submit()
|
dn1.submit()
|
||||||
|
|
||||||
si = make_sales_invoice(dn.name)
|
si = make_sales_invoice(dn.name)
|
||||||
@ -638,7 +639,9 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
si1.save()
|
si1.save()
|
||||||
si1.submit()
|
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)
|
si2 = make_sales_invoice(dn.name)
|
||||||
self.assertEquals(si2.items[0].qty, 2)
|
self.assertEquals(si2.items[0].qty, 2)
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
"so_detail",
|
"so_detail",
|
||||||
"against_sales_invoice",
|
"against_sales_invoice",
|
||||||
"si_detail",
|
"si_detail",
|
||||||
|
"dn_detail",
|
||||||
"section_break_40",
|
"section_break_40",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
@ -699,6 +700,15 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dn_detail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Against Delivery Note Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
|||||||
@ -504,7 +504,7 @@ def make_purchase_invoice(source_name, target_doc=None):
|
|||||||
|
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
if len(target.get("items")) == 0:
|
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 = frappe.get_doc(target)
|
||||||
doc.ignore_pricing_rule = 1
|
doc.ignore_pricing_rule = 1
|
||||||
@ -514,11 +514,11 @@ def make_purchase_invoice(source_name, target_doc=None):
|
|||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
target_doc.qty, returned_qty = get_pending_qty(source_doc)
|
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):
|
def get_pending_qty(item_row):
|
||||||
pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
|
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:
|
||||||
if returned_qty >= pending_qty:
|
if returned_qty >= pending_qty:
|
||||||
pending_qty = 0
|
pending_qty = 0
|
||||||
@ -576,13 +576,12 @@ def get_invoiced_qty_map(purchase_receipt):
|
|||||||
|
|
||||||
def get_returned_qty_map(purchase_receipt):
|
def get_returned_qty_map(purchase_receipt):
|
||||||
"""returns a map: {so_detail: returned_qty}"""
|
"""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
|
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
|
||||||
where pr.name = pr_item.parent
|
where pr.name = pr_item.parent
|
||||||
and pr.docstatus = 1
|
and pr.docstatus = 1
|
||||||
and pr.is_return = 1
|
and pr.is_return = 1
|
||||||
and pr.return_against = %s
|
and pr.return_against = %s
|
||||||
group by pr_item.item_code
|
|
||||||
""", purchase_receipt))
|
""", purchase_receipt))
|
||||||
|
|
||||||
return returned_qty_map
|
return returned_qty_map
|
||||||
|
|||||||
@ -475,6 +475,7 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True)
|
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 = po.name
|
||||||
pr1.items[0].purchase_order_item = po.items[0].name
|
pr1.items[0].purchase_order_item = po.items[0].name
|
||||||
|
pr1.items[0].purchase_receipt_item = pr.items[0].name
|
||||||
pr1.submit()
|
pr1.submit()
|
||||||
|
|
||||||
pi = make_purchase_invoice(pr.name)
|
pi = make_purchase_invoice(pr.name)
|
||||||
@ -498,7 +499,9 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
pi1.save()
|
pi1.save()
|
||||||
pi1.submit()
|
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)
|
pi2 = make_purchase_invoice(pr1.name)
|
||||||
self.assertEquals(pi2.items[0].qty, 2)
|
self.assertEquals(pi2.items[0].qty, 2)
|
||||||
|
|||||||
@ -71,6 +71,7 @@
|
|||||||
"quality_inspection",
|
"quality_inspection",
|
||||||
"purchase_order_item",
|
"purchase_order_item",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
|
"purchase_receipt_item",
|
||||||
"section_break_45",
|
"section_break_45",
|
||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"bom",
|
"bom",
|
||||||
@ -820,6 +821,15 @@
|
|||||||
"label": "Supplier Warehouse",
|
"label": "Supplier Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_receipt_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Purchase Receipt Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "image_column",
|
"fieldname": "image_column",
|
||||||
@ -829,7 +839,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-04-10 19:01:21.154963",
|
"modified": "2020-04-28 19:01:21.154963",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user