diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index d16f063c20..4b8bbee749 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -53,8 +53,9 @@ def validate_returned_items(doc): valid_items = frappe._dict() - select_fields = "item_code, qty, rate, parenttype" if doc.doctype=="Purchase Invoice" \ - else "item_code, qty, rate, serial_no, batch_no, parenttype" + select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor" + if doc.doctype != 'Purchase Invoice': + select_fields += ",serial_no, batch_no" if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: select_fields += ",rejected_qty, received_qty" @@ -111,7 +112,7 @@ def validate_returned_items(doc): 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'] + fields = ['stock_qty'] if doc.doctype in ['Purchase Receipt', 'Purchase Invoice']: fields.extend(['received_qty', 'rejected_qty']) @@ -119,16 +120,19 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items): for column in fields: returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0 - reference_qty = ref.get(column) + reference_qty = (ref.get(column) if column == 'stock_qty' + else ref.get(column) * ref.get("conversion_factor", 1.0)) + max_returnable_qty = flt(reference_qty) - returned_qty label = column.replace('_', ' ').title() + if reference_qty: if flt(args.get(column)) > 0: frappe.throw(_("{0} must be negative in return document").format(label)) elif returned_qty >= reference_qty and args.get(column): frappe.throw(_("Item {0} has already been returned") .format(args.item_code), StockOverReturnError) - elif abs(args.get(column)) > max_returnable_qty: + elif (abs(args.get(column)) * args.get("conversion_factor", 1.0)) > max_returnable_qty: frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") .format(args.idx, reference_qty, args.item_code), StockOverReturnError) @@ -138,6 +142,7 @@ def get_ref_item_dict(valid_items, ref_item_row): valid_items.setdefault(ref_item_row.item_code, frappe._dict({ "qty": 0, "rate": 0, + "stock_qty": 0, "rejected_qty": 0, "received_qty": 0, "serial_no": [], @@ -145,6 +150,7 @@ def get_ref_item_dict(valid_items, ref_item_row): })) item_dict = valid_items[ref_item_row.item_code] item_dict["qty"] += ref_item_row.qty + item_dict["stock_qty"] += ref_item_row.get('stock_qty', 0) if ref_item_row.get("rate", 0) > item_dict["rate"]: item_dict["rate"] = ref_item_row.get("rate", 0) @@ -161,9 +167,10 @@ def get_ref_item_dict(valid_items, ref_item_row): return valid_items def get_already_returned_items(doc): - column = 'child.item_code, sum(abs(child.qty)) as qty' + column = 'child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_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' + column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty, + sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" data = frappe.db.sql(""" select {0} @@ -180,6 +187,7 @@ def get_already_returned_items(doc): for d in data: items.setdefault(d.item_code, frappe._dict({ "qty": d.get("qty"), + "stock_qty": d.get("stock_qty"), "received_qty": d.get("received_qty"), "rejected_qty": d.get("rejected_qty") })) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b656c3f0dc..29caea156a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -11,7 +11,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext import set_perpetual_inventory from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError from erpnext.accounts.doctype.account.test_account import get_inventory_account - +from erpnext.stock.doctype.item.test_item import make_item class TestPurchaseReceipt(unittest.TestCase): def setUp(self): @@ -203,6 +203,22 @@ class TestPurchaseReceipt(unittest.TestCase): "delivery_document_no": return_pr.name }) + def test_purchase_return_for_multi_uom(self): + item_code = "_Test Purchase Return For Multi-UOM" + if not frappe.db.exists('Item', item_code): + item = make_item(item_code, {'stock_uom': 'Box'}) + row = item.append('uoms', { + 'uom': 'Unit', + 'conversion_factor': 0.1 + }) + row.db_update() + + pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0) + return_pr = make_purchase_receipt(item_code=item_code, qty=-10, uom="Unit", + stock_uom="Box", conversion_factor=0.1, is_return=1, return_against=pr.name) + + self.assertEquals(abs(return_pr.items[0].stock_qty), 1.0) + def test_closed_purchase_receipt(self): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status @@ -255,7 +271,6 @@ class TestPurchaseReceipt(unittest.TestCase): def test_not_accept_duplicate_serial_no(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note item_code = frappe.db.get_value('Item', {'has_serial_no': 1}) @@ -307,9 +322,10 @@ def make_purchase_receipt(**args): "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, + "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, - "stock_uom": "_Test UOM" + "stock_uom": args.stock_uom or "_Test UOM", + "uom": args.uom or "_Test UOM" }) if not args.do_not_save: diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 341c511f4e..77ba694388 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -71,6 +71,8 @@ class TransactionBase(StatusUpdater): validate_uom_is_integer(self, uom_field, qty_fields) def validate_with_previous_doc(self, ref): + self.exclude_fields = ["conversion_factor", "uom"] if self.get('is_return') else [] + for key, val in ref.items(): is_child = val.get("is_child_table") ref_doc = {} @@ -101,7 +103,7 @@ class TransactionBase(StatusUpdater): frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) for field, condition in fields: - if prevdoc_values[field] is not None: + if prevdoc_values[field] is not None and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc)