From 69d9f51dbbdffd54eecc605a1044d0868b5792b6 Mon Sep 17 00:00:00 2001 From: Shreya Shah Date: Mon, 12 Mar 2018 11:15:49 +0530 Subject: [PATCH 1/7] if price list not found, set default selling/ buying price list from settings (#13259) --- erpnext/accounts/party.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5237a71949..a3a889b1d4 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -141,7 +141,10 @@ def set_price_list(out, party, party_type, given_price_list): price_list = get_default_price_list(party) if not price_list: - price_list = given_price_list + if party.doctype == "Customer": + price_list = frappe.db.get_single_value('Selling Settings', 'selling_price_list') + else: + price_list = frappe.db.get_single_value('Buying Settings', 'buying_price_list') if price_list: out.price_list_currency = frappe.db.get_value("Price List", price_list, "currency") From 12aa4265ff24c63232d9677689cd991057ec06ae Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 12 Mar 2018 11:20:30 +0530 Subject: [PATCH 2/7] Don't allow to set negative quantity if transaction is not return entry (#13255) --- erpnext/controllers/status_updater.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b46c752aa6..df0fec833d 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -156,6 +156,9 @@ class StatusUpdater(Document): # get unique transactions to update for d in self.get_all_children(): + if hasattr(d, 'qty') and d.qty < 0 and not self.get('is_return'): + frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code)) + if d.doctype == args['source_dt'] and d.get(args["join_field"]): args['name'] = d.get(args['join_field']) From 58797481f0cbc08e082406fb0e041316ec9be477 Mon Sep 17 00:00:00 2001 From: Shreya Shah Date: Mon, 12 Mar 2018 13:08:01 +0530 Subject: [PATCH 3/7] uncheck report hide (#13256) --- .../doctype/physician/physician.json | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/physician/physician.json b/erpnext/healthcare/doctype/physician/physician.json index 3edad0b827..a4d2bd61de 100644 --- a/erpnext/healthcare/doctype/physician/physician.json +++ b/erpnext/healthcare/doctype/physician/physician.json @@ -40,6 +40,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -70,6 +71,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -100,6 +102,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -126,10 +129,11 @@ "print_hide_if_no_value": 0, "read_only": 0, "remember_last_selected_value": 0, - "report_hide": 1, + "report_hide": 0, "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -161,6 +165,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -192,6 +197,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -223,6 +229,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -254,6 +261,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -283,6 +291,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -313,6 +322,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -343,6 +353,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -373,6 +384,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -403,6 +415,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -433,6 +446,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -464,6 +478,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -493,6 +508,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -524,6 +540,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -554,6 +571,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -585,6 +603,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -615,6 +634,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -645,6 +665,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -674,6 +695,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -704,6 +726,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -734,6 +757,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -765,6 +789,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -796,6 +821,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -810,7 +836,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-10-05 16:08:24.624644", + "modified": "2018-03-09 07:05:24.984224", "modified_by": "Administrator", "module": "Healthcare", "name": "Physician", From 2c7a6e6b438861687a9269a14aba87a3a995df86 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Mar 2018 14:12:12 +0530 Subject: [PATCH 4/7] Reserve for subcontracting (#13195) * [fix] #8427 * review comments changes * Validation for reserved warhouse * code improvements * alignment * test case * message changes * default warehouse / remove validation / change sql * fix * patch * Fixed merge conflict * Fixes and cleanups of reserve qty for subcontracting * set from_warehouse only if purchase_order and purpose found (#12398) * [HotFix] Validation issue for subcontract stock entry (#12127) * [Fix] Validation issue for subcontract stock entry * Update stock_entry.py * Fixes and cleanups of reserve qty for subcontracting * patch fixed * Reload bin in patch * [fix] set source warehouse in stock entry for manufacture * [fix] #8540 * code alignment * code alignment * Move target warehouse validation to submit * validation code improvement * code changes for single stock entry * validation fix * call make_rm_stock_entry * remove old stock entry method/rewrite test case * Don't set bom_no against raw materials while trasferring items for sub-contracting * minor fix --- erpnext/accounts/doctype/account/account.py | 8 +- .../purchase_invoice/purchase_invoice.py | 6 +- .../doctype/purchase_order/purchase_order.js | 135 ++++++++++++++-- .../purchase_order/purchase_order.json | 32 ++++ .../doctype/purchase_order/purchase_order.py | 83 +++++++--- .../purchase_order/test_purchase_order.py | 146 +++++++++++++++--- .../purchase_order_item_supplied.json | 116 +++++++++++++- erpnext/controllers/buying_controller.py | 23 ++- erpnext/manufacturing/doctype/bom/bom.py | 13 +- erpnext/patches.txt | 3 +- .../update_reserved_qty_for_purchase_order.py | 49 ++++++ ...pdate_order_reference_in_return_entries.py | 3 +- erpnext/stock/dashboard/item_dashboard.js | 2 +- erpnext/stock/dashboard/item_dashboard.py | 5 +- erpnext/stock/doctype/bin/bin.json | 32 +++- erpnext/stock/doctype/bin/bin.py | 38 ++++- .../purchase_receipt/purchase_receipt.py | 3 +- .../stock/doctype/stock_entry/stock_entry.py | 48 +++++- .../doctype/stock_entry/test_stock_entry.py | 3 +- .../stock/page/stock_balance/stock_balance.js | 1 + .../stock_projected_qty.py | 9 +- erpnext/stock/stock_balance.py | 2 +- 22 files changed, 673 insertions(+), 87 deletions(-) create mode 100644 erpnext/patches/v10_0/update_reserved_qty_for_purchase_order.py diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 448cc83dfb..ac19690b5f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -169,10 +169,10 @@ class Account(NestedSet): # Add company abbr if not provided from erpnext.setup.doctype.company.company import get_name_with_abbr new_account = get_name_with_abbr(new, self.company) - new_account = get_name_with_number(new_account, self.account_number) - - # Validate properties before merging - if merge: + if not merge: + new_account = get_name_with_number(new_account, self.account_number) + else: + # Validate properties before merging if not frappe.db.exists("Account", new): throw(_("Account {0} does not exist").format(new)) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 28d51ce0c7..21b71ff37c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -350,7 +350,6 @@ class PurchaseInvoice(BuyingController): self.negative_expense_to_be_booked = 0.0 gl_entries = [] - self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries) @@ -424,7 +423,10 @@ class PurchaseInvoice(BuyingController): # sub-contracting warehouse if flt(item.rm_supp_cost): - supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["name"] + supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"] + if not supplier_warehouse_account: + frappe.throw(_("Please set account in Warehouse {0}") + .format(self.supplier_warehouse)) gl_entries.append(self.get_gl_dict({ "account": supplier_warehouse_account, "against": item.expense_account, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index d012dee730..09402880ad 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -15,6 +15,15 @@ frappe.ui.form.on("Purchase Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) + + frm.set_query("reserve_warehouse", "supplied_items", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + } + }); }, onload: function(frm) { @@ -134,23 +143,124 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); var me = this; - if(items.length===1) { - me._make_stock_entry(items[0]); - return; + if(items.length >= 1){ + me.raw_material_data = []; + me.show_dialog = 1; + let title = ""; + let fields = [ + {fieldtype:'Section Break', label: __('Raw Materials')}, + {fieldname: 'sub_con_rm_items', fieldtype: 'Table', + fields: [ + { + fieldtype:'Data', + fieldname:'item_code', + label: __('Item'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Data', + fieldname:'rm_item_code', + label: __('Raw Material'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'qty', + label: __('Quantity'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Data', + read_only:1, + fieldname:'warehouse', + label: __('Reserve Warehouse'), + in_list_view:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'rate', + label: __('Rate'), + hidden:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'amount', + label: __('Amount'), + hidden:1 + }, + { + fieldtype:'Link', + read_only:1, + fieldname:'uom', + label: __('UOM'), + hidden:1 + } + ], + data: me.raw_material_data, + get_data: function() { + return me.raw_material_data; + } + } + ] + + me.dialog = new frappe.ui.Dialog({ + title: title, fields: fields + }); + + if (me.frm.doc['supplied_items']) { + me.frm.doc['supplied_items'].forEach((item, index) => { + if (item.rm_item_code && item.main_item_code) { + me.raw_material_data.push ({ + 'name':index, + 'item_code': item.main_item_code, + 'rm_item_code': item.rm_item_code, + 'item_name': item.rm_item_code, + 'qty': item.required_qty, + 'warehouse':item.reserve_warehouse, + 'rate':item.rate, + 'amount':item.amount, + 'stock_uom':item.stock_uom + }); + me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); + } + }) } - frappe.prompt({fieldname:"item", options: items, fieldtype:"Select", - label: __("Select Item for Transfer"), reqd: 1}, function(data) { - me._make_stock_entry(data.item); - }, __("Select Item"), __("Make")); + + me.dialog.show() + this.dialog.set_primary_action(__('Transfer'), function() { + me.values = me.dialog.get_values(); + if(me.values) { + me.values.sub_con_rm_items.map((row,i) => { + if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { + frappe.throw(__("Item Code, warehouse, quantity are required on row" + (i+1))); + } + }) + me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) + me.dialog.hide() + } + }); + } + + me.dialog.get_close_btn().on('click', () => { + me.dialog.hide(); + }); + }, - _make_stock_entry: function(item) { + _make_rm_stock_entry: function(rm_items) { frappe.call({ - method:"erpnext.buying.doctype.purchase_order.purchase_order.make_stock_entry", + method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry", args: { purchase_order: cur_frm.doc.name, - item_code: item - }, + rm_items: rm_items + } + , callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); @@ -284,7 +394,8 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt filters: [ ['BOM', 'item', '=', d.item_code], ['BOM', 'is_active', '=', '1'], - ['BOM', 'docstatus', '=', '1'] + ['BOM', 'docstatus', '=', '1'], + ['BOM', 'company', '=', doc.company] ] } } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 53771d1742..95aca6b02f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -3133,6 +3133,38 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "eval:doc.is_subcontracted", + "fieldname": "supplied_items_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Supplied Items", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", "fieldname": "supplied_items", "fieldtype": "Table", "hidden": 0, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9e890f6bcc..b9e707525f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -12,7 +12,7 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details from erpnext.stock.stock_balance import update_bin_qty, get_ordered_qty from frappe.desk.notifications import clear_doctype_notifications from erpnext.buying.utils import validate_for_items, check_for_closed_status - +from erpnext.stock.utils import get_bin form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -80,8 +80,10 @@ class PurchaseOrder(BuyingController): def validate_supplier(self): prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos') if prevent_po: - standing = frappe.db.get_value("Supplier Scorecard",self.supplier, 'status') - frappe.throw(_("Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.").format(self.supplier, standing)) + standing = frappe.db.get_value("Supplier Scorecard", self.supplier, 'status') + if standing: + frappe.throw(_("Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.") + .format(self.supplier, standing)) warn_po = frappe.db.get_value("Supplier", self.supplier, 'warn_pos') if warn_po: @@ -192,6 +194,9 @@ class PurchaseOrder(BuyingController): self.set_status(update=True, status=status) self.update_requested_qty() self.update_ordered_qty() + if self.is_subcontracted == "Yes": + self.update_reserved_qty_for_subcontract() + self.notify_update() clear_doctype_notifications(self) @@ -204,6 +209,8 @@ class PurchaseOrder(BuyingController): self.update_prevdoc_status() self.update_requested_qty() self.update_ordered_qty() + if self.is_subcontracted == "Yes": + self.update_reserved_qty_for_subcontract() frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total) @@ -217,6 +224,9 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() + if self.is_subcontracted == "Yes": + self.update_reserved_qty_for_subcontract() + self.check_for_closed_status() frappe.db.set(self,'status','Cancelled') @@ -268,6 +278,12 @@ class PurchaseOrder(BuyingController): if item.delivered_by_supplier == 1: item.received_qty = item.qty + def update_reserved_qty_for_subcontract(self): + for d in self.supplied_items: + if d.rm_item_code: + stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor= 1.0): """get last purchase rate for an item""" if cint(frappe.db.get_single_value("Buying Settings", "disable_fetch_last_purchase_rate")): return @@ -388,23 +404,52 @@ def make_purchase_invoice(source_name, target_doc=None): return doc @frappe.whitelist() -def make_stock_entry(purchase_order, item_code): - purchase_order = frappe.get_doc("Purchase Order", purchase_order) +def make_rm_stock_entry(purchase_order, rm_items): + if isinstance(rm_items, basestring): + rm_items_list = json.loads(rm_items) + else: + frappe.throw(_("No Items available for transfer")) - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Subcontract" - stock_entry.purchase_order = purchase_order.name - stock_entry.supplier = purchase_order.supplier - stock_entry.supplier_name = purchase_order.supplier_name - stock_entry.supplier_address = purchase_order.supplier_address - stock_entry.address_display = purchase_order.address_display - stock_entry.company = purchase_order.company - stock_entry.from_bom = 1 - po_item = [d for d in purchase_order.items if d.item_code == item_code][0] - stock_entry.fg_completed_qty = po_item.qty - stock_entry.bom_no = po_item.bom - stock_entry.get_items() - return stock_entry.as_dict() + if rm_items_list: + fg_items = list(set(d["item_code"] for d in rm_items_list)) + else: + frappe.throw(_("No Items selected for transfer")) + + if purchase_order: + purchase_order = frappe.get_doc("Purchase Order", purchase_order) + + if fg_items: + items = tuple(set(d["rm_item_code"] for d in rm_items_list)) + item_wh = frappe._dict(frappe.db.sql(""" + select item_code, description + from `tabItem` where name in ({0}) + """.format(", ".join(["%s"] * len(items))), items)) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Subcontract" + stock_entry.purchase_order = purchase_order.name + stock_entry.supplier = purchase_order.supplier + stock_entry.supplier_name = purchase_order.supplier_name + stock_entry.supplier_address = purchase_order.supplier_address + stock_entry.address_display = purchase_order.address_display + stock_entry.company = purchase_order.company + for item_code in fg_items: + for rm_item_data in rm_items_list: + if rm_item_data["item_code"] == item_code: + items_dict = { + rm_item_data["rm_item_code"]: { + "item_name":rm_item_data["item_name"], + "description":item_wh.get(rm_item_data["rm_item_code"]), + 'qty':rm_item_data["qty"], + 'from_warehouse':rm_item_data["warehouse"], + 'stock_uom':rm_item_data["stock_uom"] + } + } + stock_entry.add_to_stock_entry_detail(items_dict) + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer")) + return purchase_order.name @frappe.whitelist() def update_status(status, name): diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d31b23007f..86a133785f 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -6,8 +6,9 @@ import unittest import frappe import frappe.defaults from frappe.utils import flt, add_days, nowdate -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_purchase_invoice - +from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, make_purchase_invoice, make_rm_stock_entry as make_subcontract_transfer_entry) +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +import json class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -182,24 +183,129 @@ class TestPurchaseOrder(unittest.TestCase): pi.insert() self.assertTrue(pi.get('payment_schedule')) - + def test_reserved_qty_subcontract_po(self): + # Make stock available for raw materials + make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", + qty=20, basic_rate=100) + + bin1 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + + # Submit PO + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + + bin2 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + self.assertEquals(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + self.assertEquals(bin2.projected_qty, bin1.projected_qty - 10) + + # Create stock transfer + rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", + "qty":6,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":600,"stock_uom":"Nos"}] + rm_item_string = json.dumps(rm_item) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.save() + se.submit() + + bin3 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # close PO + po.update_status("Closed") + bin4 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + # Re-open PO + po.update_status("Submitted") + bin5 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # make Purchase Receipt against PO + pr = make_purchase_receipt(po.name) + pr.supplier_warehouse = "_Test Warehouse 1 - _TC" + pr.save() + pr.submit() + + bin6 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + # Cancel PR + pr.cancel() + bin7 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # Make Purchase Invoice + pi = make_purchase_invoice(po.name) + pi.update_stock = 1 + pi.supplier_warehouse = "_Test Warehouse 1 - _TC" + pi.insert() + pi.submit() + bin8 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + # Cancel PR + pi.cancel() + bin9 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # Cancel Stock Entry + se.cancel() + bin10 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + self.assertEquals(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + + # Cancel PO + po.reload() + po.cancel() + bin11 = frappe.db.get_value("Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", as_dict=1) + + self.assertEquals(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + def get_same_items(): return [ - { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 500, - "schedule_date": add_days(nowdate(), 1) - }, - { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 4, - "rate": 500, - "schedule_date": add_days(nowdate(), 1) - } - ] + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 500, + "schedule_date": add_days(nowdate(), 1) + }, + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 4, + "rate": 500, + "schedule_date": add_days(nowdate(), 1) + } + ] def create_purchase_order(**args): po = frappe.new_doc("Purchase Order") @@ -224,6 +330,10 @@ def create_purchase_order(**args): if not args.do_not_save: po.insert() if not args.do_not_submit: + if po.is_subcontracted == "Yes": + supp_items = po.get("supplied_items") + for d in supp_items: + d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" po.submit() return po diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index 4a870374ce..d4a02fbb19 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "beta": 0, @@ -10,16 +11,20 @@ "editable_grid": 1, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 2, "fieldname": "main_item_code", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, + "in_standard_filter": 0, "label": "Item Code", "length": 0, "no_copy": 0, @@ -29,6 +34,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -36,16 +42,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 2, "fieldname": "rm_item_code", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, + "in_standard_filter": 0, "label": "Raw Material Item Code", "length": 0, "no_copy": 0, @@ -55,6 +65,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -62,16 +73,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 2, "fieldname": "required_qty", "fieldtype": "Float", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, + "in_standard_filter": 0, "label": "Supplied Qty", "length": 0, "no_copy": 0, @@ -81,6 +96,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -88,16 +104,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 2, "fieldname": "rate", "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, + "in_standard_filter": 0, "label": "Rate", "length": 0, "no_copy": 0, @@ -108,6 +128,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -115,16 +136,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "amount", "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, + "in_standard_filter": 0, "label": "Amount", "length": 0, "no_copy": 0, @@ -135,6 +160,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -142,16 +168,49 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "bom_detail_no", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, - "in_list_view": 1, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, "label": "BOM Detail No", "length": 0, "no_copy": 0, @@ -161,6 +220,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -168,16 +228,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "reference_name", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, - "in_list_view": 1, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, "label": "Reference Name", "length": 0, "no_copy": 0, @@ -187,6 +251,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -194,16 +259,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "conversion_factor", "fieldtype": "Float", "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, + "in_standard_filter": 0, "label": "Conversion Factor", "length": 0, "no_copy": 0, @@ -213,6 +282,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -220,16 +290,20 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, + "columns": 0, "fieldname": "stock_uom", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, + "in_standard_filter": 0, "label": "Stock Uom", "length": 0, "no_copy": 0, @@ -240,6 +314,38 @@ "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 2, + "fieldname": "reserve_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reserve Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -247,17 +353,17 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 1, "idx": 1, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:28:05.533063", + "modified": "2018-01-05 14:47:15.400785", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", @@ -266,5 +372,7 @@ "quick_entry": 0, "read_only": 0, "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 0, "track_seen": 0 } \ No newline at end of file diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 63e55ee9a7..4b7b43cc19 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -163,6 +163,11 @@ class BuyingController(StockController): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) + if self.doctype == "Purchase Order": + for supplied_item in self.get("supplied_items"): + if not supplied_item.reserve_warehouse: + frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code))) + else: for item in self.get("items"): if item.bom: @@ -192,8 +197,16 @@ class BuyingController(StockController): def update_raw_materials_supplied(self, item, raw_material_table): bom_items = self.get_items_from_bom(item.item_code, item.bom) raw_materials_cost = 0 + items = list(set([d.item_code for d in bom_items])) + item_wh = frappe._dict(frappe.db.sql("""select item_code, default_warehouse + from `tabItem` where name in ({0})""".format(", ".join(["%s"] * len(items))), items)) for bom_item in bom_items: + if self.doctype == "Purchase Order": + reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code) + if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company: + reserve_warehouse = None + # check if exists exists = 0 for d in self.get(raw_material_table): @@ -213,6 +226,8 @@ class BuyingController(StockController): rm.rm_item_code = bom_item.item_code rm.stock_uom = bom_item.stock_uom rm.required_qty = required_qty + if self.doctype == "Purchase Order" and not rm.reserve_warehouse: + rm.reserve_warehouse = reserve_warehouse rm.conversion_factor = item.conversion_factor @@ -264,7 +279,7 @@ class BuyingController(StockController): def get_items_from_bom(self, item_code, bom): bom_items = frappe.db.sql("""select t2.item_code, t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit, - t2.rate, t2.stock_uom, t2.name, t2.description + t2.rate, t2.stock_uom, t2.name, t2.description, t2.source_warehouse from `tabBOM` t1, `tabBOM Item` t2, tabItem t3 where t2.parent = t1.name and t1.item = %s and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s @@ -339,7 +354,7 @@ class BuyingController(StockController): frappe.get_meta(item_row.doctype).get_label(fieldname), item_row['item_code']))) def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False): - self.update_ordered_qty() + self.update_ordered_and_reserved_qty() sl_entries = [] stock_items = self.get_stock_items() @@ -381,7 +396,7 @@ class BuyingController(StockController): self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - def update_ordered_qty(self): + def update_ordered_and_reserved_qty(self): po_map = {} for d in self.get("items"): if self.doctype=="Purchase Receipt" \ @@ -400,6 +415,8 @@ class BuyingController(StockController): frappe.InvalidStatusError) po_obj.update_ordered_qty(po_item_rows) + if self.is_subcontracted: + po_obj.update_reserved_qty_for_subcontract() def make_sl_entries_for_supplier_warehouse(self, sl_entries): if hasattr(self, 'supplied_items'): diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 5e1e52a504..862b4ee504 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -597,9 +597,16 @@ def validate_bom_no(item, bom_no): if bom.docstatus != 1: if not getattr(frappe.flags, "in_test", False): frappe.throw(_("BOM {0} must be submitted").format(bom_no)) - if item and not (bom.item.lower() == item.lower() or \ - bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower()): - frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) + if item: + rm_item_exists = False + for d in bom.items: + if (d.item_code.lower() == item.lower()): + rm_item_exists = True + if bom.item.lower() == item.lower() or \ + bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower(): + rm_item_exists = True + if not rm_item_exists: + frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) @frappe.whitelist() def get_children(doctype, parent=None, is_root=False, **filters): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dd707dafa0..d06b405821 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -493,4 +493,5 @@ erpnext.patches.v10_0.set_b2c_limit erpnext.patches.v10_0.update_status_for_multiple_source_in_po erpnext.patches.v10_0.set_auto_created_serial_no_in_stock_entry erpnext.patches.v10_0.update_territory_and_customer_group -erpnext.patches.v10_0.update_warehouse_address_details \ No newline at end of file +erpnext.patches.v10_0.update_warehouse_address_details +erpnext.patches.v10_0.update_reserved_qty_for_purchase_order diff --git a/erpnext/patches/v10_0/update_reserved_qty_for_purchase_order.py b/erpnext/patches/v10_0/update_reserved_qty_for_purchase_order.py new file mode 100644 index 0000000000..681296de88 --- /dev/null +++ b/erpnext/patches/v10_0/update_reserved_qty_for_purchase_order.py @@ -0,0 +1,49 @@ +import frappe +from erpnext.stock.utils import get_bin + +def execute(): + po_item = list(frappe.db.sql((""" + select distinct po.name as poname, poitem.rm_item_code as rm_item_code, po.company + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitem + where po.name = poitem.parent + and po.is_subcontracted = "Yes" + and po.docstatus = 1"""), as_dict=1)) + if not po_item: + return + + frappe.reload_doc("stock", "doctype", "bin") + frappe.reload_doc("buying", "doctype", "purchase_order_item_supplied") + company_warehouse = frappe._dict(frappe.db.sql("""select company, min(name) from `tabWarehouse` + where is_group = 0 group by company""")) + + items = list(set([d.rm_item_code for d in po_item])) + item_wh = frappe._dict(frappe.db.sql("""select item_code, default_warehouse + from `tabItem` where name in ({0})""".format(", ".join(["%s"] * len(items))), items)) + + # Update reserved warehouse + for item in po_item: + reserve_warehouse = get_warehouse(item.rm_item_code, item.company, company_warehouse, item_wh) + frappe.db.sql("""update `tabPurchase Order Item Supplied` + set reserve_warehouse = %s + where parent = %s and rm_item_code = %s + """, (reserve_warehouse, item["poname"], item["rm_item_code"])) + + # Update bin + item_wh_bin = frappe.db.sql((""" + select distinct poitemsup.rm_item_code as rm_item_code, + poitemsup.reserve_warehouse as reserve_warehouse + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + where po.name = poitemsup.parent + and po.is_subcontracted = "Yes" + and po.docstatus = 1"""), as_dict=1) + for d in item_wh_bin: + stock_bin = get_bin(d["rm_item_code"], d["reserve_warehouse"]) + stock_bin.update_reserved_qty_for_sub_contracting() + +def get_warehouse(item_code, company, company_warehouse, item_wh): + reserve_warehouse = item_wh.get(item_code) + if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != company: + reserve_warehouse = None + if not reserve_warehouse: + reserve_warehouse = company_warehouse.get(company) + return reserve_warehouse \ No newline at end of file diff --git a/erpnext/patches/v5_8/update_order_reference_in_return_entries.py b/erpnext/patches/v5_8/update_order_reference_in_return_entries.py index d7972dc64a..503263834c 100644 --- a/erpnext/patches/v5_8/update_order_reference_in_return_entries.py +++ b/erpnext/patches/v5_8/update_order_reference_in_return_entries.py @@ -10,6 +10,7 @@ def execute(): frappe.reload_doctype("Purchase Receipt") frappe.reload_doctype("Sales Order Item") frappe.reload_doctype("Purchase Order Item") + frappe.reload_doctype("Purchase Order Item Supplied") # sales return return_entries = list(frappe.db.sql(""" @@ -86,6 +87,6 @@ def execute(): """, (order_details[0].purchase_order, order_details[0].po_detail, d.row_id)) pr = frappe.get_doc("Purchase Receipt", d.name) - pr.update_ordered_qty() + pr.update_ordered_and_reserved_qty() pr.update_prevdoc_status() diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 4a525261b8..2748436fd8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -90,7 +90,7 @@ erpnext.stock.ItemDashboard = Class.extend({ data.forEach(function(d) { d.actual_or_pending = d.projected_qty + d.reserved_qty + d.reserved_qty_for_production; d.pending_qty = 0; - d.total_reserved = d.reserved_qty + d.reserved_qty_for_production; + d.total_reserved = d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract; if(d.actual_or_pending > d.actual_qty) { d.pending_qty = d.actual_or_pending - d.actual_qty; } diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 0d75a9a544..f95daafd38 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -26,13 +26,14 @@ def get_data(item_code=None, warehouse=None, item_group=None, return frappe.db.sql(''' select b.item_code, b.warehouse, b.projected_qty, b.reserved_qty, - b.reserved_qty_for_production, b.actual_qty, b.valuation_rate, i.item_name + b.reserved_qty_for_production, b.reserved_qty_for_sub_contract, b.actual_qty, b.valuation_rate, i.item_name from tabBin b, tabItem i where b.item_code = i.name and - (b.projected_qty != 0 or b.reserved_qty != 0 or b.reserved_qty_for_production != 0 or b.actual_qty != 0) + (b.projected_qty != 0 or b.reserved_qty != 0 or b.reserved_qty_for_production != 0 + or b.reserved_qty_for_sub_contract != 0 or b.actual_qty != 0) {conditions} order by {sort_by} {sort_order} diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 1f6e9e1785..def817b65d 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -296,6 +296,36 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reserved_qty_for_sub_contract", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reserved Qty for sub contract", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -463,7 +493,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-13 13:06:32.601505", + "modified": "2017-11-22 08:14:30.615638", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index f2c214db4c..430d9fb404 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -61,7 +61,7 @@ class Bin(Document): def set_projected_qty(self): self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty) + flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) - - flt(self.reserved_qty_for_production)) + - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) def get_first_sle(self): sle = frappe.db.sql(""" @@ -90,6 +90,42 @@ class Bin(Document): self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) self.db_set('projected_qty', self.projected_qty) + def update_reserved_qty_for_sub_contracting(self): + #reserved qty + reserved_qty_for_sub_contract = frappe.db.sql(''' + select ifnull(sum(itemsup.required_qty),0) + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup + where + itemsup.rm_item_code = %s + and itemsup.parent = po.name + and po.docstatus = 1 + and po.is_subcontracted = 'Yes' + and po.status != 'Closed' + and po.per_received < 100 + and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] + + #Get Transferred Entries + materials_transferred = frappe.db.sql(""" + select + ifnull(sum(transfer_qty),0) + from + `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po + where + se.docstatus=1 + and se.purpose='Subcontract' + and ifnull(se.purchase_order, '') !='' + and sed.item_code = %s + and se.name = sed.parent + and se.purchase_order = po.name + and po.docstatus = 1 + and po.is_subcontracted = 'Yes' + and po.status != 'Closed' + and po.per_received < 100 + """, (self.item_code))[0][0] + + self.db_set('reserved_qty_for_sub_contract', (reserved_qty_for_sub_contract - materials_transferred)) + self.set_projected_qty() + self.db_set('projected_qty', self.projected_qty) def on_doctype_update(): frappe.db.add_index("Bin", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 29f3553deb..2a962cc2ac 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -122,7 +122,8 @@ class PurchaseReceipt(BuyingController): self.update_billing_status() # Updating stock ledger should always be called after updating prevdoc status, - # because updating ordered qty in bin depends upon updated ordered qty in PO + # because updating ordered qty, reserved_qty_for_subcontract in bin + # depends upon updated ordered qty in PO self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7d5af5fcf4..5aeba7c466 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -11,6 +11,7 @@ from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError, get from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor from erpnext.stock.doctype.batch.batch import get_batch_no, set_batch_nos, get_batch_qty from erpnext.manufacturing.doctype.bom.bom import validate_bom_no +from erpnext.stock.utils import get_bin import json class IncorrectValuationRateError(frappe.ValidationError): pass @@ -63,17 +64,22 @@ class StockEntry(StockController): self.calculate_rate_and_amount(update_finished_item_rate=False) def on_submit(self): + self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") self.update_production_order() self.validate_purchase_order() + if self.purchase_order and self.purpose == "Subcontract": + self.update_purchase_order_supplied_items() self.make_gl_entries() def on_cancel(self): self.update_stock_ledger() self.update_production_order() + if self.purchase_order and self.purpose == "Subcontract": + self.update_purchase_order_supplied_items() self.make_gl_entries_on_cancel() def validate_purpose(self): @@ -403,7 +409,7 @@ class StockEntry(StockController): """validation: finished good quantity should be same as manufacturing quantity""" items_with_target_warehouse = [] for d in self.get('items'): - if d.bom_no and flt(d.transfer_qty) != flt(self.fg_completed_qty) and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): + if self.purpose != "Subcontract" and d.bom_no and flt(d.transfer_qty) != flt(self.fg_completed_qty) and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ format(d.idx, d.transfer_qty, self.fg_completed_qty)) @@ -582,19 +588,32 @@ class StockEntry(StockController): frappe.throw(_("Manufacturing Quantity is mandatory")) item_dict = self.get_bom_raw_materials(self.fg_completed_qty) + + #Get PO Supplied Items Details + if self.purchase_order and self.purpose == "Subcontract": + #Get PO Supplied Items Details + item_wh = frappe._dict(frappe.db.sql(""" + select rm_item_code, reserve_warehouse + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + where po.name = poitemsup.parent + and po.name = %s""",self.purchase_order)) for item in item_dict.values(): if self.pro_doc and not self.pro_doc.skip_transfer: item["from_warehouse"] = self.pro_doc.wip_warehouse - + #Get Reserve Warehouse from PO + if self.purchase_order and self.purpose=="Subcontract": + item["from_warehouse"] = item_wh.get(item.item_code) item["to_warehouse"] = self.to_warehouse if self.purpose=="Subcontract" else "" self.add_to_stock_entry_detail(item_dict) - scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) - for item in scrap_item_dict.values(): - if self.pro_doc and self.pro_doc.scrap_warehouse: - item["to_warehouse"] = self.pro_doc.scrap_warehouse - self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + if self.purpose != "Subcontract": + scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) + for item in scrap_item_dict.values(): + if self.pro_doc and self.pro_doc.scrap_warehouse: + item["to_warehouse"] = self.pro_doc.scrap_warehouse + + self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) # fetch the serial_no of the first stock entry for the second stock entry if self.production_order and self.purpose == "Manufacture": @@ -782,7 +801,7 @@ class StockEntry(StockController): def add_to_stock_entry_detail(self, item_dict, bom_no=None): expense_account, cost_center = frappe.db.get_values("Company", self.company, \ ["default_expense_account", "cost_center"])[0] - + for d in item_dict: stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") @@ -833,7 +852,20 @@ class StockEntry(StockController): if getdate(self.posting_date) > getdate(expiry_date): frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) + def update_purchase_order_supplied_items(self): + #Get PO Supplied Items Details + item_wh = frappe._dict(frappe.db.sql(""" + select rm_item_code, reserve_warehouse + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + where po.name = poitemsup.parent + and po.name = %s""", self.purchase_order)) + #Update reserved sub contracted quantity in bin based on Supplied Item Details + for d in self.get("items"): + reserve_warehouse = item_wh.get(d.item_code) + stock_bin = get_bin(d.item_code, reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, basestring): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index b590822973..a5814f7dea 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -562,9 +562,8 @@ class TestStockEntry(unittest.TestCase): rm_cost = 0 for d in stock_entry.get("items"): - if d.s_warehouse: + if d.item_code != "_Test FG Item 2": rm_cost += flt(d.amount) - fg_cost = filter(lambda x: x.item_code=="_Test FG Item 2", stock_entry.get("items"))[0].amount self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + production_order.additional_operating_cost, 2)) diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index 16a85fa922..85ea5b19be 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -48,6 +48,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { {fieldname: 'projected_qty', label: __('Projected qty')}, {fieldname: 'reserved_qty', label: __('Reserved for sale')}, {fieldname: 'reserved_qty_for_production', label: __('Reserved for manufacturing')}, + {fieldname: 'reserved_qty_for_sub_contract', label: __('Reserved for sub contracting')}, {fieldname: 'actual_qty', label: __('Actual qty in stock')}, ] }, diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 21287b9d9e..89a256cacf 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -16,6 +16,7 @@ def get_columns(): _("UOM") + ":Link/UOM:100", _("Actual Qty") + ":Float:100", _("Planned Qty") + ":Float:100", _("Requested Qty") + ":Float:110", _("Ordered Qty") + ":Float:100", _("Reserved Qty") + ":Float:100", _("Reserved Qty for Production") + ":Float:100", + _("Reserved for sub contracting") + ":Float:100", _("Projected Qty") + ":Float:100", _("Reorder Level") + ":Float:100", _("Reorder Qty") + ":Float:100", _("Shortage Qty") + ":Float:100"] @@ -33,7 +34,8 @@ def get_data(filters): continue # item = item_map.setdefault(bin.item_code, get_item(bin.item_code)) - company = warehouse_company.setdefault(bin.warehouse, frappe.db.get_value("Warehouse", bin.warehouse, "company")) + company = warehouse_company.setdefault(bin.warehouse, + frappe.db.get_value("Warehouse", bin.warehouse, "company")) if filters.brand and filters.brand != item.brand: continue @@ -52,7 +54,8 @@ def get_data(filters): data.append([item.name, item.item_name, item.description, item.item_group, item.brand, bin.warehouse, item.stock_uom, bin.actual_qty, bin.planned_qty, bin.indented_qty, bin.ordered_qty, - bin.reserved_qty, bin.reserved_qty_for_production, bin.projected_qty, re_order_level, re_order_qty, shortage_qty]) + bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, + bin.projected_qty, re_order_level, re_order_qty, shortage_qty]) return data @@ -71,7 +74,7 @@ def get_bin_list(filters): warehouse_details.rgt)) bin_list = frappe.db.sql("""select item_code, warehouse, actual_qty, planned_qty, indented_qty, - ordered_qty, reserved_qty, reserved_qty_for_production, projected_qty + ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty from tabBin bin {conditions} order by item_code, warehouse """.format(conditions=" where " + " and ".join(conditions) if conditions else ""), as_dict=1) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 6a4ac439ee..49909d9c56 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -150,7 +150,7 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): if mismatch: bin.projected_qty = (flt(bin.actual_qty) + flt(bin.ordered_qty) + flt(bin.indented_qty) + flt(bin.planned_qty) - flt(bin.reserved_qty) - - flt(bin.reserved_qty_for_production)) + - flt(bin.reserved_qty_for_production)) - flt(bin.reserved_qty_for_sub_contract) bin.save() From aa54d934b825a0af6d1172fb27185b5faa1e0e2a Mon Sep 17 00:00:00 2001 From: Shreya Shah Date: Mon, 12 Mar 2018 15:23:28 +0530 Subject: [PATCH 5/7] make stock qty negative (#13276) --- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 4b8bbee749..ab189cf1af 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -239,6 +239,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.received_qty = -1* source_doc.received_qty target_doc.rejected_qty = -1* source_doc.rejected_qty target_doc.qty = -1* source_doc.qty + target_doc.stock_qty = -1 * source_doc.stock_qty target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse @@ -246,6 +247,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.received_qty = -1* source_doc.received_qty target_doc.rejected_qty = -1* source_doc.rejected_qty target_doc.qty = -1* source_doc.qty + target_doc.stock_qty = -1 * source_doc.stock_qty target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.rejected_warehouse = source_doc.rejected_warehouse From b34ab7549a96e244440c9b71017c1a7c25b6f613 Mon Sep 17 00:00:00 2001 From: Ameya Shenoy Date: Mon, 12 Mar 2018 15:24:01 +0530 Subject: [PATCH 6/7] fixed subject to task_name (#13278) --- erpnext/agriculture/doctype/crop/crop.py | 3 ++- erpnext/agriculture/doctype/disease/disease.py | 3 ++- erpnext/agriculture/doctype/soil_texture/soil_texture.py | 5 +++-- erpnext/agriculture/doctype/water_analysis/water_analysis.py | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/agriculture/doctype/crop/crop.py b/erpnext/agriculture/doctype/crop/crop.py index 7eeb8af6ce..ca928f862b 100644 --- a/erpnext/agriculture/doctype/crop/crop.py +++ b/erpnext/agriculture/doctype/crop/crop.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ class Crop(Document): def validate(self): @@ -12,7 +13,7 @@ class Crop(Document): for task in self.agriculture_task: # validate start_day is not > end_day if task.start_day > task.end_day: - frappe.throw("Start day is greater than end day in task '{0}'".format(task.subject)) + frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name)) # to calculate the period of the Crop Cycle if task.end_day > max_period: max_period = task.end_day if max_period > self.period: self.period = max_period diff --git a/erpnext/agriculture/doctype/disease/disease.py b/erpnext/agriculture/doctype/disease/disease.py index 42005d6b06..f7cd7df180 100644 --- a/erpnext/agriculture/doctype/disease/disease.py +++ b/erpnext/agriculture/doctype/disease/disease.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ class Disease(Document): def validate(self): @@ -12,7 +13,7 @@ class Disease(Document): for task in self.treatment_task: # validate start_day is not > end_day if task.start_day > task.end_day: - frappe.throw("Start day is greater than end day in task '{0}'".format(task.task_name)) + frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name)) # to calculate the period of the Crop Cycle if task.end_day > max_period: max_period = task.end_day self.treatment_period = max_period \ No newline at end of file diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py index 7345e862b9..26c5d5c0e4 100644 --- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py +++ b/erpnext/agriculture/doctype/soil_texture/soil_texture.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils import flt, cint +from frappe import _ class SoilTexture(Document): soil_edit_order = [2, 1, 0] @@ -20,9 +21,9 @@ class SoilTexture(Document): self.update_soil_edit('sand_composition') for soil_type in self.soil_types: if self.get(soil_type) > 100 or self.get(soil_type) < 0: - frappe.throw("{0} should be a value between 0 and 100".format(soil_type)) + frappe.throw(_("{0} should be a value between 0 and 100").format(soil_type)) if sum(self.get(soil_type) for soil_type in self.soil_types) != 100: - frappe.throw('Soil compositions do not add up to 100') + frappe.throw(_('Soil compositions do not add up to 100')) def update_soil_edit(self, soil_type): self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1 diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py index 81fdf14a37..4d3cde075d 100644 --- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py +++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ class WaterAnalysis(Document): def load_contents(self): @@ -18,6 +19,6 @@ class WaterAnalysis(Document): def validate(self): if self.collection_datetime > self.laboratory_testing_datetime: - frappe.throw('Lab testing datetime cannot be before collection datetime') + frappe.throw(_('Lab testing datetime cannot be before collection datetime')) if self.laboratory_testing_datetime > self.result_datetime: - frappe.throw('Lab result datetime cannot be before testing datetime') \ No newline at end of file + frappe.throw(_('Lab result datetime cannot be before testing datetime')) \ No newline at end of file From 04f50be306a61cfff0e7f441c4605323e21b9e92 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Mar 2018 16:01:03 +0600 Subject: [PATCH 7/7] bumped to version 10.1.8 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a4647e3b0a..7ffd424c31 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.7' +__version__ = '10.1.8' def get_default_company(user=None): '''Get default company for user'''