Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Putaway #23969

Merged
merged 22 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c1b0e65
feat: Putaway
marination Nov 12, 2020
86e0332
Merge branch 'develop' of https://github.com/frappe/erpnext into putaway
marination Nov 19, 2020
c7991f8
feat: Putaway Rule
marination Nov 20, 2020
9596276
fix: Linter and Sider
marination Nov 23, 2020
90598ea
chore: Multi UOM support for Putaway
marination Nov 23, 2020
0cec147
chore: Format unassigned Items dialog and add freeze message
marination Nov 23, 2020
ccbd432
chore: Added Tests
marination Nov 24, 2020
e762bc6
Merge branch 'develop' into putaway
marination Nov 24, 2020
68a49ef
chore: Added Putaway Rule to Desk Page and added Priority to List View
marination Nov 24, 2020
2fbaa5d
Merge branch 'putaway' of https://github.com/marination/erpnext into …
marination Nov 24, 2020
2ed8065
chore: Code Cleanup
marination Nov 24, 2020
1087d97
feat: Warehouse Capacity Summary
marination Nov 26, 2020
fac4035
feat: Apply Putaway Rules within transaction itself
marination Dec 7, 2020
0b68243
Merge branch 'develop' into putaway
marination Dec 8, 2020
0f3cfc5
feat: Trigger rule application from client side
marination Dec 8, 2020
a5d8d32
chore: Test and fixes
marination Dec 9, 2020
c47d38d
chore: Stock Entry Tests and fixes
marination Dec 14, 2020
406af27
Merge branch 'develop' into putaway
marination Dec 14, 2020
41ea77c
Merge branch 'develop' into putaway
marination Jan 5, 2021
b8aeb9e
fix: Indentation and missing semi-colon
marination Jan 5, 2021
6ac51ed
Merge branch 'develop' into putaway
marination Jan 18, 2021
957615b
fix: Stricter validations
marination Jan 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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