fix: stock reservation not working for sales invoice with update stock

This commit is contained in:
Rohit Waghchaure
2025-02-06 14:46:19 +05:30
parent 2482a3a205
commit 0c9d0ea1f4
4 changed files with 193 additions and 143 deletions

View File

@@ -450,6 +450,8 @@ class SalesInvoice(SellingController):
self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_reservation_entries()
self.update_stock_ledger()
# this sequence because outstanding may get -ve
@@ -559,6 +561,7 @@ class SalesInvoice(SellingController):
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.update_stock_reservation_entries()
self.repost_future_sle_and_gle()
self.db_set("status", "Cancelled")

View File

@@ -788,6 +788,151 @@ class SellingController(StockController):
validate_item_type(self, "is_sales_item", "sales")
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
# Don't update Delivered Qty on Return.
if self.is_return:
return
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
if self._action == "submit":
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.get(so_field) or not item.so_detail:
continue
sre_list = frappe.db.get_all(
"Stock Reservation Entry",
{
"docstatus": 1,
"voucher_type": "Sales Order",
"voucher_no": item.get(so_field),
"voucher_detail_no": item.so_detail,
"warehouse": item.warehouse,
"status": ["not in", ["Delivered", "Cancelled"]],
},
order_by="creation",
)
# Skip if no Stock Reservation Entries.
if not sre_list:
continue
qty_to_deliver = item.stock_qty
for sre in sre_list:
if qty_to_deliver <= 0:
break
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
qty_can_be_deliver = 0
if sre_doc.reservation_based_on == "Serial and Batch":
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
if sre_doc.has_serial_no:
delivered_serial_nos = [d.serial_no for d in sbb.entries]
for entry in sre_doc.sb_entries:
if entry.serial_no in delivered_serial_nos:
entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No.
entry.db_update()
qty_can_be_deliver += 1
delivered_serial_nos.remove(entry.serial_no)
else:
delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
for entry in sre_doc.sb_entries:
if entry.batch_no in delivered_batch_qty:
delivered_qty = min(
(entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no]
)
entry.delivered_qty += delivered_qty
entry.db_update()
qty_can_be_deliver += delivered_qty
delivered_batch_qty[entry.batch_no] -= delivered_qty
else:
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
qty_can_be_deliver = min(
(sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver
)
sre_doc.delivered_qty += qty_can_be_deliver
sre_doc.db_update()
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_deliver -= qty_can_be_deliver
if self._action == "cancel":
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.get(so_field) or not item.so_detail:
continue
sre_list = frappe.db.get_all(
"Stock Reservation Entry",
{
"docstatus": 1,
"voucher_type": "Sales Order",
"voucher_no": item.get(so_field),
"voucher_detail_no": item.so_detail,
"warehouse": item.warehouse,
"status": ["in", ["Partially Delivered", "Delivered"]],
},
order_by="creation",
)
# Skip if no Stock Reservation Entries.
if not sre_list:
continue
qty_to_undelivered = item.stock_qty
for sre in sre_list:
if qty_to_undelivered <= 0:
break
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
qty_can_be_undelivered = 0
if sre_doc.reservation_based_on == "Serial and Batch":
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
if sre_doc.has_serial_no:
serial_nos_to_undelivered = [d.serial_no for d in sbb.entries]
for entry in sre_doc.sb_entries:
if entry.serial_no in serial_nos_to_undelivered:
entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No.
entry.db_update()
qty_can_be_undelivered += 1
serial_nos_to_undelivered.remove(entry.serial_no)
else:
batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries}
for entry in sre_doc.sb_entries:
if entry.batch_no in batch_qty_to_undelivered:
undelivered_qty = min(
entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no]
)
entry.delivered_qty -= undelivered_qty
entry.db_update()
qty_can_be_undelivered += undelivered_qty
batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty
else:
# `Qty to Undelivered` should be less than or equal to `Delivered Qty`.
qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered)
sre_doc.delivered_qty -= qty_can_be_undelivered
sre_doc.db_update()
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_undelivered -= qty_can_be_undelivered
def set_default_income_account_for_item(obj):
for d in obj.get("items"):

View File

