Merge pull request #2763 from neilLasrado/production-planning-tool
fixes for capacity planning using production planning tool
diff --git a/erpnext/accounts/page/financial_analytics/financial_analytics.js b/erpnext/accounts/page/financial_analytics/financial_analytics.js
index 4e50711..a895aad 100644
--- a/erpnext/accounts/page/financial_analytics/financial_analytics.js
+++ b/erpnext/accounts/page/financial_analytics/financial_analytics.js
@@ -1,8 +1,6 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.require("assets/erpnext/js/account_tree_grid.js");
-
frappe.pages['financial-analytics'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
@@ -10,9 +8,11 @@
single_column: true
});
erpnext.financial_analytics = new erpnext.FinancialAnalytics(wrapper, 'Financial Analytics');
- frappe.add_breadcrumbs("Accounts")
+ frappe.add_breadcrumbs("Accounts");
-}
+};
+
+frappe.require("assets/erpnext/js/account_tree_grid.js");
erpnext.FinancialAnalytics = erpnext.AccountTreeGrid.extend({
filters: [
diff --git a/erpnext/buying/page/purchase_analytics/purchase_analytics.js b/erpnext/buying/page/purchase_analytics/purchase_analytics.js
index 33fb9b2..470e44c 100644
--- a/erpnext/buying/page/purchase_analytics/purchase_analytics.js
+++ b/erpnext/buying/page/purchase_analytics/purchase_analytics.js
@@ -11,10 +11,11 @@
new erpnext.PurchaseAnalytics(wrapper);
- frappe.add_breadcrumbs("Buying")
-
+ frappe.add_breadcrumbs("Buying");
}
+frappe.assets.views["Report"]();
+
erpnext.PurchaseAnalytics = frappe.views.TreeGridReport.extend({
init: function(wrapper) {
this._super({
diff --git a/erpnext/public/js/account_tree_grid.js b/erpnext/public/js/account_tree_grid.js
index dde943a..729e88d 100644
--- a/erpnext/public/js/account_tree_grid.js
+++ b/erpnext/public/js/account_tree_grid.js
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
+frappe.assets.views["Report"]();
+
erpnext.AccountTreeGrid = frappe.views.TreeGridReport.extend({
init: function(wrapper, title) {
this._super({
diff --git a/erpnext/public/js/stock_grid_report.js b/erpnext/public/js/stock_grid_report.js
index 726852f..e1f97f8 100644
--- a/erpnext/public/js/stock_grid_report.js
+++ b/erpnext/public/js/stock_grid_report.js
@@ -1,6 +1,8 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
+frappe.assets.views["Report"]();
+
erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
get_item_warehouse: function(warehouse, item) {
if(!this.item_warehouse[item]) this.item_warehouse[item] = {};
diff --git a/erpnext/selling/page/sales_analytics/sales_analytics.js b/erpnext/selling/page/sales_analytics/sales_analytics.js
index ad2102a..bbe69d7 100644
--- a/erpnext/selling/page/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/page/sales_analytics/sales_analytics.js
@@ -12,7 +12,9 @@
frappe.add_breadcrumbs("Selling")
-}
+};
+
+frappe.assets.views["Report"]();
erpnext.SalesAnalytics = frappe.views.TreeGridReport.extend({
init: function(wrapper) {
diff --git a/erpnext/setup/page/setup_wizard/setup_wizard.py b/erpnext/setup/page/setup_wizard/setup_wizard.py
index 7b9dce9..31dd03d 100644
--- a/erpnext/setup/page/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/page/setup_wizard/setup_wizard.py
@@ -435,7 +435,7 @@
}).insert()
def login_as_first_user(args):
- if args.get("email"):
+ if args.get("email") and hasattr(frappe.local, "login_manager"):
frappe.local.login_manager.user = args.get("email")
frappe.local.login_manager.post_login()
diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js
index 173ed67..1b9bca3 100644
--- a/erpnext/stock/doctype/material_request/material_request_list.js
+++ b/erpnext/stock/doctype/material_request/material_request_list.js
@@ -1,12 +1,13 @@
frappe.listview_settings['Material Request'] = {
add_fields: ["material_request_type", "status", "per_ordered"],
- get_status: function(doc) {
+ get_indicator: function(doc) {
+ console.log()
if(doc.status=="Stopped") {
return [__("Stopped"), "red", "status,=,Stopped"];
- } if(doc.docstatus==1 && doc.per_ordered < 100) {
+ } else if(doc.docstatus==1 && doc.per_ordered < 100) {
return [__("Pending"), "orange", "per_ordered,<,100"];
- } else if(doc.status==1 && doc.per_ordered == 100) {
- return [__("Completed"), "green", "per_ordered,=,100"];
+ } else if(doc.docstatus==1 && doc.per_ordered == 100) {
+ return [__("Ordered"), "green", "per_ordered,=,100"];
}
}
};
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 5447f8a..504c996 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -286,7 +286,7 @@
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
- val_rate_db_precision = 6 if cint(self.precision("valuation_rate")) <= 6 else 9
+ val_rate_db_precision = 6 if cint(d.precision("valuation_rate")) <= 6 else 9
# warehouse account
gl_entries.append(self.get_gl_dict({
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index eb9777a..ab90170 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -9,7 +9,7 @@
from frappe import _
from erpnext.stock.utils import get_incoming_rate
-from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center, get_conversion_factor
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
@@ -255,7 +255,7 @@
if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and d.actual_qty < d.transfer_qty:
frappe.throw(_("""Row {0}: Qty not avalable in warehouse {1} on {2} {3}.
Available Qty: {4}, Transfer Qty: {5}""").format(d.idx, d.s_warehouse,
- self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty))
+ self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty), NegativeStockError)
# get incoming rate
if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index c2cee33..535a548 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -22,7 +22,7 @@
}
});
}
- );
+ , __("Get Items"), __("Update"));
});
erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
@@ -51,7 +51,7 @@
setup: function() {
var me = this;
- this.frm.get_field("items").grid.allow_build_edit();
+ this.frm.get_docfield("items").allow_bulk_edit = 1;
if (sys_defaults.auto_accounting_for_stock) {
this.frm.add_fetch("company", "stock_adjustment_account", "expense_account");
@@ -77,29 +77,12 @@
},
refresh: function() {
- //
+ if(this.frm.doc.docstatus==1) {
+ this.show_stock_ledger();
+ this.show_general_ledger();
+ }
},
- // show_download_template: function() {
- // var me = this;
- // this.frm.add_custom_button(__("Download Template"), function() {
- // this.title = __("Stock Reconcilation Template");
- // frappe.tools.downloadify([[__("Stock Reconciliation")],
- // ["----"],
- // [__("Stock Reconciliation can be used to update the stock on a particular date, usually as per physical inventory.")],
- // [__("When submitted, the system creates difference entries to set the given stock and valuation on this date.")],
- // [__("It can also be used to create opening stock entries and to fix stock value.")],
- // ["----"],
- // [__("Notes:")],
- // [__("Item Code and Warehouse should already exist.")],
- // [__("You can update either Quantity or Valuation Rate or both.")],
- // [__("If no change in either Quantity or Valuation Rate, leave the cell blank.")],
- // ["----"],
- // ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, this);
- // return false;
- // }, "icon-download");
- // },
-
});
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 1bb3f56..ef4403f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -33,11 +33,12 @@
self.validation_messages = []
item_warehouse_combinations = []
+ default_currency = frappe.db.get_default("currency")
+
# validate no of rows
- rows = self.items
- if len(rows) > 100:
+ if len(self.items) > 100:
frappe.throw(_("""Max 100 rows for Stock Reconciliation."""))
- for row_num, row in enumerate(rows):
+ for row_num, row in enumerate(self.items):
# find duplicates
if [row.item_code, row.warehouse] in item_warehouse_combinations:
self.validation_messages.append(_get_msg(row_num, _("Duplicate entry")))
@@ -65,6 +66,13 @@
self.validation_messages.append(_get_msg(row_num,
_("Negative Valuation Rate is not allowed")))
+ if row.qty and not row.valuation_rate:
+ # try if there is a buying price list in default currency
+ buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code,
+ "buying": 1, "currency": default_currency}, "price_list_rate")
+ if buying_rate:
+ row.valuation_rate = buying_rate
+
# throw all validation messages
if self.validation_messages:
for msg in self.validation_messages:
diff --git a/erpnext/stock/page/stock_analytics/stock_analytics.js b/erpnext/stock/page/stock_analytics/stock_analytics.js
index 346750b..0b7acb8 100644
--- a/erpnext/stock/page/stock_analytics/stock_analytics.js
+++ b/erpnext/stock/page/stock_analytics/stock_analytics.js
@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt
-frappe.pages['stock-analytics'].on_page_load = function(wrapper) {
+frappe.pages['stock-analytics'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Stock Analytics'),
@@ -13,7 +13,8 @@
frappe.add_breadcrumbs("Stock")
-
-}
-frappe.require("assets/erpnext/js/stock_analytics.js");
\ No newline at end of file
+};
+
+frappe.assets.views["Report"]();
+frappe.require("assets/erpnext/js/stock_analytics.js");
diff --git a/erpnext/stock/page/stock_ledger/stock_ledger.js b/erpnext/stock/page/stock_ledger/stock_ledger.js
index 3f6a9e3..27525e9 100644
--- a/erpnext/stock/page/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/page/stock_ledger/stock_ledger.js
@@ -10,7 +10,7 @@
new erpnext.StockLedger(wrapper);
frappe.add_breadcrumbs("Stock")
-}
+};
frappe.require("assets/erpnext/js/stock_grid_report.js");
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index a77a629..da516de 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -59,278 +59,256 @@
frappe.db.sql("""delete from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
-def update_entries_after(args, allow_zero_rate=False, allow_negative_stock=False, verbose=1):
+class update_entries_after(object):
"""
update valution rate and qty after transaction
from the current time-bucket onwards
- args = {
- "item_code": "ABC",
- "warehouse": "XYZ",
- "posting_date": "2012-12-12",
- "posting_time": "12:00"
- }
+ :param args: args as dict
+
+ args = {
+ "item_code": "ABC",
+ "warehouse": "XYZ",
+ "posting_date": "2012-12-12",
+ "posting_time": "12:00"
+ }
"""
- if not _exceptions:
- frappe.local.stockledger_exceptions = []
+ def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, verbose=1):
+ from frappe.model.meta import get_field_precision
- if not allow_negative_stock:
- allow_negative_stock = cint(frappe.db.get_default("allow_negative_stock"))
+ self.exceptions = []
+ self.verbose = verbose
+ self.allow_zero_rate = allow_zero_rate
+ self.allow_negative_stock = allow_negative_stock
- previous_sle = get_sle_before_datetime(args)
+ if self.allow_negative_stock==None:
+ self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings",
+ "allow_negative_stock"))
- qty_after_transaction = flt(previous_sle.get("qty_after_transaction"))
- valuation_rate = flt(previous_sle.get("valuation_rate"))
- stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
- stock_value = flt(previous_sle.get("stock_value"))
- prev_stock_value = flt(previous_sle.get("stock_value"))
+ self.args = args
+ for key, value in args.iteritems():
+ setattr(self, key, value)
- entries_to_fix = get_sle_after_datetime(previous_sle or \
- {"item_code": args["item_code"], "warehouse": args["warehouse"]}, for_update=True)
- valuation_method = get_valuation_method(args["item_code"])
- stock_value_difference = 0.0
+ self.previous_sle = self.get_sle_before_datetime()
+ self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict()
- for sle in entries_to_fix:
- if sle.serial_no or not cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")):
+ for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
+ setattr(self, key, flt(self.previous_sle.get(key)))
+
+ self.company = frappe.db.get_value("Warehouse", self.warehouse, "company")
+ self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
+ currency=frappe.db.get_value("Company", self.company, "default_currency"))
+
+ self.prev_stock_value = self.stock_value
+ self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]")
+ self.valuation_method = get_valuation_method(self.item_code)
+ self.stock_value_difference = 0.0
+ self.build()
+
+ def build(self):
+ entries_to_fix = self.get_sle_after_datetime()
+
+ for sle in entries_to_fix:
+ self.process_sle(sle)
+
+ if self.exceptions:
+ self.raise_exceptions()
+
+ self.update_bin()
+
+ def update_bin(self):
+ # update bin
+ bin_name = frappe.db.get_value("Bin", {
+ "item_code": self.item_code,
+ "warehouse": self.warehouse
+ })
+
+ if not bin_name:
+ bin_doc = frappe.get_doc({
+ "doctype": "Bin",
+ "item_code": self.item_code,
+ "warehouse": self.warehouse
+ })
+ bin_doc.insert(ignore_permissions=True)
+ else:
+ bin_doc = frappe.get_doc("Bin", bin_name)
+
+ bin_doc.update({
+ "valuation_rate": self.valuation_rate,
+ "actual_qty": self.qty_after_transaction,
+ "stock_value": self.stock_value
+ })
+ bin_doc.save(ignore_permissions=True)
+
+ def process_sle(self, sle):
+ if sle.serial_no or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
- if not validate_negative_stock(qty_after_transaction, sle):
- qty_after_transaction += flt(sle.actual_qty)
- continue
-
+ if not self.validate_negative_stock(sle):
+ self.qty_after_transaction += flt(sle.actual_qty)
+ return
if sle.serial_no:
- valuation_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate)
- qty_after_transaction += flt(sle.actual_qty)
-
+ self.valuation_rate = self.get_serialized_values(sle)
+ self.qty_after_transaction += flt(sle.actual_qty)
+ self.stock_value = self.qty_after_transaction * self.valuation_rate
else:
if sle.voucher_type=="Stock Reconciliation":
- valuation_rate = sle.valuation_rate
- qty_after_transaction = sle.qty_after_transaction
- stock_queue = [[qty_after_transaction, valuation_rate]]
+ # assert
+ self.valuation_rate = sle.valuation_rate
+ self.qty_after_transaction = sle.qty_after_transaction
+ self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
+ self.stock_value = self.qty_after_transaction * self.valuation_rate
else:
- if valuation_method == "Moving Average":
- valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate)
+ if self.valuation_method == "Moving Average":
+ self.get_moving_average_values(sle)
+ self.qty_after_transaction += flt(sle.actual_qty)
+ self.stock_value = self.qty_after_transaction * self.valuation_rate
else:
- valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate)
+ self.get_fifo_values(sle)
+ self.qty_after_transaction += flt(sle.actual_qty)
+ self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
- qty_after_transaction += flt(sle.actual_qty)
-
- # get stock value
- if sle.serial_no:
- stock_value = qty_after_transaction * valuation_rate
- elif valuation_method == "Moving Average":
- stock_value = qty_after_transaction * valuation_rate
- else:
- stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
-
# rounding as per precision
- from frappe.model.meta import get_field_precision
- meta = frappe.get_meta("Stock Ledger Entry")
+ self.stock_value = flt(self.stock_value, self.precision)
- stock_value = flt(stock_value, get_field_precision(meta.get_field("stock_value"),
- frappe._dict({"fields": sle})))
-
- stock_value_difference = stock_value - prev_stock_value
- prev_stock_value = stock_value
+ stock_value_difference = self.stock_value - self.prev_stock_value
+ self.prev_stock_value = self.stock_value
# update current sle
frappe.db.sql("""update `tabStock Ledger Entry`
set qty_after_transaction=%s, valuation_rate=%s, stock_queue=%s,
stock_value=%s, stock_value_difference=%s where name=%s""",
- (qty_after_transaction, valuation_rate,
- json.dumps(stock_queue), stock_value, stock_value_difference, sle.name))
+ (self.qty_after_transaction, self.valuation_rate,
+ json.dumps(self.stock_queue), self.stock_value, stock_value_difference, sle.name))
- if _exceptions:
- _raise_exceptions(args, verbose)
+ def validate_negative_stock(self, sle):
+ """
+ validate negative stock for entries current datetime onwards
+ will not consider cancelled entries
+ """
+ diff = self.qty_after_transaction + flt(sle.actual_qty)
- # update bin
- if not frappe.db.exists({"doctype": "Bin", "item_code": args["item_code"],
- "warehouse": args["warehouse"]}):
- bin_wrapper = frappe.get_doc({
- "doctype": "Bin",
- "item_code": args["item_code"],
- "warehouse": args["warehouse"],
- })
- bin_wrapper.flags.ignore_permissions = 1
- bin_wrapper.insert()
-
- frappe.db.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s,
- stock_value=%s,
- projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty - reserved_qty)
- where item_code=%s and warehouse=%s""", (valuation_rate, qty_after_transaction,
- stock_value, args["item_code"], args["warehouse"]))
-
-def get_sle_before_datetime(args, for_update=False):
- """
- get previous stock ledger entry before current time-bucket
-
- Details:
- get the last sle before the current time-bucket, so that all values
- are reposted from the current time-bucket onwards.
- this is necessary because at the time of cancellation, there may be
- entries between the cancelled entries in the same time-bucket
- """
- sle = get_stock_ledger_entries(args,
- ["timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)"],
- "desc", "limit 1", for_update=for_update)
-
- return sle and sle[0] or frappe._dict()
-
-def get_sle_after_datetime(args, for_update=False):
- """get Stock Ledger Entries after a particular datetime, for reposting"""
- # NOTE: using for update of
- conditions = ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"]
-
- # Excluding name: Workaround for MariaDB timestamp() floating microsecond issue
- if args.get("name"):
- conditions.append("name!=%(name)s")
-
- return get_stock_ledger_entries(args, conditions, "asc", for_update=for_update)
-
-def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None, for_update=False):
- """get stock ledger entries filtered by specific posting datetime conditions"""
- if not args.get("posting_date"):
- args["posting_date"] = "1900-01-01"
- if not args.get("posting_time"):
- args["posting_time"] = "00:00"
-
- return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry`
- where item_code = %%(item_code)s
- and warehouse = %%(warehouse)s
- and ifnull(is_cancelled, 'No')='No'
- %(conditions)s
- order by timestamp(posting_date, posting_time) %(order)s, name %(order)s
- %(limit)s %(for_update)s""" % {
- "conditions": conditions and ("and " + " and ".join(conditions)) or "",
- "limit": limit or "",
- "for_update": for_update and "for update" or "",
- "order": order
- }, args, as_dict=1)
-
-def validate_negative_stock(qty_after_transaction, sle):
- """
- validate negative stock for entries current datetime onwards
- will not consider cancelled entries
- """
- diff = qty_after_transaction + flt(sle.actual_qty)
-
- if not _exceptions:
- frappe.local.stockledger_exceptions = []
-
- if diff < 0 and abs(diff) > 0.0001:
- # negative stock!
- exc = sle.copy().update({"diff": diff})
- _exceptions.append(exc)
- return False
- else:
- return True
-
-def get_serialized_values(qty_after_transaction, sle, valuation_rate):
- incoming_rate = flt(sle.incoming_rate)
- actual_qty = flt(sle.actual_qty)
- serial_no = cstr(sle.serial_no).split("\n")
-
- if incoming_rate < 0:
- # wrong incoming rate
- incoming_rate = valuation_rate
- elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
- # In case of delivery/stock issue, get average purchase rate
- # of serial nos of current entry
- incoming_rate = flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0))
- from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_no))),
- tuple(serial_no))[0][0])
-
- if incoming_rate and not valuation_rate:
- valuation_rate = incoming_rate
- else:
- new_stock_qty = qty_after_transaction + actual_qty
- if new_stock_qty > 0:
- new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
- if new_stock_value > 0:
- # calculate new valuation rate only if stock value is positive
- # else it remains the same as that of previous entry
- valuation_rate = new_stock_value / new_stock_qty
-
- return valuation_rate
-
-def get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate):
- incoming_rate = flt(sle.incoming_rate)
- actual_qty = flt(sle.actual_qty)
-
- if flt(sle.actual_qty) > 0:
- if qty_after_transaction < 0 and not valuation_rate:
- # if negative stock, take current valuation rate as incoming rate
- valuation_rate = incoming_rate
-
- new_stock_qty = abs(qty_after_transaction) + actual_qty
- new_stock_value = (abs(qty_after_transaction) * valuation_rate) + (actual_qty * incoming_rate)
-
- if new_stock_qty:
- valuation_rate = new_stock_value / flt(new_stock_qty)
- elif not valuation_rate and qty_after_transaction <= 0:
- valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate)
-
- return abs(flt(valuation_rate))
-
-def get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate):
- incoming_rate = flt(sle.incoming_rate)
- actual_qty = flt(sle.actual_qty)
-
- if actual_qty > 0:
- if not stock_queue:
- stock_queue.append([0, 0])
-
- if stock_queue[-1][0] > 0:
- stock_queue.append([actual_qty, incoming_rate])
+ if diff < 0 and abs(diff) > 0.0001:
+ # negative stock!
+ exc = sle.copy().update({"diff": diff})
+ self.exceptions.append(exc)
+ return False
else:
- qty = stock_queue[-1][0] + actual_qty
- if qty == 0:
- stock_queue.pop(-1)
+ return True
+
+ def get_serialized_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+ serial_no = cstr(sle.serial_no).split("\n")
+
+ if incoming_rate < 0:
+ # wrong incoming rate
+ incoming_rate = self.valuation_rate
+ elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
+ # In case of delivery/stock issue, get average purchase rate
+ # of serial nos of current entry
+ incoming_rate = flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0))
+ from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_no))),
+ tuple(serial_no))[0][0])
+
+ if incoming_rate and not self.valuation_rate:
+ self.valuation_rate = incoming_rate
+ else:
+ new_stock_qty = self.qty_after_transaction + actual_qty
+ if new_stock_qty > 0:
+ new_stock_value = self.qty_after_transaction * self.valuation_rate + actual_qty * incoming_rate
+ if new_stock_value > 0:
+ # calculate new valuation rate only if stock value is positive
+ # else it remains the same as that of previous entry
+ self.valuation_rate = new_stock_value / new_stock_qty
+
+ def get_moving_average_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+
+ if flt(sle.actual_qty) > 0:
+ if self.qty_after_transaction < 0 and not self.valuation_rate:
+ # if negative stock, take current valuation rate as incoming rate
+ self.valuation_rate = incoming_rate
+
+ new_stock_qty = abs(self.qty_after_transaction) + actual_qty
+ new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * incoming_rate)
+
+ if new_stock_qty:
+ self.valuation_rate = new_stock_value / flt(new_stock_qty)
+ elif not self.valuation_rate and self.qty_after_transaction <= 0:
+ valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate)
+
+ return abs(flt(valuation_rate))
+
+ def get_fifo_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+
+ if actual_qty > 0:
+ if not self.stock_queue:
+ self.stock_queue.append([0, 0])
+
+ if self.stock_queue[-1][0] > 0:
+ self.stock_queue.append([actual_qty, incoming_rate])
else:
- stock_queue[-1] = [qty, incoming_rate]
- else:
- qty_to_pop = abs(actual_qty)
- while qty_to_pop:
- if not stock_queue:
- stock_queue.append([0, get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate)
- if qty_after_transaction <= 0 else 0])
+ qty = self.stock_queue[-1][0] + actual_qty
+ if qty == 0:
+ self.stock_queue.pop(-1)
+ else:
+ self.stock_queue[-1] = [qty, incoming_rate]
+ else:
+ qty_to_pop = abs(actual_qty)
+ while qty_to_pop:
+ if not self.stock_queue:
+ if self.qty_after_transaction > 0:
+ _rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate)
+ else:
+ _rate = 0
+ self.stock_queue.append([0, _rate])
- batch = stock_queue[0]
+ batch = self.stock_queue[0]
- if qty_to_pop >= batch[0]:
- # consume current batch
- qty_to_pop = qty_to_pop - batch[0]
- stock_queue.pop(0)
- if not stock_queue and qty_to_pop:
- # stock finished, qty still remains to be withdrawn
- # negative stock, keep in as a negative batch
- stock_queue.append([-qty_to_pop, batch[1]])
- break
+ if qty_to_pop >= batch[0]:
+ # consume current batch
+ qty_to_pop = qty_to_pop - batch[0]
+ self.stock_queue.pop(0)
+ if not self.stock_queue and qty_to_pop:
+ # stock finished, qty still remains to be withdrawn
+ # negative stock, keep in as a negative batch
+ self.stock_queue.append([-qty_to_pop, batch[1]])
+ break
- else:
- # qty found in current batch
- # consume it and exit
- batch[0] = batch[0] - qty_to_pop
- qty_to_pop = 0
+ else:
+ # qty found in current batch
+ # consume it and exit
+ batch[0] = batch[0] - qty_to_pop
+ qty_to_pop = 0
- stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
- stock_qty = sum((flt(batch[0]) for batch in stock_queue))
+ stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
+ stock_qty = sum((flt(batch[0]) for batch in self.stock_queue))
- valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0
+ self.valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0
- return abs(valuation_rate)
+ def get_sle_before_datetime(self):
+ """get previous stock ledger entry before current time-bucket"""
+ return get_stock_ledger_entries(self.args, "<", "desc", "limit 1", for_update=False)
-def _raise_exceptions(args, verbose=1):
- deficiency = min(e["diff"] for e in _exceptions)
- msg = _("Negative Stock Error ({6}) for Item {0} in Warehouse {1} on {2} {3} in {4} {5}").format(args["item_code"],
- args.get("warehouse"), _exceptions[0]["posting_date"], _exceptions[0]["posting_time"],
- _(_exceptions[0]["voucher_type"]), _exceptions[0]["voucher_no"], deficiency)
- if verbose:
- frappe.throw(msg, NegativeStockError)
- else:
- raise NegativeStockError, msg
+ def get_sle_after_datetime(self):
+ """get Stock Ledger Entries after a particular datetime, for reposting"""
+ return get_stock_ledger_entries(self.previous_sle or self.args, ">", "asc", for_update=True)
+
+ def raise_exceptions(self):
+ deficiency = min(e["diff"] for e in self.exceptions)
+ msg = _("Negative Stock Error ({6}) for Item {0} in Warehouse {1} on {2} {3} in {4} {5}").format(self.item_code,
+ self.warehouse, self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"],
+ _(self.exceptions[0]["voucher_type"]), self.exceptions[0]["voucher_no"], deficiency)
+ if self.verbose:
+ frappe.throw(msg, NegativeStockError)
+ else:
+ raise NegativeStockError, msg
def get_previous_sle(args, for_update=False):
"""
@@ -346,13 +324,34 @@
"sle": "name of reference Stock Ledger Entry"
}
"""
- if not args.get("sle"): args["sle"] = ""
-
- sle = get_stock_ledger_entries(args, ["name != %(sle)s",
- "timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s)"],
- "desc", "limit 1", for_update=for_update)
+ args["name"] = args.get("sle", None) or ""
+ sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
return sle and sle[0] or {}
+def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
+ conditions = "timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
+ if not previous_sle.get("posting_date"):
+ previous_sle["posting_date"] = "1900-01-01"
+ if not previous_sle.get("posting_time"):
+ previous_sle["posting_time"] = "00:00"
+
+ if operator in (">", "<=") and previous_sle.get("name"):
+ conditions += " and name!=%(name)s"
+
+ return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry`
+ where item_code = %%(item_code)s
+ and warehouse = %%(warehouse)s
+ and ifnull(is_cancelled, 'No')='No'
+ and %(conditions)s
+ order by timestamp(posting_date, posting_time) %(order)s, name %(order)s
+ %(limit)s %(for_update)s""" % {
+ "conditions": conditions,
+ "limit": limit or "",
+ "for_update": for_update and "for update" or "",
+ "order": order
+ }, previous_sle, as_dict=1)
+
def get_valuation_rate(item_code, warehouse, allow_zero_rate=False):
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
diff --git a/erpnext/support/page/support_analytics/support_analytics.js b/erpnext/support/page/support_analytics/support_analytics.js
index 4ca15a2..057aebc 100644
--- a/erpnext/support/page/support_analytics/support_analytics.js
+++ b/erpnext/support/page/support_analytics/support_analytics.js
@@ -15,6 +15,8 @@
}
+frappe.assets.views["Report"]();
+
erpnext.SupportAnalytics = frappe.views.GridReportWithPlot.extend({
init: function(wrapper) {
this._super({
diff --git a/erpnext/templates/form_grid/item_grid.html b/erpnext/templates/form_grid/item_grid.html
index e792da0..6bd5d48 100644
--- a/erpnext/templates/form_grid/item_grid.html
+++ b/erpnext/templates/form_grid/item_grid.html
@@ -14,9 +14,9 @@
<br>{%= doc.item_name %}{% } %}
{% if(doc.item_name != doc.description) { %}
<p>{%= doc.description %}</p>{% } %}
- <div>
+ <p>
{% if(doc.sales_order || doc.against_sales_order) { %}
- <span class="label label-default"
+ <span class="label label-default" style="margin-right: 10px;"
title="{%= __("Sales Order") %}">
<i class="icon-file"></i>
{%= doc.sales_order || doc.against_sales_order %}
@@ -36,22 +36,28 @@
}
}
%}
- <span style="margin-left: 10px;"
+ <span style="margin-right: 10px;"
title="{%= title %}">
<span class="label {%= label_class %}">
{%= doc.warehouse %}
</span>
</span>
{% } %}
- </div>
+ </p>
{% include "templates/form_grid/includes/visible_cols.html" %}
{% if(doc.schedule_date) { %}
- <div><span title="{%= __("Reqd By Date") %}" class="label {%=
- (frappe.datetime.get_diff(doc.schedule_date) < 1
- && doc.received_qty < doc.qty)
- ? "label-danger" : "label-default" %}">
- {%= doc.get_formatted("schedule_date") %}</span>
- </div>
+ <div class="clearfix"></div>
+ <div>
+ {% if(frappe.datetime.get_diff(doc.schedule_date) < 1 && doc.received_qty < doc.qty) { %}
+ <span class="text-danger">
+ {%= __("Overdue on {0}", [doc.get_formatted("schedule_date")]) %}
+ </span>
+ {% } else { %}
+ <span class="text-muted">
+ {%= __("Due on {0}", [doc.get_formatted("schedule_date")]) %}
+ </span>
+ {% } %}
+ </div>
{% } %}
</div>
diff --git a/erpnext/templates/form_grid/material_request_grid.html b/erpnext/templates/form_grid/material_request_grid.html
index c30cf58..1e25909 100644
--- a/erpnext/templates/form_grid/material_request_grid.html
+++ b/erpnext/templates/form_grid/material_request_grid.html
@@ -13,24 +13,7 @@
<br>{%= doc.item_name %}{% } %}
{% if(doc.item_name != doc.description) { %}
<p>{%= doc.description %}</p>{% } %}
- {% include "templates/form_grid/includes/visible_cols.html" %}
- {% if(doc.schedule_date) { %}
- <br><span title="{%= __("Reqd By Date") %}" class="label {%=
- (frappe.datetime.get_diff(doc.schedule_date, frappe.datetime.get_today()) < 0
- && doc.ordered_qty < doc.qty)
- ? "label-danger" : "label-default" %}">
- {%= doc.get_formatted("schedule_date") %}</span>
- {% } %}
- </div>
-
- <!-- qty -->
- <div class="col-sm-3 text-right">
- {%= doc.get_formatted("qty") %}
- <small>{%= doc.uom || doc.stock_uom %}</small>
- {% var completed =
- 100 - cint((doc.qty - cint(doc.ordered_qty)) * 100 / doc.qty),
- title = __("Ordered"); %}
- {% include "templates/form_grid/includes/progress.html" %}
+ <div>
{% if(doc.warehouse) { %}
<div style="overflow:hidden; min-height: 25px;
white-space:nowrap; text-overflow: ellipsis;"
@@ -40,6 +23,26 @@
</span>
</div>
{% } %}
+ {% if(doc.schedule_date) { %}
+ <span title="{%= __("Reqd By Date") %}" class="label {%=
+ (frappe.datetime.get_diff(doc.schedule_date, frappe.datetime.get_today()) < 0
+ && doc.ordered_qty < doc.qty)
+ ? "label-danger" : "label-default" %}">
+ {%= doc.get_formatted("schedule_date") %}</span>
+ {% } %}
+ </div>
+ {% include "templates/form_grid/includes/visible_cols.html" %}
+ </div>
+
+ <!-- qty -->
+ <div class="col-sm-3 text-right">
+ {%= doc.get_formatted("qty") %}
+ <div class="small">{%= doc.uom || doc.stock_uom %}</div>
+ {% if(doc.qty == doc.ordered_qty) { %}
+ <div class="small text-success">{%= __("Ordered") %}</div>
+ {% } else { %}
+ <div class="small text-muted">{%= __("{0} Ordered", doc.ordered_qty) %}</div>
+ {% } %}
</div>
</div>
{% } %}