diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 267b0ed5dd5..3fdf92e7990 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -98,7 +98,29 @@ class BuyingController(SubcontractingController): item.from_warehouse, type_of_transaction="Outward", do_not_submit=True, + qty=item.qty, ) + elif ( + not self.is_new() + and item.serial_and_batch_bundle + and next( + ( + old_item + for old_item in self.get_doc_before_save().items + if old_item.name == item.name and old_item.qty != item.qty + ), + None, + ) + and len( + sabe := frappe.get_all( + "Serial and Batch Entry", + filters={"parent": item.serial_and_batch_bundle, "serial_no": ["is", "not set"]}, + pluck="name", + ) + ) + == 1 + ): + frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty) def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index eb33187033d..80f860b4553 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -811,7 +811,7 @@ class StockController(AccountsController): ) def make_package_for_transfer( - self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None + self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0 ): return make_bundle_for_material_transfer( is_new=self.is_new(), @@ -822,6 +822,7 @@ class StockController(AccountsController): warehouse=warehouse, type_of_transaction=type_of_transaction, do_not_submit=do_not_submit, + qty=qty, ) def get_sl_entries(self, d, args): @@ -1815,15 +1816,20 @@ def make_bundle_for_material_transfer(**kwargs): kwargs.type_of_transaction = "Inward" bundle_doc = frappe.copy_doc(bundle_doc) + bundle_doc.docstatus = 0 bundle_doc.warehouse = kwargs.warehouse bundle_doc.type_of_transaction = kwargs.type_of_transaction bundle_doc.voucher_type = kwargs.voucher_type bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no bundle_doc.is_cancelled = 0 + qty = 0 + if len(bundle_doc.entries) == 1 and kwargs.qty < bundle_doc.total_qty and not bundle_doc.has_serial_no: + qty = kwargs.qty + for row in bundle_doc.entries: row.is_outward = 0 - row.qty = abs(row.qty) + row.qty = abs(qty or row.qty) row.stock_value_difference = abs(row.stock_value_difference) if kwargs.type_of_transaction == "Outward": row.qty *= -1 diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8e8532a904b..984f313b848 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,8 @@ from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowti from pypika import functions as fn import erpnext +import erpnext.controllers +import erpnext.controllers.status_updater from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.buying_controller import QtyMismatchError @@ -4129,6 +4131,59 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertTrue(sles) + def test_internal_pr_qty_change_only_single_batch(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + def get_sabb_qty(sabb): + return frappe.get_value("Serial and Batch Bundle", sabb, "total_qty") + + item = make_item("Item with only Batch", {"has_batch_no": 1}) + item.create_new_batch = 1 + item.save() + + make_purchase_receipt( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + ) + + dn = create_delivery_note( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + customer="_Test Internal Customer 2", + cost_center="Main - TCP1", + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + ) + pr = make_inter_company_purchase_receipt(dn.name) + + pr.items[0].warehouse = "Stores - TCP1" + pr.items[0].qty = 8 + pr.save() + + # Test 1 - Check if SABB qty is changed on first save + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 8) + + pr.items[0].qty = 6 + pr.items[0].received_qty = 6 + pr.save() + + # Test 2 - Check if SABB qty is changed when saved again + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 6) + + pr.items[0].qty = 12 + pr.items[0].received_qty = 12 + + # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN + self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier