Merge branch 'develop' into putaway
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47483c9..20faded 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -347,7 +347,8 @@
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/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index c7efb8a..d32e98e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -123,8 +123,8 @@
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/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2f7b361..4cfbd00 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -6,6 +6,7 @@
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 @@
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:
@@ -403,6 +405,42 @@
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 += "<br><br>"
+ 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/build.json b/erpnext/public/build.json
index 2f15cbc..f0212db 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -55,6 +55,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/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index db85a3e..1cb68a6 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -517,3 +517,26 @@
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/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 9bd03d4..abc286f 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -24,6 +24,16 @@
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 @@
// more
this.content.find('.btn-more').on('click', function() {
- me.start += 20;
+ me.start += this.page_length;
me.refresh();
});
@@ -69,33 +79,41 @@
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 @@
// 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");
- $(`<span class='text-muted small'> ${message} </span>`).appendTo(this.result);
+ var message = __("No Stock Available Currently");
+ this.content.find('.result').css('text-align', 'center');
+
+ $(`<div class='text-muted' style='margin: 20px 5px; font-weight: lighter;'>
+ ${message} </div>`).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 @@
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 @@
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 0000000..ab573e5
--- /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 9068e33..74cc42d 100644
--- a/erpnext/stock/desk_page/stock/stock.json
+++ b/erpnext/stock/desk_page/stock/stock.json
@@ -8,12 +8,12 @@
{
"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\": \"Shipment\",\n \"name\": \"Shipment\",\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\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]"
},
{
"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-12-02 15:47:41.532942",
+ "modified": "2020-12-08 15:47:41.532942",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index faeeb57..ec32b0f 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -384,7 +384,10 @@
<a href="#stock-balance">' + __("Stock Levels") + '</a></h5>');
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.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index bc1d81d..45eb646 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -213,6 +213,10 @@
});
},
+ 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.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 5bb3095..55f0f0c 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",
@@ -1107,6 +1108,12 @@
"read_only": 1
},
{
+ "default": "0",
+ "fieldname": "apply_putaway_rule",
+ "fieldtype": "Check",
+ "label": "Apply Putaway Rule"
+ },
+ {
"depends_on": "eval:!doc.__islocal",
"fieldname": "per_returned",
"fieldtype": "Percent",
@@ -1120,7 +1127,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-30 12:54:23.278500",
+ "modified": "2020-12-08 18:31:32.234503",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 97e0fa7..b1ad610 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -83,6 +83,12 @@
}
])
+ 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()
@@ -103,6 +109,7 @@
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 84c64aa..cb0d1d7 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -76,6 +76,7 @@
"purchase_order_item",
"material_request_item",
"purchase_receipt_item",
+ "putaway_rule",
"section_break_45",
"allow_zero_valuation_rate",
"bom",
@@ -840,6 +841,15 @@
"fieldtype": "Column Break"
},
{
+ "fieldname": "putaway_rule",
+ "fieldtype": "Link",
+ "label": "Putaway Rule",
+ "no_copy": 1,
+ "options": "Putaway Rule",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
"fieldname": "tracking_section",
"fieldtype": "Section Break"
},
@@ -866,7 +876,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-02 10:00:38.204294",
+ "modified": "2020-12-08 10:00:38.204294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/__init__.py
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 0000000..e056920
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js
@@ -0,0 +1,43 @@
+// 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
+ }
+ };
+ });
+ },
+
+ 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
new file mode 100644
index 0000000..a003f49
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json
@@ -0,0 +1,160 @@
+{
+ "actions": [],
+ "autoname": "PUT-.####",
+ "creation": "2020-11-09 11:39:46.489501",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "disable",
+ "item_code",
+ "item_name",
+ "warehouse",
+ "priority",
+ "col_break_capacity",
+ "company",
+ "capacity",
+ "uom",
+ "conversion_factor",
+ "stock_uom",
+ "stock_capacity"
+ ],
+ "fields": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "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
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "priority",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "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"
+ },
+ {
+ "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
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor",
+ "no_copy": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-11-25 20:39:19.973437",
+ "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": "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
+ }
+ ],
+ "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.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
new file mode 100644
index 0000000..8838bb7
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -0,0 +1,223 @@
+# -*- 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
+import json
+from collections import defaultdict
+from six import string_types
+from frappe import _
+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):
+ self.validate_duplicate_rule()
+ 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})
+ 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):
+ 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'):
+ 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"))
+
+ def set_stock_capacity(self):
+ self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity)
+
+@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(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)
+
+ for item in items:
+ if isinstance(item, dict):
+ item = frappe._dict(item)
+
+ 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
+
+ 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:
+ warehouse = '' # rules available, but no free space
+ items_not_accomodated.append([item_code, pending_qty])
+ updated_table = add_row(item, pending_qty, warehouse, updated_table)
+ continue
+
+ # 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
+
+ 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)
+
+ if uom_must_be_whole_number:
+ qty_to_allocate = floor(qty_to_allocate)
+ stock_qty_to_allocate = qty_to_allocate * conversion
+
+ if not qty_to_allocate: break
+
+ 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
+ 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:
+ # 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
+
+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:") + "<br><br>"
+ formatted_item_rows = ""
+
+ for entry in items_not_accomodated:
+ item_link = frappe.utils.get_link_to_form("Item", entry[0])
+ formatted_item_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(item_link, frappe.bold(entry[1]))
+
+ msg += """
+ <table class="table">
+ <thead>
+ <td>{0}</td>
+ <td>{1}</td>
+ </thead>
+ {2}
+ </table>
+ """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
+
+ 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/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
new file mode 100644
index 0000000..725e91e
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
@@ -0,0 +1,18 @@
+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"];
+ }
+ },
+
+ reports: [
+ {
+ name: 'Warehouse Capacity Summary',
+ report_type: 'Page',
+ route: 'warehouse-capacity-summary'
+ }
+ ]
+};
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 0000000..7b81784
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -0,0 +1,261 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+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.purchase_order import make_purchase_receipt
+from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+
+class TestPutawayRule(unittest.TestCase):
+ 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
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 9121758..2be70f3 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -571,6 +571,10 @@
}
});
}
+ },
+
+ 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 61e0df6..cd01fa7 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 e3159b9..aa3425c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -42,6 +42,13 @@
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 @@
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 79e8f9a..4075e28 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",
diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js
index da21c6b..bddffd4 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -65,6 +65,9 @@
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/__init__.py
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 0000000..90112c7
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
@@ -0,0 +1,40 @@
+{% for d in data %}
+ <div class="dashboard-list-item" style="padding: 7px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small" style="margin-top: 8px;">
+ <a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <a data-type="item" data-name="{{ d.item_code }}">{{ d.item_code }}</a>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px; ">
+ {{ d.stock_capacity }}
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ {{ d.actual_qty }}
+ </div>
+ <div class="col-sm-2 small">
+ <div class="progress" title="Occupied Qty: {{ d.actual_qty }}" style="margin-bottom: 4px; height: 7px; margin-top: 14px;">
+ <div class="progress-bar" role="progressbar"
+ aria-valuenow="{{ d.percent_occupied }}"
+ aria-valuemin="0" aria-valuemax="100"
+ style="width:{{ d.percent_occupied }}%;
+ background-color: {{ d.color }}">
+ </div>
+ </div>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px;">
+ {{ d.percent_occupied }}%
+ </div>
+ {% if can_write %}
+ <div class="col-sm-1 text-right" style="margin-top: 2px;">
+ <button class="btn btn-default btn-xs btn-edit"
+ style="margin-top: 4px;margin-bottom: 4px;"
+ data-warehouse="{{ d.warehouse }}"
+ data-item="{{ escape(d.item_code) }}"
+ data-company="{{ escape(d.company) }}">{{ __("Edit Capacity") }}</a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+{% 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 0000000..c3b3b5d
--- /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 0000000..a6e5b45
--- /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 0000000..acaf180
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
@@ -0,0 +1,19 @@
+<div class="dashboard-list-item" style="padding: 12px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Warehouse
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Item
+ </div>
+ <div class="col-sm-1 small text-muted" style="margin-top: 8px;">
+ Stock Capacity
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Balance Stock Qty
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ % Occupied
+ </div>
+ </div>
+</div>
\ No newline at end of file