From 560ba391f91daf93b62046c1be41463d3b425474 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 29 Aug 2016 18:19:32 +0530 Subject: [PATCH 1/2] [Enhancement] Purchase return for rejected qty --- .../purchase_common/purchase_common.js | 31 ++----- erpnext/controllers/buying_controller.py | 4 +- .../controllers/sales_and_purchase_return.py | 87 ++++++++++++++----- 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/erpnext/buying/doctype/purchase_common/purchase_common.js b/erpnext/buying/doctype/purchase_common/purchase_common.js index ccf7a2faec..3083acb325 100644 --- a/erpnext/buying/doctype/purchase_common/purchase_common.js +++ b/erpnext/buying/doctype/purchase_common/purchase_common.js @@ -138,20 +138,15 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ }, qty: function(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) { - var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "received_qty"]); if(!(item.received_qty || item.rejected_qty) && item.qty) { item.received_qty = item.qty; } - if(item.qty > item.received_qty) { - msgprint(__("Error: {0} > {1}", [__(frappe.meta.get_label(item.doctype, "qty", item.name)), - __(frappe.meta.get_label(item.doctype, "received_qty", item.name))])) - item.qty = item.rejected_qty = 0.0; - } else { - item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); - } + frappe.model.round_floats_in(item, ["qty", "received_qty"]); + item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); } this._super(doc, cdt, cdn); @@ -160,26 +155,18 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ }, received_qty: function(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); - frappe.model.round_floats_in(item, ["qty", "received_qty"]); - - item.qty = (item.qty < item.received_qty) ? item.qty : item.received_qty; - this.qty(doc, cdt, cdn); + this.calculate_accepted_qty(doc, cdt, cdn) }, rejected_qty: function(doc, cdt, cdn) { + this.calculate_accepted_qty(doc, cdt, cdn) + }, + + calculate_accepted_qty: function(doc, cdt, cdn){ var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["received_qty", "rejected_qty"]); - if(item.rejected_qty > item.received_qty) { - msgprint(__("Error: {0} > {1}", [__(frappe.meta.get_label(item.doctype, "rejected_qty", item.name)), - __(frappe.meta.get_label(item.doctype, "received_qty", item.name))])); - item.qty = item.rejected_qty = 0.0; - } else { - - item.qty = flt(item.received_qty - item.rejected_qty, precision("qty", item)); - } - + item.qty = flt(item.received_qty - item.rejected_qty, precision("qty", item)); this.qty(doc, cdt, cdn); }, diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 88acfb7c17..f7181d7ea5 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -37,7 +37,7 @@ class BuyingController(StockController): self.validate_purchase_receipt_if_update_stock() if self.doctype=="Purchase Receipt" or (self.doctype=="Purchase Invoice" and self.update_stock): - self.validate_purchase_return() + # self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() @@ -346,7 +346,7 @@ class BuyingController(StockController): }) sl_entries.append(sle) - if flt(d.rejected_qty) > 0: + if flt(d.rejected_qty) != 0: sl_entries.append(self.get_sl_entries(d, { "warehouse": d.rejected_warehouse, "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8d30247acf..ae03a3562c 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -53,13 +53,15 @@ def validate_returned_items(doc): valid_items = frappe._dict() - select_fields = "item_code, qty" if doc.doctype=="Purchase Invoice" \ - else "item_code, qty, serial_no, batch_no" + select_fields = "item_code, qty, parenttype" if doc.doctype=="Purchase Invoice" \ + else "item_code, qty, serial_no, batch_no, parenttype" + + if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: + select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s""" .format(select_fields, doc.doctype), doc.return_against, as_dict=1): valid_items = get_ref_item_dict(valid_items, d) - if doc.doctype in ("Delivery Note", "Sales Invoice"): for d in frappe.db.sql("""select item_code, qty, serial_no, batch_no from `tabPacked Item` @@ -73,21 +75,15 @@ def validate_returned_items(doc): items_returned = False for d in doc.get("items"): - if flt(d.qty) < 0: + if flt(d.qty) < 0 or 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}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) - already_returned_qty = flt(already_returned_items.get(d.item_code)) - max_return_qty = flt(ref.qty) - already_returned_qty + validate_quantity(doc, d, ref, valid_items, already_returned_items) - if already_returned_qty >= ref.qty: - frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) - elif abs(d.qty) > max_return_qty: - frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") - .format(d.idx, ref.qty, d.item_code), StockOverReturnError) - elif ref.batch_no and d.batch_no not in ref.batch_no: + if ref.batch_no and d.batch_no not in ref.batch_no: frappe.throw(_("Row # {0}: Batch No must be same as {1} {2}") .format(d.idx, doc.doctype, doc.return_against)) elif ref.serial_no: @@ -107,18 +103,45 @@ def validate_returned_items(doc): if not items_returned: frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) - + +def validate_quantity(doc, args, ref, valid_items, already_returned_items): + fields = ['qty'] + if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: + fields.extend(['received_qty', 'rejected_qty']) + + already_returned_data = already_returned_items.get(args.item_code) or {} + + for column in fields: + return_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0 + referenced_qty = ref.get(column) + max_return_qty = flt(referenced_qty) - return_qty + label = column.replace('_', ' ').title() + + if flt(args.get(column)) > 0: + frappe.throw(_("{0} must be negative in return document").format(label)) + elif return_qty >= referenced_qty and flt(args.get(column)) != 0: + frappe.throw(_("Item {0} has already been returned").format(args.item_code), StockOverReturnError) + elif abs(args.get(column)) > max_return_qty: + frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") + .format(args.idx, referenced_qty, args.item_code), StockOverReturnError) + def get_ref_item_dict(valid_items, ref_item_row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items.setdefault(ref_item_row.item_code, frappe._dict({ "qty": 0, + "rejected_qty": 0, + "received_qty": 0, "serial_no": [], "batch_no": [] })) item_dict = valid_items[ref_item_row.item_code] item_dict["qty"] += ref_item_row.qty - + + if ref_item_row.parenttype in ['Purchase Invoice', 'Purchase Receipt']: + item_dict["received_qty"] += ref_item_row.received_qty + item_dict["rejected_qty"] += ref_item_row.rejected_qty + if ref_item_row.get("serial_no"): item_dict["serial_no"] += get_serial_nos(ref_item_row.serial_no) @@ -128,16 +151,30 @@ def get_ref_item_dict(valid_items, ref_item_row): return valid_items def get_already_returned_items(doc): - return frappe._dict(frappe.db.sql(""" - select - child.item_code, sum(abs(child.qty)) as qty + column = 'child.item_code, sum(abs(child.qty)) as qty' + if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: + column += ', sum(abs(child.rejected_qty)) as rejected_qty, sum(abs(child.received_qty)) as received_qty' + + data = frappe.db.sql(""" + select {0} from - `tab{0} Item` child, `tab{1}` par + `tab{1} Item` child, `tab{2}` par where child.parent = par.name and par.docstatus = 1 - and par.is_return = 1 and par.return_against = %s and child.qty < 0 + and par.is_return = 1 and par.return_against = %s group by item_code - """.format(doc.doctype, doc.doctype), doc.return_against)) + """.format(column, doc.doctype, doc.doctype), doc.return_against, as_dict=1) + + items = {} + + for d in data: + items.setdefault(d.item_code, frappe._dict({ + "qty": d.get("qty"), + "received_qty": d.get("received_qty"), + "rejected_qty": d.get("rejected_qty") + })) + + return items def make_return_doc(doctype, source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc @@ -166,12 +203,18 @@ def make_return_doc(doctype, source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1* source_doc.qty if doctype == "Purchase Receipt": - target_doc.received_qty = -1* source_doc.qty + target_doc.received_qty = -1* source_doc.received_qty + target_doc.rejected_qty = -1* source_doc.rejected_qty + target_doc.qty = -1* source_doc.qty target_doc.purchase_order = source_doc.purchase_order + target_doc.rejected_warehouse = source_doc.rejected_warehouse elif doctype == "Purchase Invoice": - target_doc.received_qty = -1* source_doc.qty + target_doc.received_qty = -1* source_doc.received_qty + target_doc.rejected_qty = -1* source_doc.rejected_qty + target_doc.qty = -1* source_doc.qty target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt + target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail elif doctype == "Delivery Note": From cc1c7ad2dfa14ef2deda6376252984520bedecdb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 2 Sep 2016 16:19:10 +0530 Subject: [PATCH 2/2] added test cases --- .../purchase_receipt/test_purchase_receipt.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index eba9201cb4..f961cdd1ef 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -125,7 +125,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr = make_purchase_receipt() return_pr = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-2) - + # check sle outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, "outgoing_rate") @@ -148,7 +148,21 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(expected_values[gle.account][1], gle.credit) set_perpetual_inventory(0) + + def test_purchase_return_for_rejected_qty(self): + set_perpetual_inventory() + + pr = make_purchase_receipt(received_qty=4, qty=2) + + return_pr = make_purchase_receipt(is_return=1, return_against=pr.name, received_qty = -4, qty=-2) + + actual_qty = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name, 'warehouse': return_pr.items[0].rejected_warehouse}, "actual_qty") + self.assertEqual(actual_qty, -2) + + set_perpetual_inventory(0) + def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) @@ -248,17 +262,23 @@ def make_purchase_receipt(**args): pr.currency = args.currency or "INR" pr.is_return = args.is_return pr.return_against = args.return_against - + qty = args.qty or 5 + received_qty = args.received_qty or qty + rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) + pr.append("items", { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.qty or 5, + "qty": qty, + "received_qty": received_qty, + "rejected_qty": rejected_qty, + "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rate": args.rate or 50, "conversion_factor": 1.0, "serial_no": args.serial_no, "stock_uom": "_Test UOM" }) + if not args.do_not_save: pr.insert() if not args.do_not_submit: