From 89974b221d733ede3e18263ab704c4f8eaff6652 Mon Sep 17 00:00:00 2001 From: Shreya Shah Date: Tue, 13 Nov 2018 11:15:03 +0530 Subject: [PATCH] [Feature] Create Raw Material Request from Sales Order (#15452) * Add new button to Sales Order form - Request for Raw Materials * Modify get_work_order_items function * Commonify functions in Production Plan to make it compatible with new feature * Create and submit Material Request from Sales Order * Link Sales Order with Material Request * Minor * Rename label * Fix Codacy * Modify as per review suggestions - Move dialog to a new function - Move checkboxes below other fields * Minor changes * Check for permissions * Add common checkboxes for all items * Fix codacy * fix: Travis * fix: Use variable to store query result * fix: Add comment before fetching exploded items * refactor: Break into multiple functions * test: Add test case --- .../production_plan/production_plan.js | 21 +- .../production_plan/production_plan.py | 221 ++++++++++-------- .../production_plan/test_production_plan.py | 5 +- .../doctype/sales_order/sales_order.js | 84 ++++++- .../doctype/sales_order/sales_order.py | 57 ++++- .../doctype/sales_order/test_sales_order.py | 65 +++++- 6 files changed, 337 insertions(+), 116 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index e74a375557..dbbf3d33f7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -104,13 +104,26 @@ frappe.ui.form.on('Production Plan', { } }); }, - + get_items_for_mr: function(frm) { frappe.call({ - method: "get_items_for_material_requests", + method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", freeze: true, - doc: frm.doc, - callback: function() { + args: {doc: frm.doc}, + callback: function(r) { + if(r.message) { + frm.set_value('mr_items', []); + $.each(r.message, function(i, d) { + var item = frm.add_child('mr_items'); + item.actual_qty = d.actual_qty; + item.item_code = d.item_code; + item.item_name = d.item_name; + item.min_order_qty = d.min_order_qty; + item.quantity = d.quantity; + item.sales_order = d.sales_order; + item.warehouse = d.warehouse; + }); + } refresh_field('mr_items'); } }); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 12f2f04e38..d6b62b302e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -10,6 +10,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from six import string_types +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): def validate(self): @@ -281,102 +282,6 @@ class ProductionPlan(Document): return item_dict - def get_items_for_material_requests(self): - self.mr_items = [] - - for data in self.po_items: - bom_wise_item_details = {} - if not data.planned_qty: - frappe.throw(_("For row {0}: Enter planned qty").format(data.idx)) - - if data.include_exploded_items and data.bom_no and self.include_subcontracted_items: - for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name, - bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, - item.default_material_request_type, item.min_order_qty, item_default.default_warehouse - from - `tabBOM Explosion Item` bei - JOIN `tabBOM` bom ON bom.name = bei.parent - JOIN `tabItem` item ON item.name = bei.item_code - LEFT JOIN `tabItem Default` item_default - ON item_default.parent = item.name and item_default.company=%s - where - bei.docstatus < 2 - and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format(0 if self.include_non_stock_items else 1), - (self.company, data.bom_no), as_dict=1): - bom_wise_item_details.setdefault(d.item_code, d) - else: - bom_wise_item_details = self.get_subitems(data, bom_wise_item_details, data.bom_no, 1) - - for item, item_details in bom_wise_item_details.items(): - if item_details.qty > 0: - self.add_item_in_material_request_items(item, item_details, data) - - def get_subitems(self, data, bom_wise_item_details, bom_no, parent_qty): - items = frappe.db.sql(""" - SELECT - bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, - item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, - item_default.default_warehouse - FROM - `tabBOM Item` bom_item - JOIN `tabBOM` bom ON bom.name = bom_item.parent - JOIN tabItem item ON bom_item.item_code = item.name - LEFT JOIN `tabItem Default` item_default - ON item.name = item_default.parent and item_default.company = %(company)s - where - bom.name = %(bom)s - and bom_item.docstatus < 2 - and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format(0 if self.include_non_stock_items else 1),{ - 'bom': bom_no, - 'parent_qty': parent_qty, - 'company': self.company - }, as_dict=1) - - for d in items: - if not data.include_exploded_items or not d.default_bom: - if d.item_code in bom_wise_item_details: - bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty - else: - bom_wise_item_details[d.item_code] = d - - if data.include_exploded_items and d.default_bom: - if ((d.default_material_request_type in ["Manufacture", "Purchase"] and - not d.is_sub_contracted) or (d.is_sub_contracted and self.include_subcontracted_items)): - if d.qty > 0: - self.get_subitems(data, bom_wise_item_details, d.default_bom, d.qty) - - return bom_wise_item_details - - def add_item_in_material_request_items(self, item, row, data): - total_qty = row.qty * data.planned_qty - projected_qty, actual_qty = get_bin_details(row) - - requested_qty = 0 - if self.ignore_existing_ordered_qty: - requested_qty = total_qty - else: - requested_qty = total_qty - projected_qty - - if requested_qty > 0 and requested_qty < row.min_order_qty: - requested_qty = row.min_order_qty - - if requested_qty > 0: - self.append('mr_items', { - 'item_code': item, - 'item_name': row.item_name, - 'quantity': requested_qty, - 'warehouse': row.source_warehouse or row.default_warehouse, - 'actual_qty': actual_qty, - 'min_order_qty': row.min_order_qty, - 'sales_order': data.sales_order - }) - def make_work_order(self): wo_list = [] self.validate_data() @@ -466,6 +371,87 @@ class ProductionPlan(Document): else : msgprint(_("No material request created")) +def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items): + for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, + ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name, + bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, + item.default_material_request_type, item.min_order_qty, item_default.default_warehouse + from + `tabBOM Explosion Item` bei + JOIN `tabBOM` bom ON bom.name = bei.parent + JOIN `tabItem` item ON item.name = bei.item_code + LEFT JOIN `tabItem Default` item_default + ON item_default.parent = item.name and item_default.company=%s + where + bei.docstatus < 2 + and bom.name=%s and item.is_stock_item in (1, {0}) + group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1), + (company, bom_no), as_dict=1): + bom_wise_item_details.setdefault(d.get('item_code'), d) + return bom_wise_item_details + +def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, parent_qty): + items = frappe.db.sql(""" + SELECT + bom_item.item_code, default_material_request_type, item.item_name, + ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, + item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, + item.default_bom as default_bom, bom_item.description as description, + bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, + item_default.default_warehouse + FROM + `tabBOM Item` bom_item + JOIN `tabBOM` bom ON bom.name = bom_item.parent + JOIN tabItem item ON bom_item.item_code = item.name + LEFT JOIN `tabItem Default` item_default + ON item.name = item_default.parent and item_default.company = %(company)s + where + bom.name = %(bom)s + and bom_item.docstatus < 2 + and item.is_stock_item in (1, {0}) + group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{ + 'bom': bom_no, + 'parent_qty': parent_qty, + 'company': company + }, as_dict=1) + + for d in items: + if not data.get('include_exploded_items') or not d.default_bom: + if d.item_code in bom_wise_item_details: + bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty + else: + bom_wise_item_details[d.item_code] = d + + if data.get('include_exploded_items') and d.default_bom: + if ((d.default_material_request_type in ["Manufacture", "Purchase"] and + not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)): + if d.qty > 0: + get_subitems(doc, data, bom_wise_item_details, d.default_bom, company, include_non_stock_items, include_subcontracted_items, d.qty) + return bom_wise_item_details + +def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company): + total_qty = row.qty * planned_qty + projected_qty, actual_qty = get_bin_details(row) + + requested_qty = 0 + if ignore_existing_ordered_qty: + requested_qty = total_qty + else: + requested_qty = total_qty - projected_qty + if requested_qty > 0 and requested_qty < row.min_order_qty: + requested_qty = row.min_order_qty + item_group_defaults = get_item_group_defaults(item, company) + if requested_qty > 0: + doc.setdefault('mr_items', []).append({ + 'item_code': item, + 'item_name': row.item_name, + 'quantity': requested_qty, + 'warehouse': warehouse or row.source_warehouse or row.default_warehouse or item_group_defaults.get("default_warehouse"), + 'actual_qty': actual_qty, + 'min_order_qty': row.min_order_qty, + 'sales_order': data.get('sales_order') + }) + def get_sales_orders(self): so_filter = item_filter = "" if self.from_date: @@ -520,3 +506,46 @@ def get_bin_details(row): """.format(conditions=conditions), { "item_code": row.item_code }, as_list=1) return item_projected_qty and item_projected_qty[0] or (0,0) + +@frappe.whitelist() +def get_items_for_material_requests(doc, company=None): + if isinstance(doc, string_types): + doc = frappe._dict(json.loads(doc)) + + doc['mr_items'] = [] + po_items = doc['po_items'] if doc.get('po_items') else doc['items'] + + for data in po_items: + warehouse = None + bom_wise_item_details = {} + + if data.get('required_qty'): + planned_qty = data.get('required_qty') + bom_no = data.get('bom') + ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') + include_non_stock_items = 1 + warehouse = data.get('for_warehouse') + if data.get('include_exploded_items'): + include_subcontracted_items = 1 + else: + include_subcontracted_items = 0 + else: + planned_qty = data.get('planned_qty') + bom_no = data.get('bom_no') + include_subcontracted_items = doc['include_subcontracted_items'] + company = doc['company'] + include_non_stock_items = doc['include_non_stock_items'] + ignore_existing_ordered_qty = doc['ignore_existing_ordered_qty'] + if not planned_qty: + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) + + if data.get('include_exploded_items') and bom_no and include_subcontracted_items: + # fetch exploded items from BOM + bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items) + else: + bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1) + for item, item_details in bom_wise_item_details.items(): + if item_details.qty > 0: + add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company) + + return doc['mr_items'] diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 7cf426858d..a33d42b7d0 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -10,6 +10,7 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests class TestProductionPlan(unittest.TestCase): def setUp(self): @@ -160,7 +161,9 @@ def create_production_plan(**args): 'planned_start_date': args.planned_start_date or now_datetime() }] }) - pln.get_items_for_material_requests() + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append('mr_items', d) if not args.do_not_save: pln.insert() diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 5275e2e296..54d7654c2d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -150,6 +150,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( && flt(doc.per_delivered, 6) < 100) { this.frm.add_custom_button(__('Material Request'), function() { me.make_material_request() }, __("Make")); + this.frm.add_custom_button(__('Request for Raw Materials'), + function() { me.make_raw_material_request() }, __("Make")); } // make purchase order @@ -313,6 +315,86 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }) }, + make_raw_material_request: function() { + var me = this; + this.frm.call({ + doc: this.frm.doc, + method: 'get_work_order_items', + args: { + for_raw_material_request: 1 + }, + callback: function(r) { + if(!r.message) { + frappe.msgprint({ + message: __('No Items with Bill of Materials.'), + indicator: 'orange' + }); + return; + } + else { + me.make_raw_material_request_dialog(r); + } + } + }); + }, + + make_raw_material_request_dialog: function(r) { + var fields = [ + {fieldtype:'Check', fieldname:'include_exploded_items', + label: __('Include Exploded Items')}, + {fieldtype:'Check', fieldname:'ignore_existing_ordered_qty', + label: __('Ignore Existing Ordered Qty')}, + { + fieldtype:'Table', fieldname: 'items', + description: __('Select BOM, Qty and For Warehouse'), + fields: [ + {fieldtype:'Read Only', fieldname:'item_code', + label: __('Item Code'), in_list_view:1}, + {fieldtype:'Link', fieldname:'bom', options: 'BOM', reqd: 1, + label: __('BOM'), in_list_view:1, get_query: function(doc) { + return {filters: {item: doc.item_code}}; + } + }, + {fieldtype:'Float', fieldname:'required_qty', reqd: 1, + label: __('Qty'), in_list_view:1}, + {fieldtype:'Link', fieldname:'for_warehouse', options: 'Warehouse', + label: __('For Warehouse')} + ], + data: r.message, + get_data: function() { + return r.message + } + } + ] + var d = new frappe.ui.Dialog({ + title: __("Select from Items having BOM"), + fields: fields, + primary_action: function() { + var data = d.get_values(); + me.frm.call({ + method: 'erpnext.selling.doctype.sales_order.sales_order.make_raw_material_request', + args: { + items: data, + company: me.frm.doc.company, + sales_order: me.frm.docname, + project: me.frm.project + }, + freeze: true, + callback: function(r) { + if(r.message) { + frappe.msgprint(__('Material Request {0} submitted.', + ['' + r.message.name+ ''])); + } + d.hide(); + me.frm.reload_doc(); + } + }); + }, + primary_action_label: __('Make') + }); + d.show(); + }, + make_delivery_note_based_on_delivery_date: function() { var me = this; @@ -423,7 +505,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( filters: {'parent': me.frm.doc.name} } }}, - + {"fieldtype": "Button", "label": __("Make Purchase Order"), "fieldname": "make_purchase_order", "cssClass": "btn-primary"}, ] }); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a7b4a3e2c9..5f435ced74 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -5,8 +5,9 @@ from __future__ import unicode_literals import frappe import json import frappe.utils -from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate +from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate, add_days from frappe import _ +from six import string_types from frappe.model.utils import get_fetch_values from frappe.model.mapper import get_mapped_doc from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty @@ -17,6 +18,7 @@ from frappe.desk.doctype.auto_repeat.auto_repeat import get_next_schedule_date from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -366,7 +368,7 @@ class SalesOrder(SellingController): self.indicator_color = "green" self.indicator_title = _("Paid") - def get_work_order_items(self): + def get_work_order_items(self, for_raw_material_request=0): '''Returns items with BOM that already do not have a linked work order''' items = [] @@ -375,8 +377,13 @@ class SalesOrder(SellingController): bom = get_default_bom_item(i.item_code) if bom: stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty - pending_qty= stock_qty - flt(frappe.db.sql('''select sum(qty) from `tabWork Order` + if not for_raw_material_request: + total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + pending_qty = stock_qty - total_work_order_qty + else: + pending_qty = stock_qty + if pending_qty: items.append(dict( name= i.name, @@ -384,6 +391,7 @@ class SalesOrder(SellingController): bom = bom, warehouse = i.warehouse, pending_qty = pending_qty, + required_qty = pending_qty if for_raw_material_request else 0, sales_order_item = i.name )) return items @@ -846,7 +854,7 @@ def get_supplier(doctype, txt, searchfield, start, page_len, filters): or supplier_name like %(txt)s) and name in (select supplier from `tabSales Order Item` where parent = %(parent)s) and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi - on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) + on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), @@ -902,3 +910,44 @@ def get_default_bom_item(item_code): bom = bom[0].name if bom else None return bom + +@frappe.whitelist() +def make_raw_material_request(items, company, sales_order, project=None): + if not frappe.has_permission("Sales Order", "write"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + if isinstance(items, string_types): + items = frappe._dict(json.loads(items)) + + for item in items.get('items'): + item["include_exploded_items"] = items.get('include_exploded_items') + item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') + + raw_materials = get_items_for_material_requests(items, company) + if not raw_materials: + frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + + material_request = frappe.new_doc('Material Request') + material_request.update(dict( + doctype = 'Material Request', + transaction_date = nowdate(), + company = company, + requested_by = frappe.session.user, + material_request_type = 'Purchase' + )) + for item in raw_materials: + item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) + material_request.append('items', { + 'item_code': item.get('item_code'), + 'qty': item.get('quantity'), + 'schedule_date': schedule_date, + 'warehouse': item.get('warehouse'), + 'sales_order': sales_order, + 'project': project + }) + material_request.insert() + material_request.flags.ignore_permissions = 1 + material_request.run_method("set_missing_values") + material_request.submit() + return material_request \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 538ea55737..df92cb8f71 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -11,8 +11,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json - - +from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request class TestSalesOrder(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") @@ -327,9 +326,8 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.CancelledLinkError, dn.submit) def test_service_type_product_bundle(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - + from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Service Product Bundle", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) @@ -343,9 +341,8 @@ class TestSalesOrder(unittest.TestCase): self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) def test_mix_type_product_bundle(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - + from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Mix Product Bundle", {"is_stock_item": 0}) make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) @@ -388,11 +385,10 @@ class TestSalesOrder(unittest.TestCase): def test_drop_shipping(self): from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_drop_shipment - from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.purchase_order import update_status make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - + from erpnext.stock.doctype.item.test_item import make_item po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) @@ -585,8 +581,8 @@ class TestSalesOrder(unittest.TestCase): self.assertEquals(wo_qty[0][0], so_item_name.get(item)) def test_serial_no_based_delivery(self): - from erpnext.stock.doctype.item.test_item import make_item frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) + from erpnext.stock.doctype.item.test_item import make_item item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, "maintain_stock": 1, "has_serial_no": 1, @@ -685,6 +681,55 @@ class TestSalesOrder(unittest.TestCase): se.cancel() self.assertFalse(frappe.db.exists("Serial No", {"sales_order": so.name})) + def test_request_for_raw_materials(self): + from erpnext.stock.doctype.item.test_item import make_item + item = make_item("_Test Finished Item", {"is_stock_item": 1, + "maintain_stock": 1, + "valuation_rate": 500, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + make_item("_Test Raw Item A", {"maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + make_item("_Test Raw Item B", {"maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [ + { + "default_warehouse": "_Test Warehouse - _TC", + "company": "_Test Company" + }] + }) + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + make_bom(item=item.item_code, rate=1000, + raw_materials = ['_Test Raw Item A', '_Test Raw Item B']) + + so = make_sales_order(**{ + "item_list": [{ + "item_code": item.item_code, + "qty": 1, + "rate":1000 + }] + }) + so.submit() + mr_dict = frappe._dict() + items = so.get_work_order_items(1) + mr_dict['items'] = items + mr_dict['include_exploded_items'] = 0 + mr_dict['ignore_existing_ordered_qty'] = 1 + make_raw_material_request(mr_dict, so.company, so.name) + mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0] + mr_doc = frappe.get_doc('Material Request',mr.get('name')) + self.assertEqual(mr_doc.items[0].sales_order, so.name) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) @@ -714,7 +759,7 @@ def make_sales_order(**args): }) so.delivery_date = add_days(so.transaction_date, 10) - + if not args.do_not_save: so.insert() if not args.do_not_submit: