mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 20:35:09 +00:00
fix: stock reservation not working for sales invoice with update stock
(cherry picked from commit 0c9d0ea1f4)
# Conflicts:
# erpnext/selling/doctype/sales_order/test_sales_order.py
This commit is contained in:
@@ -460,6 +460,8 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
self.make_bundle_for_sales_purchase_return(table_name)
|
self.make_bundle_for_sales_purchase_return(table_name)
|
||||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||||
|
|
||||||
|
self.update_stock_reservation_entries()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
# this sequence because outstanding may get -ve
|
# this sequence because outstanding may get -ve
|
||||||
@@ -561,6 +563,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
|
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
|
self.update_stock_reservation_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
|
|||||||
@@ -791,6 +791,151 @@ class SellingController(StockController):
|
|||||||
|
|
||||||
validate_item_type(self, "is_sales_item", "sales")
|
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):
|
def set_default_income_account_for_item(obj):
|
||||||
for d in obj.get("items"):
|
for d in obj.get("items"):
|
||||||
|
|||||||
@@ -2119,6 +2119,123 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
@IntegrationTestCase.change_settings("Stock Settings", {"enable_stock_reservation": True})
|
||||||
|
def test_warehouse_mapping_based_on_stock_reservation(self):
|
||||||
|
self.create_company(company_name="Glass Ceiling", abbr="GC")
|
||||||
|
self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company, 2000)
|
||||||
|
self.create_customer()
|
||||||
|
self.clear_old_entries()
|
||||||
|
|
||||||
|
so = frappe.new_doc("Sales Order")
|
||||||
|
so.company = self.company
|
||||||
|
so.customer = self.customer
|
||||||
|
so.transaction_date = today()
|
||||||
|
so.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": self.item,
|
||||||
|
"qty": 10,
|
||||||
|
"rate": 2000,
|
||||||
|
"warehouse": self.warehouse_stores,
|
||||||
|
"delivery_date": today(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
# Create stock
|
||||||
|
se = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Stock Entry",
|
||||||
|
"company": self.company,
|
||||||
|
"stock_entry_type": "Material Receipt",
|
||||||
|
"posting_date": today(),
|
||||||
|
"items": [
|
||||||
|
{"item_code": self.item, "t_warehouse": self.warehouse_stores, "qty": 5},
|
||||||
|
{"item_code": self.item, "t_warehouse": self.warehouse_finished_goods, "qty": 5},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
# Reserve stock on 2 different warehouses
|
||||||
|
itm = so.items[0]
|
||||||
|
so.create_stock_reservation_entries(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"sales_order_item": itm.name,
|
||||||
|
"item_code": itm.item_code,
|
||||||
|
"warehouse": self.warehouse_stores,
|
||||||
|
"qty_to_reserve": 2,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
so.create_stock_reservation_entries(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"sales_order_item": itm.name,
|
||||||
|
"item_code": itm.item_code,
|
||||||
|
"warehouse": self.warehouse_finished_goods,
|
||||||
|
"qty_to_reserve": 3,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delivery note should auto-select warehouse based on reservation
|
||||||
|
dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True})
|
||||||
|
self.assertEqual(2, len(dn.items))
|
||||||
|
self.assertEqual(dn.items[0].qty, 2)
|
||||||
|
self.assertEqual(dn.items[0].warehouse, self.warehouse_stores)
|
||||||
|
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")
|
||||||
|
|
||||||
|
>>>>>>> 0c9d0ea1f4 (fix: stock reservation not working for sales invoice with update stock)
|
||||||
|
|
||||||
def automatically_fetch_payment_terms(enable=1):
|
def automatically_fetch_payment_terms(enable=1):
|
||||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||||
|
|||||||
@@ -491,149 +491,6 @@ class DeliveryNote(SellingController):
|
|||||||
|
|
||||||
self.delete_auto_created_batches()
|
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):
|
def validate_against_stock_reservation_entries(self):
|
||||||
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
|
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user