mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 05:39:12 +00:00
fix: miscellaneous
fix: don't reserve stock in group warehouse fix: partial reservation in multiple warehouses feat: add prompt to select warehouse and qty for reservation in SO
This commit is contained in:
@@ -169,19 +169,82 @@ frappe.ui.form.on("Sales Order", {
|
||||
},
|
||||
|
||||
create_stock_reservation_entries(frm) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'create_stock_reservation_entries',
|
||||
args: {
|
||||
notify: true
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Reserving Stock...'),
|
||||
callback: (r) => {
|
||||
frm.doc.__onload.has_unreserved_stock = false;
|
||||
frm.refresh();
|
||||
let items_data = [];
|
||||
|
||||
const dialog = frappe.prompt({fieldname: 'items', fieldtype: 'Table', label: __('Items to Reserve'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'name',
|
||||
label: __('Name'),
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'item_code',
|
||||
label: __('Item Code'),
|
||||
options: 'Item',
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'warehouse',
|
||||
label: __('Warehouse'),
|
||||
options: 'Warehouse',
|
||||
reqd: 1,
|
||||
in_list_view: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'qty_to_reserve',
|
||||
label: __('Qty'),
|
||||
reqd: 1,
|
||||
in_list_view: 1
|
||||
}
|
||||
],
|
||||
data: items_data,
|
||||
in_place_edit: true,
|
||||
get_data: function() {
|
||||
return items_data;
|
||||
}
|
||||
})
|
||||
}, function(data) {
|
||||
if (data.items.length > 0) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'create_stock_reservation_entries',
|
||||
args: {
|
||||
items_details: data.items,
|
||||
notify: true
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Reserving Stock...'),
|
||||
callback: (r) => {
|
||||
frm.doc.__onload.has_unreserved_stock = false;
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, __("Stock Reservation"), __("Reserve Stock"));
|
||||
|
||||
frm.doc.items.forEach(item => {
|
||||
if (item.reserve_stock) {
|
||||
let unreserved_qty = (flt(item.stock_qty) - (flt(item.delivered_qty) * flt(item.conversion_factor)) - flt(item.stock_reserved_qty))
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
'name': item.name,
|
||||
'item_code': item.item_code,
|
||||
'warehouse': item.warehouse,
|
||||
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.fields_dict.items.grid.refresh();
|
||||
},
|
||||
|
||||
cancel_stock_reservation_entries(frm) {
|
||||
@@ -195,7 +258,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
freeze_message: __('Unreserving Stock...'),
|
||||
callback: (r) => {
|
||||
frm.doc.__onload.has_reserved_stock = false;
|
||||
frm.refresh();
|
||||
frm.reload_doc();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ class SalesOrder(SellingController):
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_reservation_entries(self, notify=True):
|
||||
def create_stock_reservation_entries(self, items_details=None, notify=True):
|
||||
"""Creates Stock Reservation Entries for Sales Order Items."""
|
||||
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
@@ -532,9 +532,18 @@ class SalesOrder(SellingController):
|
||||
"Stock Settings", "allow_partial_reservation"
|
||||
)
|
||||
|
||||
items = []
|
||||
if items_details:
|
||||
for item in items_details:
|
||||
so_item = frappe.get_doc("Sales Order Item", item["name"])
|
||||
so_item.reserve_stock = 1
|
||||
so_item.warehouse = item["warehouse"]
|
||||
so_item.qty_to_reserve = flt(item["qty_to_reserve"]) * flt(so_item.conversion_factor)
|
||||
items.append(so_item)
|
||||
|
||||
sre_count = 0
|
||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
|
||||
for item in self.get("items"):
|
||||
for item in items or self.get("items"):
|
||||
# Skip if `Reserved Stock` is not checked for the item.
|
||||
if not item.get("reserve_stock"):
|
||||
continue
|
||||
@@ -551,15 +560,27 @@ class SalesOrder(SellingController):
|
||||
item.db_set("reserve_stock", 0)
|
||||
continue
|
||||
|
||||
# Skip if Group Warehouse.
|
||||
if frappe.get_cached_value("Warehouse", item.warehouse, "is_group"):
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Stock cannot be reserved in group warehouse {1}.").format(
|
||||
item.idx, frappe.bold(item.warehouse)
|
||||
),
|
||||
title=_("Stock Reservation"),
|
||||
indicator="yellow",
|
||||
)
|
||||
continue
|
||||
|
||||
unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
|
||||
|
||||
# Stock is already reserved for the item, notify the user and skip the item.
|
||||
if unreserved_qty <= 0:
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Stock is already reserved for the Item {1} in Warehouse {2}.").format(
|
||||
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
|
||||
_("Row #{0}: Stock is already reserved for the Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
title=_("Stock Reservation"),
|
||||
indicator="yellow",
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -579,17 +600,31 @@ class SalesOrder(SellingController):
|
||||
# The quantity which can be reserved.
|
||||
qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve)
|
||||
|
||||
if hasattr(item, "qty_to_reserve"):
|
||||
if item.qty_to_reserve <= 0:
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
title=_("Stock Reservation"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
qty_to_be_reserved = min(qty_to_be_reserved, item.qty_to_reserve)
|
||||
|
||||
# Partial Reservation
|
||||
if qty_to_be_reserved < unreserved_qty:
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
||||
item.idx,
|
||||
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
||||
frappe.bold(item.item_code),
|
||||
),
|
||||
title=_("Stock Reservation"),
|
||||
indicator="orange",
|
||||
)
|
||||
if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")):
|
||||
frappe.msgprint(
|
||||
_("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
||||
item.idx,
|
||||
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
||||
frappe.bold(item.item_code),
|
||||
),
|
||||
title=_("Stock Reservation"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
# Skip the item if `Partial Reservation` is disabled in the Stock Settings.
|
||||
if not allow_partial_reservation:
|
||||
@@ -620,7 +655,7 @@ class SalesOrder(SellingController):
|
||||
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
||||
"""Returns the unreserved quantity for the Sales Order Item."""
|
||||
|
||||
existing_reserved_qty = reserved_qty_details.get((item.name, item.warehouse), 0)
|
||||
existing_reserved_qty = reserved_qty_details.get(item.name, 0)
|
||||
return (
|
||||
item.stock_qty
|
||||
- flt(item.delivered_qty) * item.get("conversion_factor", 1)
|
||||
|
||||
@@ -1939,7 +1939,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
|
||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
|
||||
for item in so.items:
|
||||
reserved_qty = reserved_qty_details[(item.name, item.warehouse)]
|
||||
reserved_qty = reserved_qty_details[item.name]
|
||||
self.assertEqual(item.stock_reserved_qty, reserved_qty)
|
||||
self.assertEqual(item.stock_qty, item.stock_reserved_qty)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user