mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-14 02:31:21 +00:00
fix: stock reservation validation in the stock entry (#47524)
This commit is contained in:
@@ -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"),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"}]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user