diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index f27197d09f..f93b244a50 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -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", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index b49b0ba0f7..64c952b67b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -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) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 77ca6b63d3..90e8b22ed9 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -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", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c3f27cd3de..560286e68a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -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 Ignore Existing Projected Quantity 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 Ignore Existing Projected Quantity 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) diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/__init__.py b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js new file mode 100644 index 0000000000..53f87582d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.js @@ -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) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.json b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.json new file mode 100644 index 0000000000..53e33c0265 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.py b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.py new file mode 100644 index 0000000000..f605985ae9 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/production_plan_material_request_warehouse.py @@ -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 diff --git a/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/test_production_plan_material_request_warehouse.py b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/test_production_plan_material_request_warehouse.py new file mode 100644 index 0000000000..ecab5fbae1 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_material_request_warehouse/test_production_plan_material_request_warehouse.py @@ -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 diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index eb298a60aa..db8bffda9d 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -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); diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 536f5fa0ac..d1f29e364a 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -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", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 739d7492ca..97606f4e3a 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -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: diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 2bdc268c78..df140ffd75 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -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", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 616de5e00a..231af1a022 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -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))) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f9aae7baa8..ddf4ec0393 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -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: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c5ba686f89..d50712aee7 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -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