feat: allow adding & deleting of items in submitted SO & PO (#19911)
* feat: allow adding of items after quotation submission
* feat: allow deletion of items from submitted SO & PO
* fix: case when items are added and deleted at once
* fix: add test cases
* For deletion of items while Updating Items after submitting PO & SO
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 08f5d8b..1712369 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -118,6 +118,73 @@
self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
+
+ def test_add_new_item_in_update_child_qty_rate(self):
+ po = create_purchase_order(do_not_save=1)
+ po.items[0].qty = 4
+ po.save()
+ po.submit()
+ pr = make_pr_against_po(po.name, 2)
+
+ po.load_from_db()
+ first_item_of_po = po.get("items")[0]
+
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ self.assertEquals(len(po.get('items')), 2)
+ self.assertEqual(po.status, 'To Receive and Bill')
+
+
+ def test_remove_item_in_update_child_qty_rate(self):
+ po = create_purchase_order(do_not_save=1)
+ po.items[0].qty = 4
+ po.save()
+ po.submit()
+ pr = make_pr_against_po(po.name, 2)
+
+ po.reload()
+ first_item_of_po = po.get("items")[0]
+ # add an item
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ # check if can remove received item
+ trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
+ self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
+
+ first_item_of_po = po.get("items")[0]
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': first_item_of_po.qty,
+ 'docname': first_item_of_po.name
+ }
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+
+ po.reload()
+ self.assertEquals(len(po.get('items')), 1)
+ self.assertEqual(po.status, 'To Receive and Bill')
+
def test_update_qty(self):
po = create_purchase_order()
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6150516..86f5d53 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1155,6 +1155,25 @@
child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item
+def check_and_delete_children(parent, data):
+ deleted_children = []
+ updated_item_names = [d.get("docname") for d in data]
+ for item in parent.items:
+ if item.name not in updated_item_names:
+ deleted_children.append(item)
+
+ for d in deleted_children:
+ if parent.doctype == "Sales Order" and flt(d.delivered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
+
+ if parent.doctype == "Purchase Order" and flt(d.received_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
+
+ if flt(d.billed_amt):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
+
+ d.cancel()
+ d.delete()
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
@@ -1163,6 +1182,8 @@
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
+ check_and_delete_children(parent, data)
+
for d in data:
new_child_flag = False
if not d.get("docname"):
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f363999..3f444f8 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -458,7 +458,8 @@
fieldname:"item_code",
options: 'Item',
in_list_view: 1,
- read_only: 1,
+ read_only: 0,
+ disabled: 0,
label: __('Item Code')
}, {
fieldtype:'Float',
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index feb6b76..d8e9a63 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -321,7 +321,12 @@
create_dn_against_so(so.name, 4)
make_sales_invoice(so.name)
- trans_item = json.dumps([{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}])
+ first_item_of_so = so.get("items")[0]
+ trans_item = json.dumps([
+ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
+ 'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name},
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}
+ ])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
@@ -330,6 +335,48 @@
self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill')
+
+ def test_remove_item_in_update_child_qty_rate(self):
+ so = make_sales_order(**{
+ "item_list": [{
+ "item_code": '_Test Item',
+ "qty": 5,
+ "rate":1000
+ }]
+ })
+ create_dn_against_so(so.name, 2)
+ make_sales_invoice(so.name)
+
+ # add an item so as to try removing items
+ trans_item = json.dumps([
+ {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
+ {"item_code": '_Test Item 2', "qty": 2, "rate":500}
+ ])
+ update_child_qty_rate('Sales Order', trans_item, so.name)
+ so.reload()
+ self.assertEqual(len(so.get("items")), 2)
+
+ # check if delivered items can be removed
+ trans_item = json.dumps([{
+ "item_code": '_Test Item 2',
+ "qty": 2,
+ "rate":500,
+ "docname": so.get("items")[1].name
+ }])
+ self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
+
+ #remove last added item
+ trans_item = json.dumps([{
+ "item_code": '_Test Item',
+ "qty": 5,
+ "rate":1000,
+ "docname": so.get("items")[0].name
+ }])
+ update_child_qty_rate('Sales Order', trans_item, so.name)
+
+ so.reload()
+ self.assertEqual(len(so.get("items")), 1)
+ self.assertEqual(so.status, 'To Deliver and Bill')
def test_update_child_qty_rate(self):