feat: Run MRP at parent level in the production plan (#21545)

* enhance: provision to make material request from other location

* fix: source warehouse for material transfer from production plan

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
This commit is contained in:
Nabin Hait 2020-05-01 18:16:25 +05:30 committed by GitHub
parent 39805918d4
commit da17f9cb05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 292 additions and 38 deletions

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2017-12-01 12:12:55.048691",
"doctype": "DocType",
"editable_grid": 1,
@ -6,8 +7,9 @@
"field_order": [
"item_code",
"item_name",
"warehouse",
"material_request_type",
"from_warehouse",
"warehouse",
"column_break_4",
"quantity",
"uom",
@ -46,6 +48,7 @@
{
"fieldname": "material_request_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Material Request Type",
"options": "\nPurchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided"
},
@ -64,11 +67,11 @@
{
"fieldname": "projected_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Projected Qty",
"read_only": 1
},
{
"default": "0",
"fieldname": "actual_qty",
"fieldtype": "Float",
"in_list_view": 1,
@ -119,10 +122,18 @@
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"depends_on": "eval:doc.material_request_type == 'Material Transfer'",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"label": "From Warehouse",
"options": "Warehouse"
}
],
"istable": 1,
"modified": "2019-11-08 15:15:43.979360",
"links": [],
"modified": "2020-02-03 12:22:29.913302",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",

View File

@ -19,7 +19,8 @@ frappe.ui.form.on('Production Plan', {
frm.set_query('for_warehouse', function(doc) {
return {
filters: {
company: doc.company
company: doc.company,
is_group: 0
}
}
});
@ -188,12 +189,53 @@ frappe.ui.form.on('Production Plan', {
},
get_items_for_mr: function(frm) {
const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom',
if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests"));
}
if (frm.doc.ignore_existing_ordered_qty) {
frm.events.get_items_for_material_requests(frm);
} else {
const title = __("Transfer Materials For Warehouse {0}", [frm.doc.for_warehouse]);
var dialog = new frappe.ui.Dialog({
title: title,
fields: [
{
"fieldtype": "Table MultiSelect", "label": __("Source Warehouses"),
"fieldname": "warehouses", "options": "Production Plan Material Request Warehouse",
"description": "System will pickup the materials from the selected warehouses",
get_query: function () {
return {
filters: {
company: frm.doc.company
}
};
},
},
]
});
dialog.show();
dialog.set_primary_action(__("Get Items"), () => {
let warehouses = dialog.get_values().warehouses;
frm.events.get_items_for_material_requests(frm, warehouses);
dialog.hide();
});
}
},
get_items_for_material_requests: function(frm, warehouses) {
const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
freeze: true,
args: {doc: frm.doc},
args: {
doc: frm.doc,
warehouses: warehouses || []
},
callback: function(r) {
if(r.message) {
frm.set_value('mr_items', []);
@ -212,14 +254,14 @@ frappe.ui.form.on('Production Plan', {
},
for_warehouse: function(frm) {
if (frm.doc.mr_items) {
if (frm.doc.mr_items && frm.doc.for_warehouse) {
frm.trigger("get_items_for_mr");
}
},
download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
open_url_post(frappe.request.url, { cmd: get_template_url, production_plan: frm.doc.name });
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc });
},
show_progress: function(frm) {

View File

@ -43,6 +43,7 @@
"total_produced_qty",
"column_break_32",
"status",
"warehouses",
"amended_from"
],
"fields": [
@ -218,12 +219,6 @@
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "for_warehouse",
"fieldtype": "Link",
"label": "For Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "download_materials_required",
@ -292,12 +287,26 @@
"options": "Production Plan",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "for_warehouse",
"fieldtype": "Link",
"label": "Material Request Warehouse",
"options": "Warehouse"
},
{
"fieldname": "warehouses",
"fieldtype": "Table MultiSelect",
"hidden": 1,
"label": "Warehouses",
"options": "Production Plan Material Request Warehouse",
"read_only": 1
}
],
"icon": "fa fa-calendar",
"is_submittable": 1,
"links": [],
"modified": "2020-01-21 19:13:10.113854",
"modified": "2020-02-03 00:25:25.934202",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@ -3,7 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
import frappe, json, copy
from frappe import msgprint, _
from six import string_types, iteritems
@ -385,6 +385,7 @@ class ProductionPlan(Document):
# add item
material_request.append("items", {
"item_code": item.item_code,
"from_warehouse": item.from_warehouse,
"qty": item.quantity,
"schedule_date": schedule_date,
"warehouse": item.warehouse,
@ -415,19 +416,18 @@ class ProductionPlan(Document):
msgprint(_("No material request created"))
@frappe.whitelist()
def download_raw_materials(production_plan):
doc = frappe.get_doc('Production Plan', production_plan)
doc.check_permission()
def download_raw_materials(doc):
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse',
'projected Qty', 'Actual Qty']]
doc = doc.as_dict()
for d in get_items_for_material_requests(doc, ignore_existing_ordered_qty=True):
for d in get_items_for_material_requests(doc):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'),
d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')])
if not doc.for_warehouse:
if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')}
for bin_dict in get_bin_details(row, doc.company, all_warehouse=True):
if d.get("warehouse") == bin_dict.get('warehouse'):
@ -610,26 +610,43 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
@frappe.whitelist()
def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None):
def get_items_for_material_requests(doc, warehouses=None):
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
warehouse_list = []
if warehouses:
if isinstance(warehouses, string_types):
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
if warehouse_list:
warehouses = list(set(warehouse_list))
if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None
doc['mr_items'] = []
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
if not po_items:
frappe.throw(_("Items are required to pull the raw materials which is associated with it."))
company = doc.get('company')
warehouse = doc.get('for_warehouse')
if not ignore_existing_ordered_qty:
ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
so_item_details = frappe._dict()
for data in po_items:
planned_qty = data.get('required_qty') or data.get('planned_qty')
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
warehouse = data.get("warehouse") or warehouse
warehouse = doc.get('for_warehouse')
item_details = {}
if data.get("bom") or data.get("bom_no"):
@ -700,12 +717,51 @@ def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None):
if items:
mr_items.append(items)
if not ignore_existing_ordered_qty and warehouses:
new_mr_items = []
for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
mr_items = new_mr_items
if not mr_items:
frappe.msgprint(_("""As raw materials projected quantity is more than required quantity, there is no need to create material request.
Still if you want to make material request, kindly enable <b>Ignore Existing Projected Quantity</b> checkbox"""))
frappe.msgprint(_("""As raw materials projected quantity is more than required quantity,
there is no need to create material request for the warehouse {0}.
Still if you want to make material request,
kindly enable <b>Ignore Existing Projected Quantity</b> checkbox""").format(doc.get('for_warehouse')))
return mr_items
def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations
locations = get_available_item_locations(item.get("item_code"),
warehouses, item.get("quantity"), company, ignore_validation=True)
if not locations:
new_mr_items.append(item)
return
required_qty = item.get("quantity")
for d in locations:
if required_qty <=0: return
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
if required_qty > 0:
new_dict.update({
"quantity": quantity,
"material_request_type": "Material Transfer",
"from_warehouse": d.get("warehouse")
})
required_qty -= quantity
new_mr_items.append(new_dict)
if required_qty:
item["quantity"] = required_qty
new_mr_items.append(item)
@frappe.whitelist()
def get_item_data(item_code):
item_details = get_item_details(item_code)

View File

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Production Plan Material Request Warehouse', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,42 @@
{
"actions": [],
"creation": "2020-02-02 10:37:16.650836",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"warehouse"
],
"fields": [
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
}
],
"links": [],
"modified": "2020-02-02 10:37:16.650836",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Material Request Warehouse",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ProductionPlanMaterialRequestWarehouse(Document):
pass

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestProductionPlanMaterialRequestWarehouse(unittest.TestCase):
pass

View File

@ -20,6 +20,17 @@ frappe.ui.form.on('Material Request', {
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.ordered_qty) ? "green" : "orange"; });
frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query"
};
});
frm.set_query("from_warehouse", "items", function(doc) {
return {
filters: {'company': doc.company}
};
})
},
onload: function(frm) {
@ -53,6 +64,16 @@ frappe.ui.form.on('Material Request', {
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
},
set_from_warehouse: function(frm) {
if (frm.doc.material_request_type == "Material Transfer"
&& frm.doc.set_from_warehouse) {
frm.doc.items.forEach(d => {
frappe.model.set_value(d.doctype, d.name,
"from_warehouse", frm.doc.set_from_warehouse);
})
}
},
make_custom_buttons: function(frm) {
if (frm.doc.docstatus==0) {
frm.add_custom_button(__("Bill of Materials"),
@ -159,6 +180,7 @@ frappe.ui.form.on('Material Request', {
args: {
args: {
item_code: item.item_code,
from_warehouse: item.from_warehouse,
warehouse: item.warehouse,
doctype: frm.doc.doctype,
buying_price_list: frappe.defaults.get_default('buying_price_list'),
@ -176,9 +198,11 @@ frappe.ui.form.on('Material Request', {
},
callback: function(r) {
const d = item;
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
if(!r.exc) {
$.each(r.message, function(k, v) {
if(!d[k]) d[k] = v;
if(!d[k] || in_list(qty_fields, k)) d[k] = v;
});
}
}
@ -324,6 +348,16 @@ frappe.ui.form.on("Material Request Item", {
frm.events.get_item_data(frm, item);
},
from_warehouse: function(frm, doctype, name) {
const item = locals[doctype][name];
frm.events.get_item_data(frm, item);
},
warehouse: function(frm, doctype, name) {
const item = locals[doctype][name];
frm.events.get_item_data(frm, item);
},
rate: function(frm, doctype, name) {
const item = locals[doctype][name];
frm.events.get_item_data(frm, item);

View File

@ -18,6 +18,8 @@
"amended_from",
"warehouse_section",
"set_warehouse",
"column_break5",
"set_from_warehouse",
"items_section",
"scan_barcode",
"items",
@ -287,13 +289,27 @@
"fieldtype": "Link",
"label": "Set Warehouse",
"options": "Warehouse"
},
{
"fieldname": "column_break5",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"depends_on": "eval:doc.material_request_type == 'Material Transfer'",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"label": "Set From Warehouse",
"options": "Warehouse"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2020-03-02 20:21:09.990867",
"modified": "2020-05-01 20:21:09.990867",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@ -456,6 +456,9 @@ def make_stock_entry(source_name, target_doc=None):
if source_parent.material_request_type == "Customer Provided":
target.allow_zero_valuation_rate = 1
if source_parent.material_request_type == "Material Transfer":
target.s_warehouse = obj.from_warehouse
def set_missing_values(source, target):
target.purpose = source.material_request_type
if source.job_card:

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:02",
"doctype": "DocType",
@ -21,6 +22,7 @@
"quantity_and_warehouse",
"qty",
"stock_uom",
"from_warehouse",
"warehouse",
"col_break2",
"uom",
@ -419,12 +421,19 @@
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:parent.material_request_type == \"Material Transfer\"",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse (Material Transfer)",
"options": "Warehouse"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-16 09:00:00.992835",
"modified": "2020-05-01 09:00:00.992835",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",

View File

@ -139,7 +139,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
item_location_map[item_doc.item_code] = available_locations
return locations
def get_available_item_locations(item_code, from_warehouses, required_qty, company):
def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False):
locations = []
if frappe.get_cached_value('Item', item_code, 'has_serial_no'):
locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company)
@ -152,7 +152,7 @@ def get_available_item_locations(item_code, from_warehouses, required_qty, compa
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0:
if remaining_qty > 0 and not ignore_validation:
frappe.msgprint(_('{0} units of {1} is not available.')
.format(remaining_qty, frappe.get_desk_link('Item', item_code)))

View File

@ -1053,9 +1053,9 @@ class StockEntry(StockController):
fields=["required_qty", "consumed_qty"]
)
req_qty = flt(req_items[0].required_qty)
req_qty = flt(req_items[0].required_qty) if req_items else flt(4)
req_qty_each = flt(req_qty / manufacturing_qty)
consumed_qty = flt(req_items[0].consumed_qty)
consumed_qty = flt(req_items[0].consumed_qty) if req_items else 0
if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)):
if qty >= req_qty:

View File

@ -77,7 +77,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args))
if out.get("warehouse"):
if (args.get("doctype") == "Material Request" and
args.get("material_request_type") == "Material Transfer"):
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
out.update(get_bin_details(args.item_code, out.warehouse))
# update args with out, if key or value not exists