fix: stock reservation validation in the stock entry (#47524)

This commit is contained in:
rohitwaghchaure
2025-05-13 21:13:42 +05:30
committed by GitHub
parent f1159b6ea6
commit dfc4aa9a57
8 changed files with 99 additions and 51 deletions

View File

@@ -207,9 +207,9 @@ frappe.ui.form.on("Production Plan", {
set_field_options("projected_qty_formula", projected_qty_formula); set_field_options("projected_qty_formula", projected_qty_formula);
}, },
has_unreserved_stock(frm, table) { has_unreserved_stock(frm, table, qty_field = "required_qty") {
let has_unreserved_stock = frm.doc[table].some( let has_unreserved_stock = frm.doc[table].some(
(item) => flt(item.qty) > flt(item.stock_reserved_qty) (item) => flt(item[qty_field]) > flt(item.stock_reserved_qty)
); );
return has_unreserved_stock; return has_unreserved_stock;
@@ -249,7 +249,7 @@ frappe.ui.form.on("Production Plan", {
setup_stock_reservation_for_raw_materials(frm) { setup_stock_reservation_for_raw_materials(frm) {
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
if (frm.events.has_unreserved_stock(frm, "mr_items")) { if (frm.events.has_unreserved_stock(frm, "mr_items", "required_bom_qty")) {
frm.add_custom_button( frm.add_custom_button(
__("Reserve for Raw Materials"), __("Reserve for Raw Materials"),
() => erpnext.stock_reservation.make_entries(frm, "mr_items"), () => erpnext.stock_reservation.make_entries(frm, "mr_items"),

View File

@@ -1619,18 +1619,20 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
if bom_no: if bom_no:
if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"): if (
item_details = {} data.get("include_exploded_items")
if doc.get("sub_assembly_items"): and doc.get("skip_available_sub_assembly_item")
item_details = get_raw_materials_of_sub_assembly_items( and doc.get("sub_assembly_items")
so_item_details[doc.get("sales_order")].keys() if so_item_details else [], ):
item_details, item_details = get_raw_materials_of_sub_assembly_items(
company, so_item_details[doc.get("sales_order")].keys() if so_item_details else [],
bom_no, item_details,
include_non_stock_items, company,
sub_assembly_items, bom_no,
planned_qty=planned_qty, include_non_stock_items,
) sub_assembly_items,
planned_qty=planned_qty,
)
elif data.get("include_exploded_items") and include_subcontracted_items: elif data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM # fetch exploded items from BOM
@@ -2089,7 +2091,7 @@ def get_reserved_qty_for_sub_assembly(item_code, warehouse):
def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False): def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False):
if isinstance(doc, str): if isinstance(doc, str):
doc = parse_json(doc) doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name")) doc = frappe.get_doc("Production Plan", doc.get("name"))
if items and isinstance(items, str): if items and isinstance(items, str):
items = parse_json(items) items = parse_json(items)
@@ -2113,9 +2115,22 @@ def make_stock_reservation_entries(doc, items=None, table_name=None, notify=Fals
sre = StockReservation(doc, items=items, kwargs=mapper[child_table_name], notify=notify) sre = StockReservation(doc, items=items, kwargs=mapper[child_table_name], notify=notify)
if doc.docstatus == 1: if doc.docstatus == 1:
sre.make_stock_reservation_entries() sre_created = sre.make_stock_reservation_entries()
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True) if sre_created:
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
elif doc.docstatus == 2: elif doc.docstatus == 2:
sre.cancel_stock_reservation_entries() sre.cancel_stock_reservation_entries()
doc.reload() doc.reload()
@frappe.whitelist()
def cancel_stock_reservation_entries(doc, sre_list):
if isinstance(doc, str):
doc = parse_json(doc)
doc = frappe.get_doc("Production Plan", doc.get("name"))
sre = StockReservation(doc)
sre.cancel_stock_reservation_entries(sre_list)
doc.reload()

View File

@@ -1415,8 +1415,12 @@ class TestProductionPlan(IntegrationTestCase):
do_not_submit=1, do_not_submit=1,
skip_available_sub_assembly_item=1, skip_available_sub_assembly_item=1,
warehouse="_Test Warehouse - _TC", warehouse="_Test Warehouse - _TC",
sub_assembly_warehouse="_Test Warehouse - _TC",
) )
plan.get_sub_assembly_items()
plan.save()
items = get_items_for_material_requests( items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}] plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
) )

View File

@@ -1426,7 +1426,7 @@ class WorkOrder(Document):
return return
item_list = list(items.values()) item_list = list(items.values())
make_stock_reservation_entries(self, item_list, notify=True) make_stock_reservation_entries(self, item_list, is_transfer=False, notify=True)
def get_list_of_materials_for_reservation(self, stock_entry): def get_list_of_materials_for_reservation(self, stock_entry):
items = frappe._dict() items = frappe._dict()
@@ -1648,7 +1648,7 @@ class WorkOrder(Document):
@frappe.whitelist() @frappe.whitelist()
def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False): def make_stock_reservation_entries(doc, items=None, table_name=None, is_transfer=True, notify=False):
if isinstance(doc, str): if isinstance(doc, str):
doc = parse_json(doc) doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name")) doc = frappe.get_doc("Work Order", doc.get("name"))
@@ -1658,14 +1658,15 @@ def make_stock_reservation_entries(doc, items=None, table_name=None, notify=Fals
sre = StockReservation(doc, items=items, notify=notify) sre = StockReservation(doc, items=items, notify=notify)
if doc.docstatus == 1: if doc.docstatus == 1:
if doc.production_plan: if doc.production_plan and is_transfer:
sre.transfer_reservation_entries_to( sre.transfer_reservation_entries_to(
doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order" doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order"
) )
else: else:
sre.make_stock_reservation_entries() sre_created = sre.make_stock_reservation_entries()
if sre_created:
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
elif doc.docstatus == 2: elif doc.docstatus == 2:
sre.cancel_stock_reservation_entries() sre.cancel_stock_reservation_entries()

View File

@@ -30,15 +30,15 @@ $.extend(erpnext.stock_reservation, {
params["qty_field"] = { params["qty_field"] = {
"Sales Order": "stock_qty", "Sales Order": "stock_qty",
"Work Order": "required_qty", "Work Order": "required_qty",
"Production Plan": "required_qty",
}[frm.doc.doctype]; }[frm.doc.doctype];
if (frm.doc.doctype === "Production Plan") { if (frm.doc.doctype === "Production Plan") {
if (table_name === "sub_assembly_items") { if (table_name === "sub_assembly_items") {
params["qty_field"] = "qty";
params["item_code_field"] = "production_item"; params["item_code_field"] = "production_item";
params["warehouse_field"] = "fg_warehouse"; params["warehouse_field"] = "fg_warehouse";
} else { } else {
params["qty_field"] = "quantity"; params["qty_field"] = "required_bom_qty";
} }
} }
@@ -50,6 +50,8 @@ $.extend(erpnext.stock_reservation, {
params["method"] = { params["method"] = {
"Sales Order": "delivered_qty", "Sales Order": "delivered_qty",
"Production Plan":
"erpnext.manufacturing.doctype.production_plan.production_plan.make_stock_reservation_entries",
"Work Order": "Work Order":
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_reservation_entries", "erpnext.manufacturing.doctype.work_order.work_order.make_stock_reservation_entries",
}[frm.doc.doctype]; }[frm.doc.doctype];
@@ -141,6 +143,10 @@ $.extend(erpnext.stock_reservation, {
}, },
render_items(frm, parms) { render_items(frm, parms) {
if (!frm.doc.reserve_stock) {
return;
}
let dialog = erpnext.stock_reservation.dialog; let dialog = erpnext.stock_reservation.dialog;
let field = frappe.scrub(parms.child_doctype); let field = frappe.scrub(parms.child_doctype);
@@ -155,25 +161,23 @@ $.extend(erpnext.stock_reservation, {
let warehouse_field = parms.warehouse_field || "warehouse"; let warehouse_field = parms.warehouse_field || "warehouse";
frm.doc[parms.table_name].forEach((item) => { frm.doc[parms.table_name].forEach((item) => {
if (frm.doc.reserve_stock) { let unreserved_qty =
let unreserved_qty = (flt(item[qty_field]) -
(flt(item[qty_field]) - (item.stock_reserved_qty
(item.stock_reserved_qty ? flt(item.stock_reserved_qty)
? flt(item.stock_reserved_qty) : flt(item[dispatch_qty_field]) * flt(item.conversion_factor || 1))) /
: flt(item[dispatch_qty_field]) * flt(item.conversion_factor || 1))) / flt(item.conversion_factor || 1);
flt(item.conversion_factor || 1);
if (unreserved_qty > 0) { if (unreserved_qty > 0) {
let args = { let args = {
__checked: 1, __checked: 1,
item_code: item[item_code_field] || item.item_code, item_code: item[item_code_field] || item.item_code,
warehouse: item[warehouse_field] || item.warehouse || item.source_warehouse, warehouse: item[warehouse_field] || item.warehouse || item.source_warehouse,
}; };
args[field] = item.name; args[field] = item.name;
args[qty_field] = unreserved_qty; args[qty_field] = unreserved_qty;
dialog.fields_dict.items.df.data.push(args); dialog.fields_dict.items.df.data.push(args);
}
} }
}); });
@@ -257,11 +261,17 @@ $.extend(erpnext.stock_reservation, {
}, },
cancel_stock_reservation(dialog, frm) { cancel_stock_reservation(dialog, frm) {
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() }; let data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
let method = "erpnext.manufacturing.doctype.work_order.work_order.cancel_stock_reservation_entries";
if (frm.doc.doctype === "Production Plan") {
method =
"erpnext.manufacturing.doctype.production_plan.production_plan.cancel_stock_reservation_entries";
}
if (data.sr_entries?.length > 0) { if (data.sr_entries?.length > 0) {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.cancel_stock_reservation_entries", method: method,
args: { args: {
doc: frm.doc, doc: frm.doc,
sre_list: data.sr_entries.map((item) => item.sre), sre_list: data.sr_entries.map((item) => item.sre),

View File

@@ -16,11 +16,11 @@ frappe.ui.form.on("Item", {
let msg = __( let msg = __(
"Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances." "Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances."
); );
msg += "<br>"; msg += "<br><br>";
msg += __( msg += __(
"Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item." "Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item."
); );
msg += "<br>"; msg += "<br><br>";
msg += __("Do you want to change valuation method?"); msg += __("Do you want to change valuation method?");
frappe.confirm( frappe.confirm(

View File

@@ -1908,19 +1908,33 @@ def get_reserved_voucher_details(kwargs):
value = { value = {
"Delivery Note": ["Delivery Note Item", "against_sales_order"], "Delivery Note": ["Delivery Note Item", "against_sales_order"],
}.get(kwargs.get("voucher_type")) "Stock Entry": ["Stock Entry", "work_order"],
"Work Order": ["Work Order", "production_plan"],
}.get(kwargs.get("sabb_voucher_type"))
if not value or not kwargs.get("sabb_voucher_no"): if not value or not kwargs.get("sabb_voucher_no"):
return reserved_voucher_details return reserved_voucher_details
reserved_voucher_details = frappe.get_all( voucher_based_filters = {
value[0], "Delivery Note": {
pluck=value[1],
filters={
"name": kwargs.get("sabb_voucher_detail_no"), "name": kwargs.get("sabb_voucher_detail_no"),
"parent": kwargs.get("sabb_voucher_no"), "parent": kwargs.get("sabb_voucher_no"),
"docstatus": 1, "docstatus": 1,
}, },
"Stock Entry": {
"name": kwargs.get("sabb_voucher_no"),
"docstatus": 1,
},
"Work Order": {
"name": kwargs.get("sabb_voucher_no"),
"docstatus": 1,
},
}.get(kwargs.get("sabb_voucher_type"))
reserved_voucher_details = frappe.get_all(
value[0],
pluck=value[1],
filters=voucher_based_filters,
) )
return reserved_voucher_details return reserved_voucher_details

View File

@@ -1044,6 +1044,7 @@ class StockReservation:
child_doctype = frappe.scrub(self.doc.doctype + " Item") child_doctype = frappe.scrub(self.doc.doctype + " Item")
is_sre_created = False
for item in items: for item in items:
sre = frappe.new_doc("Stock Reservation Entry") sre = frappe.new_doc("Stock Reservation Entry")
if isinstance(item, dict): if isinstance(item, dict):
@@ -1106,6 +1107,9 @@ class StockReservation:
self.set_serial_batch(sre, item.serial_and_batch_bundles) self.set_serial_batch(sre, item.serial_and_batch_bundles)
sre.submit() sre.submit()
is_sre_created = True
return is_sre_created
def set_serial_batch(self, sre, serial_batch_bundles): def set_serial_batch(self, sre, serial_batch_bundles):
bundle_details = frappe.get_all( bundle_details = frappe.get_all(