@@ -2276,6 +2276,51 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(dn.items[1].qty, 3)
self.assertEqual(dn.items[1].warehouse, self.warehouse_finished_goods)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
warehouse = create_warehouse("Test Warehouse 1", company=self.company)
make_stock_entry(
item_code=self.item,
target=warehouse,
qty=5,
company=self.company,
)
so = frappe.new_doc("Sales Order")
so.reserve_stock = 1
so.company = self.company
so.customer = self.customer
so.transaction_date = today()
so.currency = "INR"
so.append(
"items",
{
"item_code": self.item,
"qty": 5,
"rate": 2000,
"warehouse": warehouse,
"delivery_date": today(),
},
)
so.submit()
sres = frappe.get_all(
"Stock Reservation Entry",
filters={"voucher_no": so.name},
fields=["name"],
)
self.assertEqual(len(sres), 1)
sre_doc = frappe.get_doc("Stock Reservation Entry", sres[0].name)
self.assertFalse(sre_doc.status == "Delivered")
si = make_sales_invoice(so.name)
si.update_stock = 1
si.submit()
sre_doc.reload()
self.assertTrue(sre_doc.status == "Delivered")
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")

View File

@@ -493,149 +493,6 @@ class DeliveryNote(SellingController):
self.delete_auto_created_batches()
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
# Don't update Delivered Qty on Return.
if self.is_return:
return
if self._action == "submit":
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.against_sales_order or not item.so_detail:
continue
sre_list = frappe.db.get_all(
"Stock Reservation Entry",
{
"docstatus": 1,
"voucher_type": "Sales Order",
"voucher_no": item.against_sales_order,
"voucher_detail_no": item.so_detail,
"warehouse": item.warehouse,
"status": ["not in", ["Delivered", "Cancelled"]],
},
order_by="creation",
)
# Skip if no Stock Reservation Entries.
if not sre_list:
continue
qty_to_deliver = item.stock_qty
for sre in sre_list:
if qty_to_deliver <= 0:
break
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
qty_can_be_deliver = 0
if sre_doc.reservation_based_on == "Serial and Batch":
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
if sre_doc.has_serial_no:
delivered_serial_nos = [d.serial_no for d in sbb.entries]
for entry in sre_doc.sb_entries:
if entry.serial_no in delivered_serial_nos:
entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No.
entry.db_update()
qty_can_be_deliver += 1
delivered_serial_nos.remove(entry.serial_no)
else:
delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
for entry in sre_doc.sb_entries:
if entry.batch_no in delivered_batch_qty:
delivered_qty = min(
(entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no]
)
entry.delivered_qty += delivered_qty
entry.db_update()
qty_can_be_deliver += delivered_qty
delivered_batch_qty[entry.batch_no] -= delivered_qty
else:
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
qty_can_be_deliver = min(
(sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver
)
sre_doc.delivered_qty += qty_can_be_deliver
sre_doc.db_update()
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_deliver -= qty_can_be_deliver
if self._action == "cancel":
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.against_sales_order or not item.so_detail:
continue
sre_list = frappe.db.get_all(
"Stock Reservation Entry",
{
"docstatus": 1,
"voucher_type": "Sales Order",
"voucher_no": item.against_sales_order,
"voucher_detail_no": item.so_detail,
"warehouse": item.warehouse,
"status": ["in", ["Partially Delivered", "Delivered"]],
},
order_by="creation",
)
# Skip if no Stock Reservation Entries.
if not sre_list:
continue
qty_to_undelivered = item.stock_qty
for sre in sre_list:
if qty_to_undelivered <= 0:
break
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
qty_can_be_undelivered = 0
if sre_doc.reservation_based_on == "Serial and Batch":
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
if sre_doc.has_serial_no:
serial_nos_to_undelivered = [d.serial_no for d in sbb.entries]
for entry in sre_doc.sb_entries:
if entry.serial_no in serial_nos_to_undelivered:
entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No.
entry.db_update()
qty_can_be_undelivered += 1
serial_nos_to_undelivered.remove(entry.serial_no)
else:
batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries}
for entry in sre_doc.sb_entries:
if entry.batch_no in batch_qty_to_undelivered:
undelivered_qty = min(
entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no]
)
entry.delivered_qty -= undelivered_qty
entry.db_update()
qty_can_be_undelivered += undelivered_qty
batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty
else:
# `Qty to Undelivered` should be less than or equal to `Delivered Qty`.
qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered)
sre_doc.delivered_qty -= qty_can_be_undelivered
sre_doc.db_update()
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_undelivered -= qty_can_be_undelivered
def validate_against_stock_reservation_entries(self):
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""