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
This commit is contained in:
marination 2020-11-26 10:45:44 +05:30
parent 2ed80656aa
commit 1087d97c03
14 changed files with 367 additions and 25 deletions

View File

@ -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"
]
}

View File

@ -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");
$(`<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 @@ 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,
}
}
})

View File

@ -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

View File

@ -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",

View File

@ -384,7 +384,10 @@ $.extend(erpnext.item, {
<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();
});

View File

@ -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",

View File

@ -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
}

View File

@ -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'
}
]
};

View File

@ -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() {

View File

@ -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 %}

View File

@ -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');
});
}

View File

@ -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"
}

View File

@ -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>