Skip to content

Commit

Permalink
Merge pull request #23969 from marination/putaway
Browse files Browse the repository at this point in the history
feat: Putaway
  • Loading branch information
rohitwaghchaure committed Jan 25, 2021
2 parents 1640791 + 957615b commit fa3ec3c
Show file tree
Hide file tree
Showing 33 changed files with 1,359 additions and 35 deletions.
3 changes: 2 additions & 1 deletion erpnext/buying/doctype/purchase_order/purchase_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,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 ...")
})
},

Expand Down
4 changes: 2 additions & 2 deletions erpnext/buying/doctype/purchase_order/purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ def validate_bom_for_subcontracting_items(self):
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'):
Expand Down
54 changes: 54 additions & 0 deletions erpnext/controllers/stock_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, check_if_stock_and_account_balance_synced
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController
Expand All @@ -23,6 +24,7 @@ def validate(self):
self.validate_inspection()
self.validate_serialized_batch()
self.validate_customer_provided_item()
self.validate_putaway_capacity()

def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
Expand Down Expand Up @@ -391,6 +393,58 @@ def validate_customer_provided_item(self):
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_available_putaway_capacity
valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice",
"Stock Reconciliation")

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"):
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 = self.prepare_over_receipt_message(rule, values)
frappe.throw(msg=message, title=_("Over Receipt"))

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 += "<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)
return message

def repost_future_sle_and_gle(self):
args = frappe._dict({
"posting_date": self.posting_date,
Expand Down
4 changes: 3 additions & 1 deletion erpnext/public/build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
2 changes: 1 addition & 1 deletion erpnext/public/js/controllers/buying.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
});

dialog.show();
}
}
32 changes: 32 additions & 0 deletions erpnext/public/js/controllers/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -2025,3 +2025,35 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
}, show_dialog);
});
}

erpnext.apply_putaway_rule = (frm, purpose=null) => {
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,
purpose: purpose
},
callback: (result) => {
if (!result.exc && result.message) {
frm.clear_table("items");

let items = result.message;
items.forEach((row) => {
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();
}
}
});
};
81 changes: 62 additions & 19 deletions erpnext/stock/dashboard/item_dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({

// more
this.content.find('.btn-more').on('click', function() {
me.start += 20;
me.start += me.page_length;
me.refresh();
});

Expand All @@ -69,33 +79,43 @@ 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();
}

var context = this.get_item_dashboard_data(data, this.max_count, true);
let context = "";
if (this.page_name === "warehouse-capacity-summary") {
context = this.get_capacity_dashboard_data(data);
} else {
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
Expand All @@ -106,12 +126,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 = [];
Expand All @@ -128,8 +153,8 @@ erpnext.stock.ItemDashboard = Class.extend({
d.total_reserved, max_count);
});

var can_write = 0;
if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){
let can_write = 0;
if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) {
can_write = 1;
}

Expand All @@ -138,9 +163,27 @@ erpnext.stock.ItemDashboard = Class.extend({
max_count: max_count,
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,
};
}
})
});

erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({
Expand Down
69 changes: 69 additions & 0 deletions erpnext/stock/dashboard/warehouse_capacity_dashboard.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions erpnext/stock/desk_page/stock/stock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion erpnext/stock/doctype/item/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down

0 comments on commit fa3ec3c

Please sign in to comment.