mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 17:04:47 +00:00
* feat: subcontracting inward * feat: stock reservation * feat: subcontracting delivery * feat: all remaining stuff * fix: linter errors * fix: patch * fix: modify stock entry type validation * fix: customer provided item cost field mandatory validation * fix: failing tests * fix: failing tests * fix: subcontracting controlller * refactor: semi final * refactor: final * chore: resolve conflicts * refactor: changes requested * fix: reservation transfer of extra qty * fix: consider add cost for customer provided rate field * test: create test data * test: subcontracted sales order (partial) * test: fin * fix: do not add self RM in DN created from SI * fix: failing test case * fix: conflicting function name * refactor: final changes * fix: more bugs * perf: various and major performance improvements * fix: consider warehouse as well in all queries * fix: same item code with diff warehouse in manufacture entry * refactor: readability * fix: frontend validations * perf: replace query inside loop with single query * fix: set additional item flag to true when extra customer provided item is received * fix: bugs found by coderabbit * fix: more coderabbit bugs * fix: add validation to disallow cancellation of manufacturing entry * perf: use cached values wherever it makes sense * test: fix redundant insert to child tables * fix: consider SI return of billed self RM * fix: bug found by coderabbit --------- Co-authored-by: Mihir Kandoi <mihirkandoi@Mihirs-MacBook-Air.local>
366 lines
9.1 KiB
JavaScript
366 lines
9.1 KiB
JavaScript
frappe.provide("erpnext.stock_reservation");
|
|
|
|
$.extend(erpnext.stock_reservation, {
|
|
make_entries(frm, table_name) {
|
|
erpnext.stock_reservation.setup(frm, table_name);
|
|
},
|
|
|
|
setup(frm, table_name) {
|
|
let parms = erpnext.stock_reservation.get_parms(frm, table_name);
|
|
|
|
erpnext.stock_reservation.dialog = new frappe.ui.Dialog({
|
|
title: __("Stock Reservation"),
|
|
size: "extra-large",
|
|
fields: erpnext.stock_reservation.get_dialog_fields(frm, parms),
|
|
primary_action_label: __("Reserve Stock"),
|
|
primary_action: () => {
|
|
erpnext.stock_reservation.reserve_stock(frm, table_name, parms);
|
|
},
|
|
});
|
|
|
|
erpnext.stock_reservation.render_items(frm, parms);
|
|
},
|
|
|
|
get_parms(frm, table_name) {
|
|
let params = {
|
|
table_name: table_name || "items",
|
|
child_doctype: frm.doc.doctype + " Item",
|
|
};
|
|
|
|
params["qty_field"] = {
|
|
"Sales Order": "stock_qty",
|
|
"Work Order": "required_qty",
|
|
"Production Plan": "required_qty",
|
|
}[frm.doc.doctype];
|
|
|
|
if (frm.doc.doctype === "Production Plan") {
|
|
if (table_name === "sub_assembly_items") {
|
|
params["item_code_field"] = "production_item";
|
|
params["warehouse_field"] = "fg_warehouse";
|
|
} else {
|
|
params["qty_field"] = "required_bom_qty";
|
|
}
|
|
}
|
|
|
|
params["dispatch_qty_field"] = {
|
|
"Sales Order": "delivered_qty",
|
|
"Work Order": "transferred_qty",
|
|
"Production Plan": "delivered_qty",
|
|
}[frm.doc.doctype];
|
|
|
|
params["method"] = {
|
|
"Sales Order": "delivered_qty",
|
|
"Production Plan":
|
|
"erpnext.manufacturing.doctype.production_plan.production_plan.make_stock_reservation_entries",
|
|
"Work Order":
|
|
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_reservation_entries",
|
|
}[frm.doc.doctype];
|
|
|
|
return params;
|
|
},
|
|
|
|
get_dialog_fields(frm, parms) {
|
|
let fields = erpnext.stock_reservation.fields || [];
|
|
let qty_field = parms.qty_field;
|
|
let dialog = erpnext.stock_reservation.dialog;
|
|
|
|
let table_fields = [
|
|
{ fieldtype: "Section Break" },
|
|
{
|
|
fieldname: "items",
|
|
fieldtype: "Table",
|
|
label: __("Items to Reserve"),
|
|
allow_bulk_edit: false,
|
|
cannot_add_rows: true,
|
|
cannot_delete_rows: true,
|
|
data: [],
|
|
fields: [
|
|
{
|
|
fieldname: frappe.scrub(parms.child_doctype),
|
|
fieldtype: "Link",
|
|
label: __(parms.child_doctype),
|
|
options: parms.child_doctype,
|
|
reqd: 1,
|
|
in_list_view: 1,
|
|
get_query: () => {
|
|
return {
|
|
query: "erpnext.controllers.queries.get_filtered_child_rows",
|
|
filters: {
|
|
parenttype: frm.doc.doctype,
|
|
parent: frm.doc.name,
|
|
reserve_stock: 1,
|
|
},
|
|
};
|
|
},
|
|
onchange: (event) => {
|
|
if (event) {
|
|
let name = $(event.currentTarget).closest(".grid-row").attr("data-name");
|
|
let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
|
|
|
|
frm.doc.items.forEach((item) => {
|
|
if (item.name === item_row.sales_order_item) {
|
|
item_row.item_code = item.item_code;
|
|
}
|
|
});
|
|
dialog.fields_dict.items.grid.refresh();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
fieldname: "item_code",
|
|
fieldtype: "Link",
|
|
label: __("Item Code"),
|
|
options: "Item",
|
|
reqd: 1,
|
|
read_only: 1,
|
|
in_list_view: 1,
|
|
},
|
|
{
|
|
fieldname: "warehouse",
|
|
fieldtype: "Link",
|
|
label: __("Warehouse"),
|
|
options: "Warehouse",
|
|
reqd: 1,
|
|
in_list_view: 1,
|
|
get_query: () => {
|
|
return {
|
|
filters: [["Warehouse", "is_group", "!=", 1]],
|
|
};
|
|
},
|
|
},
|
|
{
|
|
fieldname: qty_field,
|
|
fieldtype: "Float",
|
|
label: __("Qty"),
|
|
reqd: 1,
|
|
in_list_view: 1,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
return fields.concat(table_fields);
|
|
},
|
|
|
|
render_items(frm, parms) {
|
|
if (!frm.doc.reserve_stock) {
|
|
return;
|
|
}
|
|
|
|
let dialog = erpnext.stock_reservation.dialog;
|
|
let field = frappe.scrub(parms.child_doctype);
|
|
|
|
let qty_field = parms.qty_field;
|
|
let dispatch_qty_field = parms.dispatch_qty_field;
|
|
|
|
if (frm.doc.doctype === "Work Order" && frm.doc.skip_transfer) {
|
|
dispatch_qty_field = "consumed_qty";
|
|
}
|
|
|
|
let item_code_field = parms.item_code_field || "item_code";
|
|
let warehouse_field = parms.warehouse_field || "warehouse";
|
|
|
|
frm.doc[parms.table_name].forEach((item) => {
|
|
let unreserved_qty =
|
|
(flt(item[qty_field]) -
|
|
(item.stock_reserved_qty
|
|
? flt(item.stock_reserved_qty)
|
|
: flt(item[dispatch_qty_field]) * flt(item.conversion_factor || 1))) /
|
|
flt(item.conversion_factor || 1);
|
|
|
|
if (unreserved_qty > 0) {
|
|
let args = {
|
|
__checked: 1,
|
|
item_code: item[item_code_field] || item.item_code,
|
|
warehouse: item[warehouse_field] || item.warehouse || item.source_warehouse,
|
|
};
|
|
|
|
args[field] = item.name;
|
|
args[qty_field] = unreserved_qty;
|
|
dialog.fields_dict.items.df.data.push(args);
|
|
}
|
|
});
|
|
|
|
dialog.fields_dict.items.grid.refresh();
|
|
dialog.show();
|
|
},
|
|
|
|
reserve_stock(frm, table_name, parms) {
|
|
let dialog = erpnext.stock_reservation.dialog;
|
|
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
|
|
|
if (data.items && data.items.length > 0) {
|
|
frappe.call({
|
|
method: parms.method,
|
|
args: {
|
|
doc: frm.doc,
|
|
items: data.items,
|
|
is_transfer: 0,
|
|
table_name: table_name,
|
|
notify: true,
|
|
},
|
|
freeze: true,
|
|
freeze_message: __("Reserving Stock..."),
|
|
callback: (r) => {
|
|
frm.doc.__onload.has_unreserved_stock = false;
|
|
frm.reload_doc();
|
|
},
|
|
});
|
|
|
|
dialog.hide();
|
|
} else {
|
|
frappe.msgprint(__("Please select items to reserve."));
|
|
}
|
|
},
|
|
|
|
unreserve_stock(frm) {
|
|
erpnext.stock_reservation.get_stock_reservation_entries(frm.doctype, frm.docname).then((r) => {
|
|
if (!r.exc && r.message) {
|
|
if (r.message.length > 0) {
|
|
erpnext.stock_reservation.prepare_for_cancel_sre_entries(frm, r.message);
|
|
} else {
|
|
frappe.msgprint(__("No reserved stock to unreserve."));
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
prepare_for_cancel_sre_entries(frm, sre_entries) {
|
|
const dialog = new frappe.ui.Dialog({
|
|
title: __("Stock Unreservation"),
|
|
size: "extra-large",
|
|
fields: [
|
|
{
|
|
fieldname: "sr_entries",
|
|
fieldtype: "Table",
|
|
label: __("Reserved Stock"),
|
|
allow_bulk_edit: false,
|
|
cannot_add_rows: true,
|
|
cannot_delete_rows: true,
|
|
in_place_edit: true,
|
|
data: [],
|
|
fields: erpnext.stock_reservation.get_fields_for_cancel(),
|
|
},
|
|
],
|
|
primary_action_label: __("Unreserve Stock"),
|
|
primary_action: () => {
|
|
erpnext.stock_reservation.cancel_stock_reservation(dialog, frm);
|
|
},
|
|
});
|
|
|
|
sre_entries.forEach((sre) => {
|
|
dialog.fields_dict.sr_entries.df.data.push({
|
|
sre: sre.name,
|
|
item_code: sre.item_code,
|
|
warehouse: sre.warehouse,
|
|
qty: flt(sre.reserved_qty) - flt(sre.delivered_qty),
|
|
});
|
|
});
|
|
|
|
dialog.fields_dict.sr_entries.grid.refresh();
|
|
dialog.show();
|
|
},
|
|
|
|
cancel_stock_reservation(dialog, frm) {
|
|
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) {
|
|
frappe.call({
|
|
method: method,
|
|
args: {
|
|
doc: frm.doc,
|
|
sre_list: data.sr_entries.map((item) => item.sre),
|
|
},
|
|
freeze: true,
|
|
freeze_message: __("Unreserving Stock..."),
|
|
callback: (r) => {
|
|
frm.doc.__onload.has_reserved_stock = false;
|
|
frm.reload_doc();
|
|
},
|
|
});
|
|
|
|
dialog.hide();
|
|
} else {
|
|
frappe.msgprint(__("Please select items to unreserve."));
|
|
}
|
|
},
|
|
|
|
get_stock_reservation_entries(voucher_type, voucher_no) {
|
|
return frappe.call({
|
|
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.get_stock_reservation_entries_for_voucher",
|
|
args: {
|
|
voucher_type: voucher_type,
|
|
voucher_no: voucher_no,
|
|
},
|
|
});
|
|
},
|
|
|
|
get_fields_for_cancel() {
|
|
return [
|
|
{
|
|
fieldname: "sre",
|
|
fieldtype: "Link",
|
|
label: __("Stock Reservation Entry"),
|
|
options: "Stock Reservation Entry",
|
|
reqd: 1,
|
|
read_only: 1,
|
|
in_list_view: 1,
|
|
},
|
|
{
|
|
fieldname: "item_code",
|
|
fieldtype: "Link",
|
|
label: __("Item Code"),
|
|
options: "Item",
|
|
reqd: 1,
|
|
read_only: 1,
|
|
in_list_view: 1,
|
|
},
|
|
{
|
|
fieldname: "warehouse",
|
|
fieldtype: "Link",
|
|
label: __("Warehouse"),
|
|
options: "Warehouse",
|
|
reqd: 1,
|
|
read_only: 1,
|
|
in_list_view: 1,
|
|
},
|
|
{
|
|
fieldname: "qty",
|
|
fieldtype: "Float",
|
|
label: __("Qty"),
|
|
reqd: 1,
|
|
read_only: 1,
|
|
in_list_view: 1,
|
|
},
|
|
];
|
|
},
|
|
|
|
show_reserved_stock(frm, table_name) {
|
|
if (!table_name) {
|
|
table_name = "items";
|
|
}
|
|
|
|
// Get the latest modified date from the items table.
|
|
var to_date = moment(
|
|
new Date(Math.max(...frm.doc[table_name].map((e) => new Date(e.modified))))
|
|
).format("YYYY-MM-DD");
|
|
|
|
let from_date = frm.doc.transaction_date || new Date(frm.doc.creation);
|
|
|
|
frappe.route_options = {
|
|
company: frm.doc.company,
|
|
from_date: from_date,
|
|
to_date: to_date,
|
|
voucher_type: frm.doc.doctype,
|
|
voucher_no: frm.doc.name,
|
|
};
|
|
frappe.set_route("query-report", "Reserved Stock");
|
|
},
|
|
});
|