diff --git a/selling/doctype/sales_common/sales_common.py b/selling/doctype/sales_common/sales_common.py index 797462a853..47d139f955 100644 --- a/selling/doctype/sales_common/sales_common.py +++ b/selling/doctype/sales_common/sales_common.py @@ -384,36 +384,38 @@ class DocType(TransactionBase): def get_item_list(self, obj, is_stopped=0): """get item list""" il = [] - for d in getlist(obj.doclist,obj.fname): - reserved_wh, reserved_qty = '', 0 # used for delivery note - qty = flt(d.qty) - if is_stopped: - qty = flt(d.qty) > flt(d.delivered_qty) and flt(flt(d.qty) - flt(d.delivered_qty)) or 0 + for d in getlist(obj.doclist, obj.fname): + reserved_warehouse = "" + reserved_qty_for_main_item = 0 + + if obj.doc.doctype == "Sales Order": + reserved_warehouse = d.reserved_warehouse + if flt(d.qty) > flt(d.delivered_qty): + reserved_qty_for_main_item = flt(d.qty) - flt(d.delivered_qty) - if d.prevdoc_doctype == 'Sales Order': - # used in delivery note to reduce reserved_qty - # Eg.: 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. + if obj.doc.doctype == "Delivery Note" and d.prevdoc_doctype == 'Sales Order': + # 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(obj.doc.name, + d.prevdoc_docname, d.prevdoc_detail_docname) + so_qty, reserved_warehouse = self.get_so_qty_and_warehouse(d.prevdoc_detail_docname) + + if already_delivered_qty + d.qty > so_qty: + reserved_qty_for_main_item = -(so_qty - already_delivered_qty) + else: + reserved_qty_for_main_item = -flt(d.qty) - tot_qty, max_qty, tot_amt, max_amt, reserved_wh = self.get_curr_and_ref_doc_details(d.doctype, 'prevdoc_detail_docname', d.prevdoc_detail_docname, obj.doc.name, obj.doc.doctype) - if((flt(tot_qty) + flt(qty) > flt(max_qty))): - reserved_qty = -(flt(max_qty)-flt(tot_qty)) - else: - reserved_qty = - flt(qty) - - if obj.doc.doctype == 'Sales Order': - reserved_wh = d.reserved_warehouse - if self.has_sales_bom(d.item_code): for p in getlist(obj.doclist, 'packing_details'): if p.parent_detail_docname == d.name and p.parent_item == d.item_code: # the packing details table's qty is already multiplied with parent's qty il.append({ 'warehouse': p.warehouse, - 'reserved_warehouse': reserved_wh, + 'reserved_warehouse': reserved_warehouse, 'item_code': p.item_code, 'qty': flt(p.qty), - 'reserved_qty': (flt(p.qty)/qty)*(reserved_qty), + 'reserved_qty': (flt(p.qty)/flt(d.qty)) * reserved_qty_for_main_item, 'uom': p.uom, 'batch_no': cstr(p.batch_no).strip(), 'serial_no': cstr(p.serial_no).strip(), @@ -422,10 +424,10 @@ class DocType(TransactionBase): else: il.append({ 'warehouse': d.warehouse, - 'reserved_warehouse': reserved_wh, + 'reserved_warehouse': reserved_warehouse, 'item_code': d.item_code, - 'qty': qty, - 'reserved_qty': reserved_qty, + 'qty': d.qty, + 'reserved_qty': reserved_qty_for_main_item, 'uom': d.stock_uom, 'batch_no': cstr(d.batch_no).strip(), 'serial_no': cstr(d.serial_no).strip(), @@ -433,27 +435,20 @@ class DocType(TransactionBase): }) return il + def get_already_delivered_qty(self, dn, so, so_detail): + qty = webnotes.conn.sql("""select sum(qty) from `tabDelivery Note Item` + where prevdoc_detail_docname = %s and docstatus = 1 + and prevdoc_doctype = 'Sales Order' and prevdoc_docname = %s + and parent != %s""", (so_detail, so, dn)) + return qty and flt(qty[0][0]) or 0.0 - def get_curr_and_ref_doc_details(self, curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name, curr_parent_doctype): - """ Get qty, amount already billed or delivered against curr line item for current doctype - For Eg: SO-RV get total qty, amount from SO and also total qty, amount against that SO in RV - """ - #Get total qty, amt of current doctype (eg RV) except for qty, amt of this transaction - if curr_parent_doctype == 'Installation Note': - curr_det = webnotes.conn.sql("select sum(qty) from `tab%s` where %s = '%s' and docstatus = 1 and parent != '%s'"% (curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name)) - qty, amt = curr_det and flt(curr_det[0][0]) or 0, 0 - else: - curr_det = webnotes.conn.sql("select sum(qty), sum(amount) from `tab%s` where %s = '%s' and docstatus = 1 and parent != '%s'"% (curr_doctype, ref_tab_fname, ref_tab_dn, curr_parent_name)) - qty, amt = curr_det and flt(curr_det[0][0]) or 0, curr_det and flt(curr_det[0][1]) or 0 + def get_so_qty_and_warehouse(self, so_detail): + so_item = webnotes.conn.sql("""select qty, reserved_warehouse from `tabSales Order Item` + where name = %s and docstatus = 1""", so_detail, as_dict=1) + so_qty = so_item and flt(so_item[0]["qty"]) or 0.0 + so_warehouse = so_item and so_item[0]["reserved_warehouse"] or "" + return so_qty, so_warehouse - # get total qty of ref doctype - so_det = webnotes.conn.sql("select qty, amount, reserved_warehouse from `tabSales Order Item` where name = '%s' and docstatus = 1"% ref_tab_dn) - max_qty, max_amt, res_wh = so_det and flt(so_det[0][0]) or 0, so_det and flt(so_det[0][1]) or 0, so_det and cstr(so_det[0][2]) or '' - return qty, max_qty, amt, max_amt, res_wh - - - # Make Packing List from Sales BOM - # ======================================================================= def has_sales_bom(self, item_code): return webnotes.conn.sql("select name from `tabSales BOM` where new_item_code=%s and docstatus != 2", item_code) diff --git a/selling/doctype/sales_order/sales_order.py b/selling/doctype/sales_order/sales_order.py index b176161709..31182b274a 100644 --- a/selling/doctype/sales_order/sales_order.py +++ b/selling/doctype/sales_order/sales_order.py @@ -320,28 +320,28 @@ class DocType(SellingController): def stop_sales_order(self): self.check_modified_date() - self.update_stock_ledger(update_stock = -1,clear = 1) + self.update_stock_ledger(update_stock = -1,is_stopped = 1) webnotes.conn.set(self.doc, 'status', 'Stopped') msgprint("""%s: %s has been Stopped. To make transactions against this Sales Order you need to Unstop it.""" % (self.doc.doctype, self.doc.name)) def unstop_sales_order(self): self.check_modified_date() - self.update_stock_ledger(update_stock = 1,clear = 1) + self.update_stock_ledger(update_stock = 1,is_stopped = 1) webnotes.conn.set(self.doc, 'status', 'Submitted') msgprint("%s: %s has been Unstopped" % (self.doc.doctype, self.doc.name)) - def update_stock_ledger(self, update_stock, clear = 0): - for d in self.get_item_list(clear): + def update_stock_ledger(self, update_stock, is_stopped = 0): + for d in self.get_item_list(is_stopped): if webnotes.conn.get_value("Item", d['item_code'], "is_stock_item") == "Yes": if not d['reserved_warehouse']: msgprint("""Please enter Reserved Warehouse for item %s as it is stock Item""" % d['item_code'], raise_exception=1) - + args = { "item_code": d['item_code'], - "reserved_qty": flt(update_stock) * flt(d['qty']), + "reserved_qty": flt(update_stock) * flt(d['reserved_qty']), "posting_date": self.doc.transaction_date, "voucher_type": self.doc.doctype, "voucher_no": self.doc.name, @@ -350,8 +350,8 @@ class DocType(SellingController): get_obj('Warehouse', d['reserved_warehouse']).update_bin(args) - def get_item_list(self, clear): - return get_obj('Sales Common').get_item_list( self, clear) + def get_item_list(self, is_stopped): + return get_obj('Sales Common').get_item_list( self, is_stopped) def on_update(self): pass \ No newline at end of file diff --git a/selling/doctype/sales_order/test_sales_order.py b/selling/doctype/sales_order/test_sales_order.py index f1a159a9c5..5d820fe769 100644 --- a/selling/doctype/sales_order/test_sales_order.py +++ b/selling/doctype/sales_order/test_sales_order.py @@ -3,50 +3,219 @@ from webnotes.utils import flt import unittest class TestSalesOrder(unittest.TestCase): - def make(self): - w = webnotes.model_wrapper(webnotes.copy_doclist(test_records[0])) + def create_so(self, so_doclist = None): + if not so_doclist: + so_doclist =test_records[0] + + w = webnotes.bean(copy=so_doclist) w.insert() w.submit() return w + def create_dn_against_so(self, so, delivered_qty=0): + from stock.doctype.delivery_note.test_delivery_note import test_records as dn_test_records + dn = webnotes.bean(webnotes.copy_doclist(dn_test_records[0])) + dn.doclist[1].item_code = so.doclist[1].item_code + dn.doclist[1].prevdoc_doctype = "Sales Order" + dn.doclist[1].prevdoc_docname = so.doc.name + dn.doclist[1].prevdoc_detail_docname = so.doclist[1].name + if delivered_qty: + dn.doclist[1].qty = delivered_qty + dn.insert() + dn.submit() + return dn + def get_bin_reserved_qty(self, item_code, warehouse): return flt(webnotes.conn.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty")) + + def delete_bin(self, item_code, warehouse): + bin = webnotes.conn.exists({"doctype": "Bin", "item_code": item_code, + "warehouse": warehouse}) + if bin: + webnotes.delete_doc("Bin", bin[0][0]) + + def check_reserved_qty(self, item_code, warehouse, qty): + bin_reserved_qty = self.get_bin_reserved_qty(item_code, warehouse) + self.assertEqual(bin_reserved_qty, qty) + + def test_reserved_qty_for_so(self): + # reset bin + self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"]) - def test_reserved_qty_so_submit_cancel(self): # submit - so = self.make() - print self.get_bin_reserved_qty(so.doclist[1].item_code, - so.doclist[1].reserved_warehouse) - - reserved_qty = self.get_bin_reserved_qty(so.doclist[1].item_code, - so.doclist[1].reserved_warehouse) - self.assertEqual(reserved_qty, 10.0) + so = self.create_so() + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0) + # cancel so.cancel() - reserved_qty = self.get_bin_reserved_qty(so.doclist[1].item_code, - so.doclist[1].reserved_warehouse) - self.assertEqual(reserved_qty, 0.0) + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0) + - def test_reserved_qty_dn_submit_cancel(self): - so = self.make() + def test_reserved_qty_for_partial_delivery(self): + # reset bin + self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"]) + + # submit so + so = self.create_so() # allow negative stock webnotes.conn.set_default("allow_negative_stock", 1) - # dn submit (against so) - from stock.doctype.delivery_note.test_delivery_note import test_records as dn_test_records - dn = webnotes.model_wrapper(webnotes.copy_doclist(dn_test_records[0])) - dn.doclist[1].prevdoc_doctype = "Sales Order" - dn.doclist[1].prevdoc_docname = so.doc.name - dn.doclist[1].prevdoc_detail_docname = so.doclist[1].name - dn.insert() - dn.submit() + # submit dn + dn = self.create_dn_against_so(so) - reserved_qty = self.get_bin_reserved_qty(so.doclist[1].item_code, - so.doclist[1].reserved_warehouse) - self.assertEqual(reserved_qty, 6.0) + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 6.0) + # stop so + so.load_from_db() + so.obj.stop_sales_order() + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0) + + # unstop so + so.load_from_db() + so.obj.unstop_sales_order() + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 6.0) + + # cancel dn + dn.cancel() + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0) + + def test_reserved_qty_for_over_delivery(self): + # reset bin + self.delete_bin(test_records[0][1]["item_code"], test_records[0][1]["reserved_warehouse"]) + + # submit so + so = self.create_so() + + # allow negative stock + webnotes.conn.set_default("allow_negative_stock", 1) + + # set over-delivery tolerance + webnotes.conn.set_value('Item', so.doclist[1].item_code, 'tolerance', 50) + + # submit dn + dn = self.create_dn_against_so(so, 15) + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 0.0) + + # cancel dn + dn.cancel() + self.check_reserved_qty(so.doclist[1].item_code, so.doclist[1].reserved_warehouse, 10.0) + + def test_reserved_qty_for_so_with_packing_list(self): + from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records + + # change item in test so record + test_record = test_records[0][:] + test_record[1]["item_code"] = "_Test Sales BOM Item" + + # reset bin + self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"]) + self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"]) + + # submit + so = self.create_so(test_record) + + + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 50.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 20.0) + + # cancel + so.cancel() + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + + def test_reserved_qty_for_partial_delivery_with_packing_list(self): + from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records + + # change item in test so record + + test_record = webnotes.copy_doclist(test_records[0]) + test_record[1]["item_code"] = "_Test Sales BOM Item" + + # reset bin + self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"]) + self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"]) + + # submit + so = self.create_so(test_record) + + # allow negative stock + webnotes.conn.set_default("allow_negative_stock", 1) + + # submit dn + dn = self.create_dn_against_so(so) + + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 30.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 12.0) + + # stop so + so.load_from_db() + so.obj.stop_sales_order() + + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + + # unstop so + so.load_from_db() + so.obj.unstop_sales_order() + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 30.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 12.0) + + # cancel dn + dn.cancel() + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 50.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 20.0) + + def test_reserved_qty_for_over_delivery_with_packing_list(self): + from stock.doctype.sales_bom.test_sales_bom import test_records as sbom_test_records + + # change item in test so record + test_record = webnotes.copy_doclist(test_records[0]) + test_record[1]["item_code"] = "_Test Sales BOM Item" + + # reset bin + self.delete_bin(sbom_test_records[0][1]["item_code"], test_record[1]["reserved_warehouse"]) + self.delete_bin(sbom_test_records[0][2]["item_code"], test_record[1]["reserved_warehouse"]) + + # submit + so = self.create_so(test_record) + + # allow negative stock + webnotes.conn.set_default("allow_negative_stock", 1) + + # set over-delivery tolerance + webnotes.conn.set_value('Item', so.doclist[1].item_code, 'tolerance', 50) + + # submit dn + dn = self.create_dn_against_so(so, 15) + + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 0.0) + + # cancel dn + dn.cancel() + self.check_reserved_qty(sbom_test_records[0][1]["item_code"], + so.doclist[1].reserved_warehouse, 50.0) + self.check_reserved_qty(sbom_test_records[0][2]["item_code"], + so.doclist[1].reserved_warehouse, 20.0) + +test_dependencies = ["Sales BOM"] + test_records = [ [ { @@ -80,5 +249,5 @@ test_records = [ "amount": 500.0, "reserved_warehouse": "_Test Warehouse", } - ] + ], ] \ No newline at end of file diff --git a/stock/doctype/delivery_note/delivery_note.py b/stock/doctype/delivery_note/delivery_note.py index b8d20fbe85..f54edf221a 100644 --- a/stock/doctype/delivery_note/delivery_note.py +++ b/stock/doctype/delivery_note/delivery_note.py @@ -319,9 +319,9 @@ class DocType(SellingController): webnotes.msgprint("%s Packing Slip(s) Cancelled" % res[0][1]) - def update_stock_ledger(self, update_stock, is_stopped = 0): + def update_stock_ledger(self, update_stock): self.values = [] - for d in self.get_item_list(is_stopped): + for d in self.get_item_list(): if webnotes.conn.get_value("Item", d['item_code'], "is_stock_item") == "Yes": if not d['warehouse']: msgprint("Please enter Warehouse for item %s as it is stock item" @@ -344,8 +344,8 @@ class DocType(SellingController): get_obj('Stock Ledger', 'Stock Ledger').update_stock(self.values) - def get_item_list(self, is_stopped): - return get_obj('Sales Common').get_item_list(self, is_stopped) + def get_item_list(self): + return get_obj('Sales Common').get_item_list(self) def make_sl_entry(self, d, wh, qty, in_value, update_stock): diff --git a/stock/doctype/item/test_item.py b/stock/doctype/item/test_item.py index 4238e149bb..853283e4c6 100644 --- a/stock/doctype/item/test_item.py +++ b/stock/doctype/item/test_item.py @@ -145,4 +145,23 @@ test_records = [ "is_sub_contracted_item": "No", "stock_uom": "_Test UOM" }], + [{ + "doctype": "Item", + "item_code": "_Test Sales BOM Item", + "item_name": "_Test Sales BOM Item", + "description": "_Test Sales BOM Item", + "item_group": "_Test Item Group Desktops", + "is_stock_item": "No", + "is_asset_item": "No", + "has_batch_no": "No", + "has_serial_no": "No", + "is_purchase_item": "Yes", + "is_sales_item": "Yes", + "is_service_item": "No", + "is_sample_item": "No", + "inspection_required": "No", + "is_pro_applicable": "No", + "is_sub_contracted_item": "No", + "stock_uom": "_Test UOM" + }], ] \ No newline at end of file diff --git a/stock/doctype/sales_bom/test_sales_bom.py b/stock/doctype/sales_bom/test_sales_bom.py new file mode 100644 index 0000000000..850616fdbb --- /dev/null +++ b/stock/doctype/sales_bom/test_sales_bom.py @@ -0,0 +1,20 @@ +test_records = [ + [ + { + "doctype": "Sales BOM", + "new_item_code": "_Test Sales BOM Item" + }, + { + "doctype": "Sales BOM Item", + "item_code": "_Test Item", + "parentfield": "sales_bom_items", + "qty": 5.0 + }, + { + "doctype": "Sales BOM Item", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "sales_bom_items", + "qty": 2.0 + } + ], +] \ No newline at end of file