From c1b0e65f9f9df5fdad4a813a788f4f458b2e8318 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Nov 2020 09:54:44 +0530 Subject: [PATCH 01/15] feat: Putaway --- .../stock/doctype/putaway_rule/__init__.py | 0 .../doctype/putaway_rule/putaway_rule.js | 18 +++ .../doctype/putaway_rule/putaway_rule.json | 111 ++++++++++++++++++ .../doctype/putaway_rule/putaway_rule.py | 93 +++++++++++++++ .../doctype/putaway_rule/putaway_rule_list.js | 10 ++ .../doctype/putaway_rule/test_putaway_rule.py | 10 ++ 6 files changed, 242 insertions(+) create mode 100644 erpnext/stock/doctype/putaway_rule/__init__.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.js create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.json create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule_list.js create mode 100644 erpnext/stock/doctype/putaway_rule/test_putaway_rule.py diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js new file mode 100644 index 0000000000..ae08e82c28 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -0,0 +1,18 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Putaway Rule', { + setup: function(frm) { + frm.set_query("warehouse", function() { + return { + "filters": { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + } + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json new file mode 100644 index 0000000000..6a132c7e25 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "autoname": "format:{item_code}-{warehouse}", + "creation": "2020-11-09 11:39:46.489501", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable", + "item_code", + "item_name", + "warehouse", + "col_break_capacity", + "company", + "capacity", + "priority", + "stock_uom" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "col_break_capacity", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capacity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Capacity", + "reqd": 1 + }, + { + "default": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-10 17:06:27.151335", + "modified_by": "Administrator", + "module": "Stock", + "name": "Putaway Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py new file mode 100644 index 0000000000..9f02833431 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -0,0 +1,93 @@ +# -*- 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 +import copy +from frappe import _ +from frappe.utils import flt +from frappe.model.document import Document + +class PutawayRule(Document): + def validate(self): + self.validate_duplicate_rule() + self.validate_warehouse_and_company() + self.validate_capacity() + self.validate_priority() + + def validate_duplicate_rule(self): + existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + if existing_rule and existing_rule != self.name: + frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") + .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), + title=_("Duplicate")) + + def validate_priority(self): + if self.priority < 1: + frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority")) + + def validate_warehouse_and_company(self): + company = frappe.db.get_value("Warehouse", self.warehouse, "company") + if company != self.company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.") + .format(frappe.bold(self.warehouse), frappe.bold(self.company)), + title=_("Invalid Warehouse")) + + def validate_capacity(self): + # check if capacity is lesser than current balance in warehouse + pass + +@frappe.whitelist() +def get_ordered_putaway_rules(item_code, company, qty): + """Returns an ordered list of putaway rules to apply on an item.""" + + # get enabled putaway rules for this item code in this company that have pending capacity + # order the rules by priority first + # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) + # return this list + # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + +@frappe.whitelist() +def apply_putaway_rule(items, company): + """ Applies Putaway Rule on line items. + + items: List of line items in a Purchase Receipt + company: Company in Purchase Receipt + """ + items_not_accomodated = [] + for item in items: + item_qty = item.qty + at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + + if not rules: + if at_capacity: + items_not_accomodated.append([item.item_code, item_qty]) + continue + + item_row_updated = False + for rule in rules: + while item_qty > 0: + if not item_row_updated: + # update pre-existing row + item.qty = rule.qty + item.warehouse = rule.warehouse + item_row_updated = True + else: + # add rows for split quantity + added_row = copy.deepcopy(item) + added_row.qty = rule.qty + added_row.warehouse = rule.warehouse + items.append(added_row) + + item_qty -= flt(rule.qty) + + # if pending qty after applying rules, add row without warehouse + if item_qty > 0: + added_row = copy.deepcopy(item) + added_row.qty = item_qty + added_row.warehouse = '' + items.append(added_row) + items_not_accomodated.append([item.item_code, item_qty]) + + # need to check pricing rule, item tax impact \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js new file mode 100644 index 0000000000..bb1654cf24 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Putaway Rule'] = { + add_fields: ["disable"], + get_indicator: (doc) => { + if (doc.disable) { + return [__("Disabled"), "darkgrey", "disable,=,1"]; + } else { + return [__("Active"), "blue", "disable,=,0"]; + }; + } +}; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py new file mode 100644 index 0000000000..e262217f84 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.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 TestPutawayRule(unittest.TestCase): + pass From c7991f85612a0b3b608e942ec593d9c980b5c302 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 20 Nov 2020 11:53:20 +0530 Subject: [PATCH 02/15] feat: Putaway Rule --- .../doctype/purchase_order/purchase_order.py | 2 + .../doctype/putaway_rule/putaway_rule.json | 4 +- .../doctype/putaway_rule/putaway_rule.py | 104 ++++++++++++------ 3 files changed, 76 insertions(+), 34 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a1..53326fd6b2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,7 +349,9 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule target.ignore_pricing_rule = 1 + target.items = apply_putaway_rule(target.items, target.company) target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 6a132c7e25..0d90c47b50 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -55,7 +55,7 @@ "reqd": 1 }, { - "default": "item_code.stock_uom", + "fetch_from": "item_code.stock_uom", "fieldname": "stock_uom", "fieldtype": "Link", "label": "Stock UOM", @@ -86,7 +86,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-10 17:06:27.151335", + "modified": "2020-11-12 11:20:52.765163", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 9f02833431..1ac76b6c30 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -5,9 +5,11 @@ from __future__ import unicode_literals import frappe import copy +from collections import defaultdict from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, nowdate from frappe.model.document import Document +from erpnext.stock.utils import get_stock_balance class PutawayRule(Document): def validate(self): @@ -35,59 +37,97 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): - # check if capacity is lesser than current balance in warehouse - pass + balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) + if flt(self.capacity) < flt(balance_qty): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") + .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() -def get_ordered_putaway_rules(item_code, company, qty): +def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" + rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + filters={"item_code": item_code, "company": company, "disable": 0}, + order_by="priority asc, capacity desc") - # get enabled putaway rules for this item code in this company that have pending capacity - # order the rules by priority first - # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) - # return this list - # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + if not rules: + return False, None + + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.capacity) - flt(balance_qty) + + if free_space > 0: + rule["free_space"] = free_space + else: + del rule + + if not rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + True, None + + rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) + return False, rules @frappe.whitelist() def apply_putaway_rule(items, company): """ Applies Putaway Rule on line items. - items: List of line items in a Purchase Receipt - company: Company in Purchase Receipt + items: List of Purchase Receipt Item objects + company: Company in the Purchase Receipt """ - items_not_accomodated = [] + items_not_accomodated, updated_table = [], [] + item_wise_rules = defaultdict(list) + for item in items: - item_qty = item.qty - at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + item_qty, item_code = flt(item.qty), item.item_code + if not item_qty: continue + + at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item.item_code, item_qty]) + items_not_accomodated.append([item_code, item_qty]) continue - item_row_updated = False - for rule in rules: - while item_qty > 0: - if not item_row_updated: - # update pre-existing row - item.qty = rule.qty - item.warehouse = rule.warehouse - item_row_updated = True - else: - # add rows for split quantity - added_row = copy.deepcopy(item) - added_row.qty = rule.qty - added_row.warehouse = rule.warehouse - items.append(added_row) + # maintain item wise rules, to handle if item is entered twice + # in the table, due to different price, etc. + if not item_wise_rules[item_code]: + item_wise_rules[item_code] = rules - item_qty -= flt(rule.qty) + for rule in item_wise_rules[item_code]: + # it gets split if rule has lesser qty + # if rule_qty >= pending_qty => allocate pending_qty in row + # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule + if item_qty > 0 and rule.free_space: + to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = rule.warehouse + updated_table.append(new_updated_table_row) + + item_qty -= to_allocate + rule["free_space"] -= to_allocate + if item_qty == 0: break # if pending qty after applying rules, add row without warehouse if item_qty > 0: added_row = copy.deepcopy(item) + added_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 added_row.qty = item_qty added_row.warehouse = '' - items.append(added_row) + updated_table.append(added_row) items_not_accomodated.append([item.item_code, item_qty]) - # need to check pricing rule, item tax impact \ No newline at end of file + if items_not_accomodated: + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

" + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + + return updated_table if updated_table else items + # TODO: check pricing rule, item tax impact \ No newline at end of file From 9596276b95baeaa8fd24be6eb9df8372145aefb5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 10:32:17 +0530 Subject: [PATCH 03/15] fix: Linter and Sider --- erpnext/buying/doctype/purchase_order/purchase_order.py | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule_list.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 53326fd6b2..bb67eb92c0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ - .format(item.item_code, item.idx))) + frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") + .format(item.item_code, item.idx)) def get_schedule_dates(self): for d in self.get('items'): diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index bb1654cf24..e48c415f14 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -5,6 +5,6 @@ frappe.listview_settings['Putaway Rule'] = { return [__("Disabled"), "darkgrey", "disable,=,1"]; } else { return [__("Active"), "blue", "disable,=,0"]; - }; + } } }; From 90598ea19cae9f271e7906ee0753cdddb70070c2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 17:35:13 +0530 Subject: [PATCH 04/15] chore: Multi UOM support for Putaway - Added UOM & conversion factor field in Putaway Rule - Items are split and assigned as per UOM - Handled Whole UOMs too --- .../doctype/putaway_rule/putaway_rule.js | 25 ++++++ .../doctype/putaway_rule/putaway_rule.json | 29 ++++++- .../doctype/putaway_rule/putaway_rule.py | 78 +++++++++++-------- 3 files changed, 96 insertions(+), 36 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index ae08e82c28..00a84b0e8d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -11,7 +11,32 @@ frappe.ui.form.on('Putaway Rule', { } }; }); + }, + + uom: function(frm) { + if(frm.doc.item_code && frm.doc.uom) { + return frm.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { + item_code: frm.doc.item_code, + uom: frm.doc.uom + }, + callback: function(r) { + if(!r.exc) { + let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); + frm.set_value('conversion_factor', r.message.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + } + }); + } + }, + + capacity: function(frm) { + let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); } + // refresh: function(frm) { // } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 0d90c47b50..d5ae68faf3 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -10,17 +10,19 @@ "item_code", "item_name", "warehouse", + "priority", "col_break_capacity", "company", "capacity", - "priority", - "stock_uom" + "uom", + "conversion_factor", + "stock_uom", + "stock_capacity" ], "fields": [ { "fieldname": "item_code", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Item", "options": "Item", @@ -82,11 +84,30 @@ "fieldname": "disable", "fieldtype": "Check", "label": "Disable" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "stock_capacity", + "fieldtype": "Float", + "label": "Capacity in Stock UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-12 11:20:52.765163", + "modified": "2020-11-23 16:53:48.387054", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 1ac76b6c30..53a947f417 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -7,7 +7,7 @@ import frappe import copy from collections import defaultdict from frappe import _ -from frappe.utils import flt, nowdate +from frappe.utils import flt, floor, nowdate from frappe.model.document import Document from erpnext.stock.utils import get_stock_balance @@ -38,14 +38,14 @@ class PutawayRule(Document): def validate_capacity(self): balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.capacity) < flt(balance_qty): + if flt(self.stock_capacity) < flt(balance_qty): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -54,8 +54,7 @@ def get_ordered_putaway_rules(item_code, company): for rule in rules: balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) - free_space = flt(rule.capacity) - flt(balance_qty) - + free_space = flt(rule.stock_capacity) - flt(balance_qty) if free_space > 0: rule["free_space"] = free_space else: @@ -64,7 +63,7 @@ def get_ordered_putaway_rules(item_code, company): if not rules: # After iterating through rules, if no rules are left # then there is not enough space left in any rule - True, None + return True, None rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) return False, rules @@ -79,15 +78,33 @@ def apply_putaway_rule(items, company): items_not_accomodated, updated_table = [], [] item_wise_rules = defaultdict(list) + def add_row(item, to_allocate, warehouse): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = warehouse + updated_table.append(new_updated_table_row) + for item in items: - item_qty, item_code = flt(item.qty), item.item_code - if not item_qty: continue + conversion = flt(item.conversion_factor) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code + + if not pending_qty: + add_row(item, pending_qty, item.warehouse) + continue at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item_code, item_qty]) + # rules available, but no free space + add_row(item, pending_qty, '') + items_not_accomodated.append([item_code, pending_qty]) + else: + # no rules to apply + add_row(item, pending_qty, item.warehouse) continue # maintain item wise rules, to handle if item is entered twice @@ -96,31 +113,28 @@ def apply_putaway_rule(items, company): item_wise_rules[item_code] = rules for rule in item_wise_rules[item_code]: - # it gets split if rule has lesser qty - # if rule_qty >= pending_qty => allocate pending_qty in row - # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule - if item_qty > 0 and rule.free_space: - to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty - new_updated_table_row = copy.deepcopy(item) - new_updated_table_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - new_updated_table_row.qty = to_allocate - new_updated_table_row.warehouse = rule.warehouse - updated_table.append(new_updated_table_row) + if pending_stock_qty > 0 and rule.free_space: + stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + qty_to_allocate = stock_qty_to_allocate / (conversion or 1) - item_qty -= to_allocate - rule["free_space"] -= to_allocate - if item_qty == 0: break + if uom_must_be_whole_number: + qty_to_allocate = floor(qty_to_allocate) + stock_qty_to_allocate = qty_to_allocate * conversion - # if pending qty after applying rules, add row without warehouse - if item_qty > 0: - added_row = copy.deepcopy(item) - added_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - added_row.qty = item_qty - added_row.warehouse = '' - updated_table.append(added_row) - items_not_accomodated.append([item.item_code, item_qty]) + if not qty_to_allocate: break + + add_row(item, qty_to_allocate, rule.warehouse) + + pending_stock_qty -= stock_qty_to_allocate + pending_qty -= qty_to_allocate + rule["free_space"] -= stock_qty_to_allocate + + if not pending_stock_qty: break + + # if pending qty after applying all rules, add row without warehouse + if pending_stock_qty > 0: + add_row(item, pending_qty, '') + items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

  • " From 0cec1477f2044f8b09e7f147749fa7b47d9256e9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 19:19:35 +0530 Subject: [PATCH 05/15] chore: Format unassigned Items dialog and add freeze message --- .../doctype/purchase_order/purchase_order.js | 3 ++- .../doctype/putaway_rule/putaway_rule.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47483c9d1c..20faded9bb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -347,7 +347,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( make_purchase_receipt: function() { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", - frm: cur_frm + frm: cur_frm, + freeze_message: __("Creating Purchase Receipt ...") }) }, diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 53a947f417..cc58def33a 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -42,6 +42,9 @@ class PutawayRule(Document): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + if not self.capacity: + frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" @@ -137,10 +140,26 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    • " - formatted_item_qty = [entry[0] + " : " + str(entry[1]) for entry in items_not_accomodated] - msg += "
    • ".join(formatted_item_qty) - msg += "
    " + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) return updated_table if updated_table else items From ccbd432b56b952e7d40003c15202279379338336 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 12:47:13 +0530 Subject: [PATCH 06/15] chore: Added Tests - Fixed Sider Issues - Added perms to Putaway Rule - Added Unit Tests to check warehouse assignment --- .../doctype/putaway_rule/putaway_rule.js | 4 +- .../doctype/putaway_rule/putaway_rule.json | 27 +- .../doctype/putaway_rule/putaway_rule.py | 7 +- .../doctype/putaway_rule/test_putaway_rule.py | 257 +++++++++++++++++- 4 files changed, 287 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index 00a84b0e8d..e0569206ef 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -14,7 +14,7 @@ frappe.ui.form.on('Putaway Rule', { }, uom: function(frm) { - if(frm.doc.item_code && frm.doc.uom) { + if (frm.doc.item_code && frm.doc.uom) { return frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", args: { @@ -22,7 +22,7 @@ frappe.ui.form.on('Putaway Rule', { uom: frm.doc.uom }, callback: function(r) { - if(!r.exc) { + if (!r.exc) { let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); frm.set_value('conversion_factor', r.message.conversion_factor); frm.set_value('stock_capacity', stock_capacity); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index d5ae68faf3..e5b6b2b98f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -107,7 +107,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 16:53:48.387054", + "modified": "2020-11-23 19:25:50.948068", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -121,7 +121,30 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", "share": 1, "write": 1 } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index cc58def33a..73534aa14f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -17,6 +17,7 @@ class PutawayRule(Document): self.validate_warehouse_and_company() self.validate_capacity() self.validate_priority() + self.set_stock_capacity() def validate_duplicate_rule(self): existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) @@ -45,10 +46,13 @@ class PutawayRule(Document): if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + def set_stock_capacity(self): + self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -86,6 +90,7 @@ def apply_putaway_rule(items, company): new_updated_table_row.name = '' new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 new_updated_table_row.qty = to_allocate + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse updated_table.append(new_updated_table_row) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index e262217f84..7b81784d5f 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,9 +2,260 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - -# import frappe +import frappe import unittest +from frappe.utils import add_days, nowdate +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order class TestPutawayRule(unittest.TestCase): - pass + def setUp(self): + if not frappe.db.exists("Item", "_Rice"): + make_item("_Rice", { + 'is_stock_item': 1, + 'has_batch_no' : 1, + 'create_new_batch': 1, + 'stock_uom': 'Kg' + }) + + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): + create_warehouse("Rack 1") + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): + create_warehouse("Rack 2") + + if not frappe.db.exists("UOM", "Bag"): + new_uom = frappe.new_doc("UOM") + new_uom.uom_name = "Bag" + new_uom.save() + + def test_putaway_rules_priority(self): + """Test if rule is applied by priority, irrespective of free space.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, + uom="Kg", priority=2) + + po = create_purchase_order(item_code="_Rice", qty=300) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_same_priority(self): + """Test if rule with more free space is applied, + among two rules with same priority and capacity.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500, + uom="Kg") + + # out of 500 kg capacity, occupy 100 kg in warehouse_1 + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=700) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 500) + # warehouse_2 has 500 kg free space, it is given priority + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 200) + # warehouse_1 has 400 kg free space, it is given less priority + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_insufficient_capacity(self): + """Test if qty exceeding capacity, is handled.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, + uom="Kg") + + po = create_purchase_order(item_code="_Rice", qty=350) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + # extra qty has no warehouse assigned + self.assertEqual(pr.items[2].qty, 50) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom(self): + """Test rules applied on uom other than stock uom.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 3000) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4, + uom="Bag") + self.assertEqual(rule_2.stock_capacity, 4000) + + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 6000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 4) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 2) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom_whole_uom(self): + """Test if whole UOMs are handled.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + # Putaway Rule in different UOM + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 1000) + # Putaway Rule in Stock UOM + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500) + self.assertEqual(rule_2.stock_capacity, 500) + # total capacity is 1500 Kg + + po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True) + # PO for 2 Bags (2000 Kg) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 2000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 1) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # leftover space was for 500 kg (0.5 Bag) + # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertEqual(pr.items[1].qty, 1) + self.assertEqual(pr.items[1].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_reoccurring_item(self): + """Test rules on same item entered multiple times.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100, + uom="Kg", priority=2) + # total capacity is 300 Kg + + po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True) + po.append("items", { + "item_code":"_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 300, + "rate": 120, + "schedule_date": add_days(nowdate(), 1), + }) + po.save() + po.submit() + # PO for 500 Kg (two rows of same item, different rates) + self.assertEqual(len(po.items), 2) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # same rules applied to second item row + # with previous assignment considered + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + # unassigned 200 Kg + self.assertEqual(pr.items[2].qty, 200) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + +def create_putaway_rule(**args): + args = frappe._dict(args) + putaway = frappe.new_doc("Putaway Rule") + + putaway.disable = args.disable or 0 + putaway.company = args.company or "_Test Company" + putaway.item_code = args.item or args.item_code or "_Test Item" + putaway.warehouse = args.warehouse + putaway.priority = args.priority or 1 + putaway.capacity = args.capacity or 1 + putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") + putaway.uom = args.uom or putaway.stock_uom + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + + if not args.do_not_save: + putaway.save() + + return putaway \ No newline at end of file From 68a49efc8098808386e234c380692791e926b2aa Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 17:38:34 +0530 Subject: [PATCH 07/15] chore: Added Putaway Rule to Desk Page and added Priority to List View --- erpnext/stock/desk_page/stock/stock.json | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3..aa4fc28ec9 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Putaway Rule\",\n \"name\": \"Putaway Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-11-24 15:43:20.496057", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index e5b6b2b98f..325e6f1355 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -68,6 +68,7 @@ "default": "1", "fieldname": "priority", "fieldtype": "Int", + "in_list_view": 1, "label": "Priority" }, { @@ -107,7 +108,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 19:25:50.948068", + "modified": "2020-11-24 16:20:18.306671", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", From 2ed80656aa1be30fb735a85202ce71d62eba6763 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 21:06:43 +0530 Subject: [PATCH 08/15] chore: Code Cleanup - Validate capacity < stock level only on new rule - Mention stock uom while validating capacity in new rule - Separate function to format and display unassigned items - Format ORM args --- .../doctype/putaway_rule/putaway_rule.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 73534aa14f..606e190458 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -38,10 +38,13 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): + stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") - .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + + if flt(self.stock_capacity) < flt(balance_qty) and self.get('__islocal'): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") + .format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity")) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -49,10 +52,10 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) -@frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -145,27 +148,29 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " - formatted_item_rows = "" - - for entry in items_not_accomodated: - item_link = frappe.utils.get_link_to_form("Item", entry[0]) - formatted_item_rows += """ - {0} - {1} - """.format(item_link, frappe.bold(entry[1])) - - msg += """ - - - - - - {2} -
    {0}{1}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) - - frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + format_unassigned_items_error(items_not_accomodated) return updated_table if updated_table else items - # TODO: check pricing rule, item tax impact \ No newline at end of file + +def format_unassigned_items_error(items_not_accomodated): + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) \ No newline at end of file From 1087d97c03a0ea9973ffc0d70472c4a3fcac8654 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Nov 2020 10:45:44 +0530 Subject: [PATCH 09/15] feat: Warehouse Capacity Summary - Added Page Warehouse Capacity Summary - Added Page to Desk and Putaway List View - Reused Item Dashboard/Stock Balance page render code - Added naming series to Putaway Rule --- erpnext/public/build.json | 4 +- erpnext/stock/dashboard/item_dashboard.js | 75 ++++++++--- .../dashboard/warehouse_capacity_dashboard.py | 69 ++++++++++ erpnext/stock/desk_page/stock/stock.json | 4 +- erpnext/stock/doctype/item/item.js | 5 +- .../purchase_receipt/purchase_receipt.json | 9 +- .../doctype/putaway_rule/putaway_rule.json | 8 +- .../doctype/putaway_rule/putaway_rule_list.js | 10 +- .../stock/page/stock_balance/stock_balance.js | 3 + .../warehouse_capacity_summary/__init__.py | 0 .../warehouse_capacity_summary.html | 40 ++++++ .../warehouse_capacity_summary.js | 120 ++++++++++++++++++ .../warehouse_capacity_summary.json | 26 ++++ .../warehouse_capacity_summary_header.html | 19 +++ 14 files changed, 367 insertions(+), 25 deletions(-) create mode 100644 erpnext/stock/dashboard/warehouse_capacity_dashboard.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/__init__.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269..8b18a1fcfb 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -54,6 +54,8 @@ "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" + "stock/dashboard/item_dashboard.js", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html" ] } diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 9bd03d45cb..abc286fcc6 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({ handle_move_add($(this), "Add") }); + this.content.on('click', '.btn-edit', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + let company = unescape($(this).attr('data-company')); + frappe.db.get_value('Putaway Rule', + {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + }); + }); + function handle_move_add(element, action) { let item = unescape(element.attr('data-item')); let warehouse = unescape(element.attr('data-warehouse')); @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += 20; + me.start += this.page_length; me.refresh(); }); @@ -69,33 +79,41 @@ erpnext.stock.ItemDashboard = Class.extend({ this.before_refresh(); } + let args = { + item_code: this.item_code, + warehouse: this.warehouse, + parent_warehouse: this.parent_warehouse, + item_group: this.item_group, + company: this.company, + start: this.start, + sort_by: this.sort_by, + sort_order: this.sort_order + } + var me = this; frappe.call({ - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - args: { - item_code: this.item_code, - warehouse: this.warehouse, - item_group: this.item_group, - start: this.start, - sort_by: this.sort_by, - sort_order: this.sort_order, - }, + method: this.method, + args: args, callback: function(r) { me.render(r.message); } }); }, render: function(data) { - if(this.start===0) { + if (this.start===0) { this.max_count = 0; this.result.empty(); } + if (this.page_name === "warehouse-capacity-summary") { + var context = this.get_capacity_dashboard_data(data); + } else { + var context = this.get_item_dashboard_data(data, this.max_count, true); + } - var context = this.get_item_dashboard_data(data, this.max_count, true); this.max_count = this.max_count; // show more button - if(data && data.length===21) { + if (data && data.length===(this.page_length + 1)) { this.content.find('.more').removeClass('hidden'); // remove the last element @@ -106,12 +124,17 @@ erpnext.stock.ItemDashboard = Class.extend({ // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); + this.content.find('.result').css('text-align', 'unset'); + $(frappe.render_template(this.template, context)).appendTo(this.result); } else { - var message = __("Currently no stock available in any warehouse"); - $(` ${message} `).appendTo(this.result); + var message = __("No Stock Available Currently"); + this.content.find('.result').css('text-align', 'center'); + + $(`
    + ${message}
    `).appendTo(this.result); } }, + get_item_dashboard_data: function(data, max_count, show_item) { if(!max_count) max_count = 0; if(!data) data = []; @@ -128,7 +151,7 @@ erpnext.stock.ItemDashboard = Class.extend({ d.total_reserved, max_count); }); - var can_write = 0; + let can_write = 0; if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ can_write = 1; } @@ -139,6 +162,24 @@ erpnext.stock.ItemDashboard = Class.extend({ can_write:can_write, show_item: show_item || false } + }, + + get_capacity_dashboard_data: function(data) { + if(!data) data = []; + + data.forEach(function(d) { + d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; + }); + + let can_write = 0; + if(frappe.boot.user.can_write.indexOf("Putaway Rule")>=0){ + can_write = 1; + } + + return { + data: data, + can_write: can_write, + } } }) diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py new file mode 100644 index 0000000000..ab573e566a --- /dev/null +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import frappe +from frappe.model.db_query import DatabaseQuery +from frappe.utils import nowdate +from frappe.utils import flt +from erpnext.stock.utils import get_stock_balance + +@frappe.whitelist() +def get_data(item_code=None, warehouse=None, parent_warehouse=None, + company=None, start=0, sort_by="stock_capacity", sort_order="desc"): + """Return data to render the warehouse capacity dashboard.""" + filters = get_filters(item_code, warehouse, parent_warehouse, company) + + no_permission, filters = get_warehouse_filter_based_on_permissions(filters) + if no_permission: + return [] + + capacity_data = get_warehouse_capacity_data(filters, start) + + asc_desc = -1 if sort_order == "desc" else 1 + capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + + return capacity_data + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, + company=None): + filters = [['disable', '=', 0]] + if item_code: + filters.append(['item_code', '=', item_code]) + if warehouse: + filters.append(['warehouse', '=', warehouse]) + if company: + filters.append(['company', '=', company]) + if parent_warehouse: + lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) + warehouses = frappe.db.sql_list(""" + select name from `tabWarehouse` + where lft >=%s and rgt<=%s + """, (lft, rgt)) + filters.append(['warehouse', 'in', warehouses]) + return filters + +def get_warehouse_filter_based_on_permissions(filters): + try: + # check if user has any restrictions based on user permissions on warehouse + if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): + filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + return False, filters + except frappe.PermissionError: + # user does not have access on warehouse + return True, [] + +def get_warehouse_capacity_data(filters, start): + capacity_data = frappe.db.get_all('Putaway Rule', + fields=['item_code', 'warehouse','stock_capacity', 'company'], + filters=filters, + limit_start=start, + limit_page_length='11' + ) + + for entry in capacity_data: + balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 + entry.update({ + 'actual_qty': balance_qty, + 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) + }) + + return capacity_data \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index aa4fc28ec9..0038c0a971 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Stock Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Putaway Rule\"\n ],\n \"label\": \"Warehouse Capacity Summary\",\n \"name\": \"warehouse-capacity-summary\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-11-24 15:43:20.496057", + "modified": "2020-11-26 10:43:48.286663", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index faeeb578fe..ec32b0f044 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -384,7 +384,10 @@ $.extend(erpnext.item, { ' + __("Stock Levels") + ''); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, - item_code: frm.doc.name + item_code: frm.doc.name, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }); erpnext.item.item_dashboard.refresh(); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 13c8ceb759..7213eb8616 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,6 +21,7 @@ "posting_date", "posting_time", "set_posting_time", + "apply_putaway_rule", "is_return", "return_against", "section_addresses", @@ -1104,13 +1105,19 @@ "fieldtype": "Small Text", "label": "Billing Address", "read_only": 1 + }, + { + "default": "0", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-10-30 14:00:08.347534", + "modified": "2020-11-25 18:31:32.234503", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 325e6f1355..a003f4986f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:{item_code}-{warehouse}", + "autoname": "PUT-.####", "creation": "2020-11-09 11:39:46.489501", "doctype": "DocType", "editable_grid": 1, @@ -90,12 +90,14 @@ "fieldname": "uom", "fieldtype": "Link", "label": "UOM", + "no_copy": 1, "options": "UOM" }, { "fieldname": "stock_capacity", "fieldtype": "Float", "label": "Capacity in Stock UOM", + "no_copy": 1, "read_only": 1 }, { @@ -103,12 +105,13 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "no_copy": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-24 16:20:18.306671", + "modified": "2020-11-25 20:39:19.973437", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -152,5 +155,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "item_code", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index e48c415f14..725e91ee8d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -6,5 +6,13 @@ frappe.listview_settings['Putaway Rule'] = { } else { return [__("Active"), "blue", "disable,=,0"]; } - } + }, + + reports: [ + { + name: 'Warehouse Capacity Summary', + report_type: 'Page', + route: 'warehouse-capacity-summary' + } + ] }; diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index da21c6bc64..bddffd465e 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { frappe.require('assets/js/item-dashboard.min.js', function() { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }) page.item_dashboard.before_refresh = function() { diff --git a/erpnext/stock/page/warehouse_capacity_summary/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html new file mode 100644 index 0000000000..90112c78a8 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -0,0 +1,40 @@ +{% for d in data %} +
    +
    + + +
    + {{ d.stock_capacity }} +
    +
    + {{ d.actual_qty }} +
    +
    +
    +
    +
    +
    +
    +
    + {{ d.percent_occupied }}% +
    + {% if can_write %} +
    +
    + {% endif %} +
    +
    +{% endfor %} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js new file mode 100644 index 0000000000..c3b3b5d8ec --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -0,0 +1,120 @@ +frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Warehouse Capacity Summary', + single_column: true + }); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.start = 0; + + page.company_field = page.add_field({ + fieldname: 'company', + label: __('Company'), + fieldtype:'Link', + options:'Company', + reqd: 1, + default: frappe.defaults.get_default("company"), + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.warehouse_field = page.add_field({ + fieldname: 'warehouse', + label: __('Warehouse'), + fieldtype:'Link', + options:'Warehouse', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.item_field = page.add_field({ + fieldname: 'item_code', + label: __('Item'), + fieldtype:'Link', + options:'Item', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.parent_warehouse_field = page.add_field({ + fieldname: 'parent_warehouse', + label: __('Parent Warehouse'), + fieldtype:'Link', + options:'Warehouse', + get_query: function() { + return { + filters: { + "is_group": 1 + } + }; + }, + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.sort_selector = new frappe.ui.SortSelector({ + parent: page.wrapper.find('.page-form'), + args: { + sort_by: 'stock_capacity', + sort_order: 'desc', + options: [ + {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, + {fieldname: 'percent_occupied', label:__('% Occupied')}, + {fieldname: 'actual_qty', label:__('Balance Qty (Stock ')} + ] + }, + change: function(sort_by, sort_order) { + page.capacity_dashboard.sort_by = sort_by; + page.capacity_dashboard.sort_order = sort_order; + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + frappe.require('assets/js/item-dashboard.min.js', function() { + $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + + page.capacity_dashboard = new erpnext.stock.ItemDashboard({ + page_name: "warehouse-capacity-summary", + page_length: 10, + parent: page.main, + sort_by: 'stock_capacity', + sort_order: 'desc', + method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', + template: 'warehouse_capacity_summary' + }) + + page.capacity_dashboard.before_refresh = function() { + this.item_code = page.item_field.get_value(); + this.warehouse = page.warehouse_field.get_value(); + this.parent_warehouse = page.parent_warehouse_field.get_value(); + this.company = page.company_field.get_value(); + } + + page.capacity_dashboard.refresh(); + + let setup_click = function(doctype) { + page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { + var name = $(this).attr('data-name'); + var field = page[doctype.toLowerCase() + '_field']; + if(field.get_value()===name) { + frappe.set_route('Form', doctype, name) + } else { + field.set_input(name); + page.capacity_dashboard.refresh(); + } + }); + } + + setup_click('Item'); + setup_click('Warehouse'); + }); +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json new file mode 100644 index 0000000000..a6e5b45332 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-11-25 12:07:54.056208", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-11-25 11:07:54.056208", + "modified_by": "Administrator", + "module": "Stock", + "name": "warehouse-capacity-summary", + "owner": "Administrator", + "page_name": "Warehouse Capacity Summary", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Warehouse Capacity Summary" +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html new file mode 100644 index 0000000000..acaf180a90 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -0,0 +1,19 @@ +
    +
    +
    + Warehouse +
    +
    + Item +
    +
    + Stock Capacity +
    +
    + Balance Stock Qty +
    +
    + % Occupied +
    +
    +
    \ No newline at end of file From fac4035f23b9b655da466ba6a436533b821b61d8 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Dec 2020 21:35:49 +0530 Subject: [PATCH 10/15] feat: Apply Putaway Rules within transaction itself - Added checkbox 'Apply Putaway Rule' in PR and SE - Added link to rule in child tables - Rule is applied on Save - Validation for over receipt - Apply Rule on Stock Entry as well for Material Transfer and Receipt --- .../doctype/purchase_order/purchase_order.py | 2 - erpnext/controllers/stock_controller.py | 38 +++++ erpnext/public/js/controllers/buying.js | 23 +++ .../purchase_receipt/purchase_receipt.js | 4 + .../purchase_receipt/purchase_receipt.py | 7 + .../purchase_receipt_item.json | 12 +- .../doctype/putaway_rule/putaway_rule.py | 153 ++++++++++++------ .../stock/doctype/stock_entry/stock_entry.js | 4 + .../doctype/stock_entry/stock_entry.json | 10 +- .../stock/doctype/stock_entry/stock_entry.py | 8 + .../stock_entry_detail.json | 14 +- 11 files changed, 217 insertions(+), 58 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index bb67eb92c0..d32e98e8d9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,9 +349,7 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): - from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule target.ignore_pricing_rule = 1 - target.items = apply_putaway_rule(target.items, target.company) target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2d2fff8fd5..c7fadde16e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,6 +6,7 @@ import frappe, erpnext from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults +from collections import defaultdict from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController @@ -23,6 +24,7 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.validate_putaway_capacity() def make_gl_entries(self, gl_entries=None): if self.docstatus == 2: @@ -399,6 +401,42 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def validate_putaway_capacity(self): + # if over receipt is attempted while 'apply putaway rule' is disabled + # and if rule was applied on the transaction, validate it. + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry") + rule_applied = any(item.get("putaway_rule") for item in self.get("items")) + + if valid_doctype and rule_applied and not self.apply_putaway_rule: + rule_map = defaultdict(dict) + for item in self.get("items"): + if item.get("putaway_rule"): + rule = item.get("putaway_rule") + disabled = frappe.db.get_value("Putaway Rule", rule, "disable") + if disabled: return # dont validate for disabled rule + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + if not rule_map[rule]: + rule_map[rule]["warehouse"] = item.get(warehouse_field) + rule_map[rule]["item"] = item.get("item_code") + rule_map[rule]["qty_put"] = 0 + rule_map[rule]["capacity"] = get_putaway_capacity(rule) + rule_map[rule]["qty_put"] += flt(stock_qty) + + for rule, values in rule_map.items(): + if flt(values["qty_put"]) > flt(values["capacity"]): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

    " + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + frappe.throw(msg=message, title=_("Over Receipt")) + return rule_map + def compare_existing_and_expected_gle(existing_gle, expected_gle): matched = True for entry in expected_gle: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58ac38f0a8..ac48d451b9 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -524,3 +524,26 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { dialog.show(); } + +erpnext.apply_putaway_rule = (frm) => { + if (!frm.doc.company) { + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}) + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + items: frm.doc.items, + company: frm.doc.company + }, + callback: (result) => { + if(!result.exc) { + if(result.message) { + frm.doc.items = result.message; + frm.get_field("items").refresh(); + } + } + } + }); +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bc1d81d356..45eb646d4b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -213,6 +213,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }); }, + apply_putaway_rule: function() { + // if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + } + }); // for backward compatibility: combine new and previous states diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 2cc4679c8c..511bae6f58 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -70,6 +70,12 @@ class PurchaseReceipt(BuyingController): where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" }) + def before_save(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + + if self.get("items") and self.apply_putaway_rule: + self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() @@ -90,6 +96,7 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + def validate_cwip_accounts(self): for item in self.get('items'): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index c1e1f901ba..fcbf6ccf6e 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -72,6 +72,7 @@ "purchase_order_item", "material_request_item", "purchase_receipt_item", + "putaway_rule", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -834,12 +835,21 @@ "collapsible": 1, "fieldname": "image_column", "fieldtype": "Column Break" + }, + { + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-28 19:01:21.154963", + "modified": "2020-11-26 12:16:14.897160", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 606e190458..8838bb75f1 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -5,11 +5,14 @@ from __future__ import unicode_literals import frappe import copy +import json from collections import defaultdict +from six import string_types from frappe import _ -from frappe.utils import flt, floor, nowdate +from frappe.utils import flt, floor, nowdate, cint from frappe.model.document import Document from erpnext.stock.utils import get_stock_balance +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class PutawayRule(Document): def validate(self): @@ -52,70 +55,50 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) -def get_ordered_putaway_rules(item_code, company): - """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", - fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], - filters={"item_code": item_code, "company": company, "disable": 0}, - order_by="priority asc, capacity desc") - - if not rules: - return False, None - - for rule in rules: - balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) - free_space = flt(rule.stock_capacity) - flt(balance_qty) - if free_space > 0: - rule["free_space"] = free_space - else: - del rule - - if not rules: - # After iterating through rules, if no rules are left - # then there is not enough space left in any rule - return True, None - - rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) - return False, rules +@frappe.whitelist() +def get_putaway_capacity(rule): + stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, + ["stock_capacity", "item_code", "warehouse"]) + balance_qty = get_stock_balance(item_code, warehouse, nowdate()) + free_space = flt(stock_capacity) - flt(balance_qty) + return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(items, company): +def apply_putaway_rule(doctype, items, company): """ Applies Putaway Rule on line items. items: List of Purchase Receipt Item objects company: Company in the Purchase Receipt """ + if isinstance(items, string_types): + items = json.loads(items) + items_not_accomodated, updated_table = [], [] item_wise_rules = defaultdict(list) - def add_row(item, to_allocate, warehouse): - new_updated_table_row = copy.deepcopy(item) - new_updated_table_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - new_updated_table_row.qty = to_allocate - new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) - new_updated_table_row.warehouse = warehouse - updated_table.append(new_updated_table_row) - for item in items: - conversion = flt(item.conversion_factor) - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') - pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code + if isinstance(item, dict): + item = frappe._dict(item) - if not pending_qty: - add_row(item, pending_qty, item.warehouse) + source_warehouse = item.get("s_warehouse") + serial_nos = get_serial_nos(item.get("serial_no")) + conversion = flt(item.conversion_factor) or 1 + pending_qty, item_code = flt(item.qty), item.item_code + pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules(item_code, company) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: + warehouse = item.warehouse if at_capacity: - # rules available, but no free space - add_row(item, pending_qty, '') + warehouse = '' # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) - else: - # no rules to apply - add_row(item, pending_qty, item.warehouse) + updated_table = add_row(item, pending_qty, warehouse, updated_table) continue # maintain item wise rules, to handle if item is entered twice @@ -126,7 +109,7 @@ def apply_putaway_rule(items, company): for rule in item_wise_rules[item_code]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty - qty_to_allocate = stock_qty_to_allocate / (conversion or 1) + qty_to_allocate = stock_qty_to_allocate / (conversion) if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) @@ -134,7 +117,8 @@ def apply_putaway_rule(items, company): if not qty_to_allocate: break - add_row(item, qty_to_allocate, rule.warehouse) + updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, + rule.name, serial_nos=serial_nos) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate @@ -144,15 +128,71 @@ def apply_putaway_rule(items, company): # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: - add_row(item, pending_qty, '') + # updated_table = add_row(item, pending_qty, '', updated_table, serial_nos=serial_nos) items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - format_unassigned_items_error(items_not_accomodated) + show_unassigned_items_message(items_not_accomodated) return updated_table if updated_table else items -def format_unassigned_items_error(items_not_accomodated): +def get_ordered_putaway_rules(item_code, company, source_warehouse=None): + """Returns an ordered list of putaway rules to apply on an item.""" + filters = { + "item_code": item_code, + "company": company, + "disable": 0 + } + if source_warehouse: + filters.update({"warehouse": ["!=", source_warehouse]}) + + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + filters=filters, + order_by="priority asc, capacity desc") + + if not rules: + return False, None + + vacant_rules = [] + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.stock_capacity) - flt(balance_qty) + if free_space > 0: + rule["free_space"] = free_space + vacant_rules.append(rule) + + if not vacant_rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + return True, None + + vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + + return False, vacant_rules + +def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 + new_updated_table_row.name = "New " + str(item.doctype) + " " + str(new_updated_table_row.idx) + new_updated_table_row.qty = to_allocate + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + if item.doctype == "Stock Entry Detail": + new_updated_table_row.t_warehouse = warehouse + else: + new_updated_table_row.warehouse = warehouse + new_updated_table_row.rejected_qty = 0 + new_updated_table_row.received_qty = to_allocate + + if rule: + new_updated_table_row.putaway_rule = rule + if serial_nos: + new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) + + updated_table.append(new_updated_table_row) + return updated_table + +def show_unassigned_items_message(items_not_accomodated): msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " formatted_item_rows = "" @@ -173,4 +213,11 @@ def format_unassigned_items_error(items_not_accomodated): """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) - frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) \ No newline at end of file + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + +def get_serial_nos_to_allocate(serial_nos, to_allocate): + if serial_nos: + allocated_serial_nos = serial_nos[0: cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" + else: return "" \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca..2be70f37f2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -571,6 +571,10 @@ frappe.ui.form.on('Stock Entry', { } }); } + }, + + apply_putaway_rule: function(frm) { + // if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); } }) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 61e0df6723..cd01fa7277 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,6 +27,7 @@ "set_posting_time", "inspection_required", "from_bom", + "apply_putaway_rule", "sb1", "bom_no", "fg_completed_qty", @@ -640,13 +641,20 @@ "fieldtype": "Check", "label": "Add to Transit", "no_copy": 1 + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], doc.purpose)", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-11 19:10:07.954981", + "modified": "2020-12-07 14:58:13.267321", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3159b95c3..aa3425cbd8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -42,6 +42,13 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) + def before_save(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + if self.get("items") and apply_rule: + self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.pro_doc = frappe._dict() if self.work_order: @@ -79,6 +86,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount(update_finished_item_rate=False) + self.validate_putaway_capacity() def on_submit(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 79e8f9af8f..4075e28b7b 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -61,6 +62,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "putaway_rule", "column_break_51", "transferred_qty", "reference_purchase_receipt", @@ -498,13 +500,23 @@ "fieldname": "set_basic_rate_manually", "fieldtype": "Check", "label": "Set Basic Rate Manually" + }, + { + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], parent.purpose)", + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-23 17:55:03.384138", + "modified": "2020-12-07 15:00:44.489442", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 0f3cfc502bc81a475cf5f6b8beabfd43730fe5b8 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Dec 2020 19:11:51 +0530 Subject: [PATCH 11/15] feat: Trigger rule application from client side - Table is reset and overwritten with applied rules on checkbox trigger - Sider fixes --- erpnext/public/js/controllers/buying.js | 23 ------------- erpnext/public/js/controllers/transaction.js | 30 +++++++++++++++++ erpnext/stock/dashboard/item_dashboard.js | 22 +++++++------ .../purchase_receipt/purchase_receipt.js | 2 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../doctype/putaway_rule/putaway_rule.py | 22 +++++++------ .../stock/doctype/stock_entry/stock_entry.js | 6 ++-- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../warehouse_capacity_summary.js | 32 +++++++++---------- 9 files changed, 77 insertions(+), 64 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1cb68a6cda..0c6bcad721 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -516,27 +516,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); -} - -erpnext.apply_putaway_rule = (frm) => { - if (!frm.doc.company) { - frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}) - } - if (!frm.doc.items.length) return; - - frappe.call({ - method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", - args: { - items: frm.doc.items, - company: frm.doc.company - }, - callback: (result) => { - if(!result.exc) { - if(result.message) { - frm.doc.items = result.message; - frm.get_field("items").refresh(); - } - } - } - }); } \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7f08cd1359..31efb6aa34 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2029,3 +2029,33 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }, show_dialog); }); } + +erpnext.apply_putaway_rule = (frm) => { + if (!frm.doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + doctype: frm.doctype, + items: frm.doc.items, + company: frm.doc.company, + sync: true + }, + callback: (result) => { + if (!result.exc && result.message) { + frm.clear_table("items"); + + let items = result.message; + items.forEach((row) => { + delete row["name"]; + let child = frm.add_child("items"); + Object.assign(child, row); + }); + frm.get_field("items").grid.refresh(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index abc286fcc6..070589b3dc 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -30,8 +30,8 @@ erpnext.stock.ItemDashboard = Class.extend({ let company = unescape($(this).attr('data-company')); frappe.db.get_value('Putaway Rule', {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { - frappe.set_route("Form", "Putaway Rule", r.name); - }); + frappe.set_route("Form", "Putaway Rule", r.name); + }); }); function handle_move_add(element, action) { @@ -88,7 +88,7 @@ erpnext.stock.ItemDashboard = Class.extend({ start: this.start, sort_by: this.sort_by, sort_order: this.sort_order - } + }; var me = this; frappe.call({ @@ -104,10 +104,12 @@ erpnext.stock.ItemDashboard = Class.extend({ this.max_count = 0; this.result.empty(); } + + let context = ""; if (this.page_name === "warehouse-capacity-summary") { - var context = this.get_capacity_dashboard_data(data); + context = this.get_capacity_dashboard_data(data); } else { - var context = this.get_item_dashboard_data(data, this.max_count, true); + context = this.get_item_dashboard_data(data, this.max_count, true); } this.max_count = this.max_count; @@ -152,7 +154,7 @@ erpnext.stock.ItemDashboard = Class.extend({ }); let can_write = 0; - if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ + if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) { can_write = 1; } @@ -165,23 +167,23 @@ erpnext.stock.ItemDashboard = Class.extend({ }, get_capacity_dashboard_data: function(data) { - if(!data) data = []; + if (!data) data = []; data.forEach(function(d) { d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; }); let can_write = 0; - if(frappe.boot.user.can_write.indexOf("Putaway Rule")>=0){ + if (frappe.boot.user.can_write.indexOf("Putaway Rule") >= 0) { can_write = 1; } return { data: data, can_write: can_write, - } + }; } -}) +}); erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 45eb646d4b..218fb9e121 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -214,7 +214,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }, apply_putaway_rule: function() { - // if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); } }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b1ad6103d3..4372bdcc59 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -87,7 +87,7 @@ class PurchaseReceipt(BuyingController): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule if self.get("items") and self.apply_putaway_rule: - self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): self.validate_posting_time() diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 8838bb75f1..4ed4dafac7 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -64,7 +64,7 @@ def get_putaway_capacity(rule): return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(doctype, items, company): +def apply_putaway_rule(doctype, items, company, sync=None): """ Applies Putaway Rule on line items. items: List of Purchase Receipt Item objects @@ -82,7 +82,7 @@ def apply_putaway_rule(doctype, items, company): source_warehouse = item.get("s_warehouse") serial_nos = get_serial_nos(item.get("serial_no")) - conversion = flt(item.conversion_factor) or 1 + item.conversion_factor = flt(item.conversion_factor) or 1 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) if not pending_qty or not item_code: @@ -109,11 +109,11 @@ def apply_putaway_rule(doctype, items, company): for rule in item_wise_rules[item_code]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty - qty_to_allocate = stock_qty_to_allocate / (conversion) + qty_to_allocate = stock_qty_to_allocate / item.conversion_factor if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) - stock_qty_to_allocate = qty_to_allocate * conversion + stock_qty_to_allocate = qty_to_allocate * item.conversion_factor if not qty_to_allocate: break @@ -124,17 +124,19 @@ def apply_putaway_rule(doctype, items, company): pending_qty -= qty_to_allocate rule["free_space"] -= stock_qty_to_allocate - if not pending_stock_qty: break + if not pending_stock_qty > 0: break # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: - # updated_table = add_row(item, pending_qty, '', updated_table, serial_nos=serial_nos) items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: show_unassigned_items_message(items_not_accomodated) - return updated_table if updated_table else items + items[:] = updated_table if updated_table else items # modify items table + + if sync and json.loads(sync): # sync with client side + return items def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" @@ -174,12 +176,14 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 - new_updated_table_row.name = "New " + str(item.doctype) + " " + str(new_updated_table_row.idx) + new_updated_table_row.name = None new_updated_table_row.qty = to_allocate - new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse + new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) else: + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse new_updated_table_row.rejected_qty = 0 new_updated_table_row.received_qty = to_allocate diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 2be70f37f2..fd920a585a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -573,10 +573,10 @@ frappe.ui.form.on('Stock Entry', { } }, - apply_putaway_rule: function(frm) { - // if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); + apply_putaway_rule: function (frm) { + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); } -}) +}); frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aa3425cbd8..f07039f564 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -47,7 +47,7 @@ class StockEntry(StockController): apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) if self.get("items") and apply_rule: - self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): self.pro_doc = frappe._dict() diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index c3b3b5d8ec..b610e7dd58 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -10,8 +10,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.company_field = page.add_field({ fieldname: 'company', label: __('Company'), - fieldtype:'Link', - options:'Company', + fieldtype: 'Link', + options: 'Company', reqd: 1, default: frappe.defaults.get_default("company"), change: function() { @@ -23,8 +23,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.warehouse_field = page.add_field({ fieldname: 'warehouse', label: __('Warehouse'), - fieldtype:'Link', - options:'Warehouse', + fieldtype: 'Link', + options: 'Warehouse', change: function() { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); @@ -34,8 +34,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.item_field = page.add_field({ fieldname: 'item_code', label: __('Item'), - fieldtype:'Link', - options:'Item', + fieldtype: 'Link', + options: 'Item', change: function() { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); @@ -45,8 +45,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.parent_warehouse_field = page.add_field({ fieldname: 'parent_warehouse', label: __('Parent Warehouse'), - fieldtype:'Link', - options:'Warehouse', + fieldtype: 'Link', + options: 'Warehouse', get_query: function() { return { filters: { @@ -67,8 +67,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { sort_order: 'desc', options: [ {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, - {fieldname: 'percent_occupied', label:__('% Occupied')}, - {fieldname: 'actual_qty', label:__('Balance Qty (Stock ')} + {fieldname: 'percent_occupied', label: __('% Occupied')}, + {fieldname: 'actual_qty', label: __('Balance Qty (Stock ')} ] }, change: function(sort_by, sort_order) { @@ -90,14 +90,14 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { sort_order: 'desc', method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', template: 'warehouse_capacity_summary' - }) + }); page.capacity_dashboard.before_refresh = function() { this.item_code = page.item_field.get_value(); this.warehouse = page.warehouse_field.get_value(); this.parent_warehouse = page.parent_warehouse_field.get_value(); this.company = page.company_field.get_value(); - } + }; page.capacity_dashboard.refresh(); @@ -105,16 +105,16 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { var name = $(this).attr('data-name'); var field = page[doctype.toLowerCase() + '_field']; - if(field.get_value()===name) { - frappe.set_route('Form', doctype, name) + if (field.get_value()===name) { + frappe.set_route('Form', doctype, name); } else { field.set_input(name); page.capacity_dashboard.refresh(); } }); - } + }; setup_click('Item'); setup_click('Warehouse'); }); -} \ No newline at end of file +}; \ No newline at end of file From a5d8d3277560c794377a2449e534a1354dfba9a3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Dec 2020 16:27:18 +0530 Subject: [PATCH 12/15] chore: Test and fixes - Tests as per new design flow - Fixed duplicate data bug in Warehouse Capacity Summary - Set Amount currently on applying rule in client side - Apply rules on server side before validate --- erpnext/public/js/controllers/transaction.js | 3 +- erpnext/stock/dashboard/item_dashboard.js | 2 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 2 + .../doctype/putaway_rule/test_putaway_rule.py | 135 +++++++++--------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 6 files changed, 72 insertions(+), 74 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 31efb6aa34..54634482b0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2050,9 +2050,10 @@ erpnext.apply_putaway_rule = (frm) => { let items = result.message; items.forEach((row) => { - delete row["name"]; + delete row["name"]; // dont overwrite name from server side let child = frm.add_child("items"); Object.assign(child, row); + frm.script_manager.trigger("qty", child.doctype, child.name); }); frm.get_field("items").grid.refresh(); } diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 070589b3dc..20415bc07e 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -69,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += this.page_length; + me.start += me.page_length; me.refresh(); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4372bdcc59..d36d5159db 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -83,7 +83,7 @@ class PurchaseReceipt(BuyingController): } ]) - def before_save(self): + def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule if self.get("items") and self.apply_putaway_rule: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9b8eeed1a1..7b3a83065b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -979,6 +979,7 @@ def make_purchase_receipt(**args): pr.currency = args.currency or "INR" pr.is_return = args.is_return pr.return_against = args.return_against + pr.apply_putaway_rule = args.apply_putaway_rule qty = args.qty or 5 received_qty = args.received_qty or qty rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) @@ -994,6 +995,7 @@ def make_purchase_receipt(**args): "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 7b81784d5f..17619e01bc 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -9,8 +9,8 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestPutawayRule(unittest.TestCase): def setUp(self): @@ -42,17 +42,15 @@ class TestPutawayRule(unittest.TestCase): rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, uom="Kg", priority=2) - po = create_purchase_order(item_code="_Rice", qty=300) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, warehouse_1) self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, warehouse_2) - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -70,10 +68,8 @@ class TestPutawayRule(unittest.TestCase): # out of 500 kg capacity, occupy 100 kg in warehouse_1 stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) - po = create_purchase_order(item_code="_Rice", qty=700) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, + do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority @@ -82,8 +78,8 @@ class TestPutawayRule(unittest.TestCase): # warehouse_1 has 400 kg free space, it is given less priority self.assertEqual(pr.items[1].warehouse, warehouse_1) - po.cancel() stock_receipt.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -97,21 +93,14 @@ class TestPutawayRule(unittest.TestCase): rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, uom="Kg") - po = create_purchase_order(item_code="_Rice", qty=350) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) - - self.assertEqual(len(pr.items), 3) + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, warehouse_2) self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, warehouse_1) - # extra qty has no warehouse assigned - self.assertEqual(pr.items[2].qty, 50) - self.assertEqual(pr.items[2].warehouse, '') - - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -135,24 +124,19 @@ class TestPutawayRule(unittest.TestCase): uom="Bag") self.assertEqual(rule_2.stock_capacity, 4000) + # populate 'Rack 1' with 1 Bag, making the free space 2 Bags stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) - po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True) - po.items[0].uom = "Bag" - po.save() - po.submit() - - self.assertEqual(po.items[0].stock_qty, 6000) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) self.assertEqual(pr.items[0].warehouse, warehouse_2) self.assertEqual(pr.items[1].qty, 2) self.assertEqual(pr.items[1].warehouse, warehouse_1) - po.cancel() stock_receipt.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -180,24 +164,15 @@ class TestPutawayRule(unittest.TestCase): self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg - po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True) - # PO for 2 Bags (2000 Kg) - po.items[0].uom = "Bag" - po.save() - po.submit() - - self.assertEqual(po.items[0].stock_qty, 2000) - - pr = make_purchase_receipt(po.name) - self.assertEqual(len(pr.items), 2) + pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) self.assertEqual(pr.items[0].warehouse, warehouse_1) # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned - self.assertEqual(pr.items[1].qty, 1) - self.assertEqual(pr.items[1].warehouse, '') - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -208,38 +183,58 @@ class TestPutawayRule(unittest.TestCase): rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100, - uom="Kg", priority=2) - # total capacity is 300 Kg + # total capacity is 200 Kg - po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True) - po.append("items", { - "item_code":"_Rice", + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, + do_not_submit=1) + pr.append("items", { + "item_code": "_Rice", "warehouse": "_Test Warehouse - _TC", - "qty": 300, - "rate": 120, - "schedule_date": add_days(nowdate(), 1), - }) - po.save() - po.submit() - # PO for 500 Kg (two rows of same item, different rates) - self.assertEqual(len(po.items), 2) - - pr = make_purchase_receipt(po.name) - self.assertEqual(len(pr.items), 3) - self.assertEqual(pr.items[0].qty, 200) + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }) # same item entered again in PR but with different rate + pr.save() + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 100) self.assertEqual(pr.items[0].warehouse, warehouse_1) - # same rules applied to second item row + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + # same rule applied to second item row # with previous assignment considered - self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_2) - # unassigned 200 Kg - self.assertEqual(pr.items[2].qty, 200) - self.assertEqual(pr.items[2].warehouse, '') + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].putaway_rule, rule_1.name) - po.cancel() + pr.delete() + rule_1.delete() + + def test_validate_over_receipt_in_warehouse(self): + """Test if overreceipt is blocked in the presence of putaway rules.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + + # force overreceipt and disable apply putaway rule in PR + pr.items[0].qty = 300 + pr.items[0].stock_qty = 300 + pr.apply_putaway_rule = 0 + self.assertRaises(frappe.ValidationError, pr.save) + + pr.delete() rule_1.delete() - rule_2.delete() def create_putaway_rule(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f07039f564..aed69e11dd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -42,7 +42,7 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) - def before_save(self): + def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) From c47d38dc1820c3328b2e782c740fa10ad3471890 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Dec 2020 11:48:04 +0530 Subject: [PATCH 13/15] chore: Stock Entry Tests and fixes - Maintain item-warehouse wise rules for Stock Entry Material Transfer - Test cases for Stock Entry with more use cases - Sider fixes --- erpnext/public/js/controllers/transaction.js | 5 +- .../doctype/putaway_rule/putaway_rule.py | 38 +-- .../doctype/putaway_rule/test_putaway_rule.py | 233 ++++++++++++++---- .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 3 +- .../doctype/stock_entry/stock_entry_utils.py | 5 +- 6 files changed, 216 insertions(+), 70 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 54634482b0..2dabb9a647 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2030,7 +2030,7 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }); } -erpnext.apply_putaway_rule = (frm) => { +erpnext.apply_putaway_rule = (frm, purpose=null) => { if (!frm.doc.company) { frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); } @@ -2042,7 +2042,8 @@ erpnext.apply_putaway_rule = (frm) => { doctype: frm.doctype, items: frm.doc.items, company: frm.doc.company, - sync: true + sync: true, + purpose: purpose }, callback: (result) => { if (!result.exc && result.message) { diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 4ed4dafac7..56752583fa 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -64,11 +64,14 @@ def get_putaway_capacity(rule): return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(doctype, items, company, sync=None): +def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): """ Applies Putaway Rule on line items. - items: List of Purchase Receipt Item objects - company: Company in the Purchase Receipt + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls """ if isinstance(items, string_types): items = json.loads(items) @@ -82,31 +85,36 @@ def apply_putaway_rule(doctype, items, company, sync=None): source_warehouse = item.get("s_warehouse") serial_nos = get_serial_nos(item.get("serial_no")) - item.conversion_factor = flt(item.conversion_factor) or 1 + item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) - if not pending_qty or not item_code: - updated_table = add_row(item, pending_qty, item.warehouse, updated_table) - continue - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) + continue + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = item.warehouse + warehouse = source_warehouse or item.warehouse if at_capacity: - warehouse = '' # rules available, but no free space + # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) - updated_table = add_row(item, pending_qty, warehouse, updated_table) + else: + updated_table = add_row(item, pending_qty, warehouse, updated_table) continue - # maintain item wise rules, to handle if item is entered twice + # maintain item/item-warehouse wise rules, to handle if item is entered twice # in the table, due to different price, etc. - if not item_wise_rules[item_code]: - item_wise_rules[item_code] = rules + key = item_code + if doctype == "Stock Entry" and purpose == "Material Transfer" and source_warehouse: + key = (item_code, source_warehouse) - for rule in item_wise_rules[item_code]: + if not item_wise_rules[key]: + item_wise_rules[key] = rules + + for rule in item_wise_rules[key]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty qty_to_allocate = stock_qty_to_allocate / item.conversion_factor diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 17619e01bc..86f7dc3e08 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -4,12 +4,11 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import add_days, nowdate from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestPutawayRule(unittest.TestCase): @@ -27,6 +26,9 @@ class TestPutawayRule(unittest.TestCase): if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): create_warehouse("Rack 2") + self.warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + self.warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + if not frappe.db.exists("UOM", "Bag"): new_uom = frappe.new_doc("UOM") new_uom.uom_name = "Bag" @@ -34,21 +36,18 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2) pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_2) + self.assertEqual(pr.items[1].warehouse, self.warehouse_2) pr.delete() rule_1.delete() @@ -57,26 +56,23 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg") # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 200) # warehouse_1 has 400 kg free space, it is given less priority - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) stock_receipt.cancel() pr.delete() @@ -85,21 +81,20 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + # total 300 assigned, 50 unassigned + pr.delete() rule_1.delete() rule_2.delete() @@ -114,26 +109,23 @@ class TestPutawayRule(unittest.TestCase): }) item.save() - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag") self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag") self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 2) - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) stock_receipt.cancel() pr.delete() @@ -152,15 +144,12 @@ class TestPutawayRule(unittest.TestCase): frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - # Putaway Rule in different UOM - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag") self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg @@ -168,7 +157,7 @@ class TestPutawayRule(unittest.TestCase): conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned @@ -177,11 +166,8 @@ class TestPutawayRule(unittest.TestCase): rule_2.delete() def test_putaway_rules_with_reoccurring_item(self): - """Test rules on same item entered multiple times.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + """Test rules on same item entered multiple times with different rate.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") # total capacity is 200 Kg @@ -201,12 +187,12 @@ class TestPutawayRule(unittest.TestCase): pr.save() self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 100) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # same rule applied to second item row # with previous assignment considered self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) pr.delete() @@ -214,17 +200,14 @@ class TestPutawayRule(unittest.TestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # force overreceipt and disable apply putaway rule in PR @@ -236,6 +219,156 @@ class TestPutawayRule(unittest.TestCase): pr.delete() rule_1.delete() + def test_putaway_rule_on_stock_entry_material_transfer(self): + """Test if source warehouse is considered while applying rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # higher priority + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg", priority=2) + + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided + # even though it has more free space and higher priority + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): + """Test if reoccuring item is correctly considered.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, + uom="Kg", priority=2) + + # create SE with first row having source warehouse as Rack 2 + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + # Add rows with source warehouse as Rack 1 + stock_entry.extend("items", [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100 + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200 + } + ]) + + stock_entry.save() + + # since source warehouse was Rack 2, exclude rule_2 + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 200) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + + # since source warehouse was Rack 1, exclude rule_1 even though it has + # higher priority + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 100) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + + self.assertEqual(stock_entry.items[2].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[2].qty, 200) + self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): + """Test if batch and serial items are split correctly.""" + if not frappe.db.exists("Item", "Water Bottle"): + make_item("Water Bottle", { + "is_stock_item": 1, + "has_batch_no" : 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos" + }) + + rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, + uom="Nos") + rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, + uom="Nos") + + make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") + + pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1) + pr.items[0].batch_no = "BOTTL-BATCH-1" + pr.save() + pr.submit() + + serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = [d.name for d in serial_nos] + + stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, + target="Finished Goods - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_save=1) + stock_entry.items[0].batch_no = "BOTTL-BATCH-1" + stock_entry.items[0].serial_no = "\n".join(serial_nos) + stock_entry.save() + + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 3) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3])) + self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1") + + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 2) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) + self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + + stock_entry.delete() + pr.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_receipt(self): + """Test if rules are applied in Stock Entry of type Receipt.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # more capacity + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg") + + stock_entry = make_stock_entry(item_code="_Rice", qty=100, + target="_Test Warehouse - _TC", purpose="Material Receipt", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry_item.qty, 100) + self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fd920a585a..5d9ea8daee 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -574,7 +574,7 @@ frappe.ui.form.on('Stock Entry', { }, apply_putaway_rule: function (frm) { - if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aed69e11dd..e37e1223bc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -47,7 +47,8 @@ class StockEntry(StockController): apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) if self.get("items") and apply_rule: - apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company, + purpose=self.purpose) def validate(self): self.pro_doc = frappe._dict() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b78c6be983..b12a8547fe 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -53,6 +53,8 @@ def make_stock_entry(**args): args.target = args.to_warehouse if args.item_code: args.item = args.item_code + if args.apply_putaway_rule: + s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): if '.' in args.qty: @@ -118,7 +120,8 @@ def make_stock_entry(**args): "t_warehouse": args.target, "qty": args.qty, "basic_rate": args.rate or args.basic_rate, - "conversion_factor": 1.0, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, 'batch_no': args.batch_no, 'cost_center': args.cost_center, From b8aeb9ea88bc62d04945af6936b5320bba41470c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Jan 2021 12:16:25 +0530 Subject: [PATCH 14/15] fix: Indentation and missing semi-colon - Fix incorrect indentation after merge conflict - Sider --- erpnext/stock/dashboard/item_dashboard.js | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 20415bc07e..d3c442d303 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -163,7 +163,7 @@ erpnext.stock.ItemDashboard = Class.extend({ max_count: max_count, can_write:can_write, show_item: show_item || false - } + }; }, get_capacity_dashboard_data: function(data) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1cb71c2db6..ecdd396f3a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -87,7 +87,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount() - self.validate_putaway_capacity() + self.validate_putaway_capacity() def on_submit(self): self.update_stock_ledger() From 957615bec83e6d4c13eb5dd72fade8c3a3d6ed5b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Jan 2021 23:47:24 +0530 Subject: [PATCH 15/15] fix: Stricter validations - Validation for overreceipt on Purchase Invoice, Stock Entry, Purchase Receipt & Stock Reconciliation - Every incoming stock transaction must be checked to avoid overcapacity - However application of rule and splitting only on certain doctypes - Validate capacity < stock balance on save in putaway rule, irrespective --- erpnext/controllers/stock_controller.py | 66 ++++++++++++------- .../purchase_receipt/purchase_receipt.py | 4 +- .../doctype/putaway_rule/putaway_rule.py | 4 +- .../stock_reconciliation.py | 1 + 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index af26d7b668..e0fcf47365 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -396,38 +396,54 @@ class StockController(AccountsController): def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. - from erpnext.stock.doctype.putaway_rule.putaway_rule import get_putaway_capacity - valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry") - rule_applied = any(item.get("putaway_rule") for item in self.get("items")) + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice", + "Stock Reconciliation") - if valid_doctype and rule_applied and not self.apply_putaway_rule: + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: + valid_doctype = False + + if valid_doctype: rule_map = defaultdict(dict) for item in self.get("items"): - if item.get("putaway_rule"): - rule = item.get("putaway_rule") - disabled = frappe.db.get_value("Putaway Rule", rule, "disable") - if disabled: return # dont validate for disabled rule - stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) - warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" - if not rule_map[rule]: - rule_map[rule]["warehouse"] = item.get(warehouse_field) - rule_map[rule]["item"] = item.get("item_code") - rule_map[rule]["qty_put"] = 0 - rule_map[rule]["capacity"] = get_putaway_capacity(rule) - rule_map[rule]["qty_put"] += flt(stock_qty) + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + rule = frappe.db.get_value("Putaway Rule", + { + "item_code": item.get("item_code"), + "warehouse": item.get(warehouse_field) + }, + ["name", "disable"], as_dict=True) + if rule: + if rule.get("disabled"): continue # dont validate for disabled rule + + if self.doctype == "Stock Reconciliation": + stock_qty = flt(item.qty) + else: + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + + rule_name = rule.get("name") + if not rule_map[rule_name]: + rule_map[rule_name]["warehouse"] = item.get(warehouse_field) + rule_map[rule_name]["item"] = item.get("item_code") + rule_map[rule_name]["qty_put"] = 0 + rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name) + rule_map[rule_name]["qty_put"] += flt(stock_qty) for rule, values in rule_map.items(): if flt(values["qty_put"]) > flt(values["capacity"]): - message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ - .format( - frappe.bold(values["qty_put"]), frappe.bold(values["item"]), - frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) - ) - message += "

    " - rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) - message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + message = self.prepare_over_receipt_message(rule, values) frappe.throw(msg=message, title=_("Over Receipt")) - return rule_map + + def prepare_over_receipt_message(self, rule, values): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

    " + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + return message def repost_future_sle_and_gle(self): args = frappe._dict({ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5e6a3f22b5..61c531067c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -86,7 +86,7 @@ class PurchaseReceipt(BuyingController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - if self.get("items") and self.apply_putaway_rule: + if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): @@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController): if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_entries) def get_asset_gl_entry(self, gl_entries): diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 56752583fa..ea26caced0 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -44,7 +44,7 @@ class PutawayRule(Document): stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.stock_capacity) < flt(balance_qty) and self.get('__islocal'): + if flt(self.stock_capacity) < flt(balance_qty): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") .format(self.item_code, frappe.bold(balance_qty), stock_uom), title=_("Insufficient Capacity")) @@ -56,7 +56,7 @@ class PutawayRule(Document): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) @frappe.whitelist() -def get_putaway_capacity(rule): +def get_available_putaway_capacity(rule): stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"]) balance_qty = get_stock_balance(item_code, warehouse, nowdate()) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5b40292ea8..f0a90f9754 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -30,6 +30,7 @@ class StockReconciliation(StockController): self.validate_data() self.validate_expense_account() self.set_total_qty_and_amount() + self.validate_putaway_capacity() if self._action=="submit": self.make_batches('warehouse')