Multi-UOM for sales/purchase return (#13132)
* Multi-UOM for sales/purchase return
* Update sales_and_purchase_return.py
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index d16f063..4b8bbee 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -53,8 +53,9 @@
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 @@
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 @@
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 @@
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 @@
}))
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 @@
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 @@
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 b656c3f..29caea1 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 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 @@
"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 @@
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 @@
"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 341c511..77ba694 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -71,6 +71,8 @@
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 @@
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)