Merge branch 'develop' into Product-Bundle-Balance
diff --git a/erpnext/stock/report/product_bundle_balance/__init__.py b/erpnext/stock/report/product_bundle_balance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/product_bundle_balance/__init__.py
diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js
new file mode 100644
index 0000000..4458a72
--- /dev/null
+++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
+// For license information, please see license.txt
+
+frappe.query_reports["Product Bundle Balance"] = {
+ "filters": [
+ {
+ "fieldname":"date",
+ "label": __("Date"),
+ "fieldtype": "Date",
+ "width": "80",
+ "reqd": 1,
+ "default": frappe.datetime.get_today(),
+ },
+ {
+ "fieldname": "item_code",
+ "label": __("Item"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Item",
+ "get_query": function() {
+ return {
+ query: "erpnext.controllers.queries.item_query",
+ filters: {"is_stock_item": 0}
+ };
+ }
+ },
+ {
+ "fieldname": "item_group",
+ "label": __("Item Group"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Item Group"
+ },
+ {
+ "fieldname":"brand",
+ "label": __("Brand"),
+ "fieldtype": "Link",
+ "options": "Brand"
+ },
+ {
+ "fieldname": "warehouse",
+ "label": __("Warehouse"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Warehouse"
+ },
+ ],
+ "initial_depth": 0,
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (!data.parent_item) {
+ value = $(`<span>${value}</span>`);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("<p></p>").parent().html();
+ }
+ return value;
+ }
+};
diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.json b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.json
new file mode 100644
index 0000000..420df59
--- /dev/null
+++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.json
@@ -0,0 +1,23 @@
+{
+ "add_total_row": 0,
+ "apply_user_permissions": 1,
+ "creation": "2019-03-06 01:40:35.418304",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2019-03-06 01:40:35.418304",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Product Bundle Balance",
+ "owner": "Administrator",
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Product Bundle Balance",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ }
+ ]
+}
diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py
new file mode 100644
index 0000000..39b20e0
--- /dev/null
+++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py
@@ -0,0 +1,187 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import flt
+from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
+from six import iteritems
+
+
+def execute(filters=None):
+ if not filters:
+ filters = frappe._dict()
+
+ columns = get_columns()
+ item_details, pb_details, parent_items, child_items = get_items(filters)
+ stock_balance = get_stock_balance(filters, child_items)
+
+ data = []
+ for parent_item in parent_items:
+ parent_item_detail = item_details[parent_item]
+
+ required_items = pb_details[parent_item]
+ warehouse_company_map = {}
+ for child_item in required_items:
+ child_item_balance = stock_balance.get(child_item.item_code, frappe._dict())
+ for warehouse, sle in iteritems(child_item_balance):
+ if flt(sle.qty_after_transaction) > 0:
+ warehouse_company_map[warehouse] = sle.company
+
+ for warehouse, company in iteritems(warehouse_company_map):
+ parent_row = {
+ "indent": 0,
+ "item_code": parent_item,
+ "item_name": parent_item_detail.item_name,
+ "item_group": parent_item_detail.item_group,
+ "brand": parent_item_detail.brand,
+ "description": parent_item_detail.description,
+ "warehouse": warehouse,
+ "uom": parent_item_detail.stock_uom,
+ "company": company,
+ }
+
+ child_rows = []
+ for child_item_detail in required_items:
+ child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get(warehouse, frappe._dict())
+ child_row = {
+ "indent": 1,
+ "parent_item": parent_item,
+ "item_code": child_item_detail.item_code,
+ "item_name": child_item_detail.item_name,
+ "item_group": child_item_detail.item_group,
+ "brand": child_item_detail.brand,
+ "description": child_item_detail.description,
+ "warehouse": warehouse,
+ "uom": child_item_detail.uom,
+ "actual_qty": flt(child_item_balance.qty_after_transaction),
+ "minimum_qty": flt(child_item_detail.qty),
+ "company": company,
+ }
+ child_row["bundle_qty"] = child_row["actual_qty"] // child_row["minimum_qty"]
+ child_rows.append(child_row)
+
+ min_bundle_qty = min(map(lambda d: d["bundle_qty"], child_rows))
+ parent_row["bundle_qty"] = min_bundle_qty
+
+ data.append(parent_row)
+ data += child_rows
+
+ return columns, data
+
+
+def get_columns():
+ columns = [
+ {"fieldname": "item_code", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 300},
+ {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", "width": 100},
+ {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 70},
+ {"fieldname": "bundle_qty", "label": _("Bundle Qty"), "fieldtype": "Float", "width": 100},
+ {"fieldname": "actual_qty", "label": _("Actual Qty"), "fieldtype": "Float", "width": 100},
+ {"fieldname": "minimum_qty", "label": _("Minimum Qty"), "fieldtype": "Float", "width": 100},
+ {"fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", "width": 100},
+ {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100},
+ {"fieldname": "description", "label": _("Description"), "width": 140},
+ {"fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", "width": 100}
+ ]
+ return columns
+
+
+def get_items(filters):
+ pb_details = frappe._dict()
+ item_details = frappe._dict()
+
+ conditions = get_parent_item_conditions(filters)
+ parent_item_details = frappe.db.sql("""
+ select item.name as item_code, item.item_name, pb.description, item.item_group, item.brand, item.stock_uom
+ from `tabItem` item
+ inner join `tabProduct Bundle` pb on pb.new_item_code = item.name
+ where ifnull(item.disabled, 0) = 0 {0}
+ """.format(conditions), filters, as_dict=1) # nosec
+
+ parent_items = []
+ for d in parent_item_details:
+ parent_items.append(d.item_code)
+ item_details[d.item_code] = d
+
+ if parent_items:
+ child_item_details = frappe.db.sql("""
+ select
+ pb.new_item_code as parent_item, pbi.item_code, item.item_name, pbi.description, item.item_group, item.brand,
+ item.stock_uom, pbi.uom, pbi.qty
+ from `tabProduct Bundle Item` pbi
+ inner join `tabProduct Bundle` pb on pb.name = pbi.parent
+ inner join `tabItem` item on item.name = pbi.item_code
+ where pb.new_item_code in ({0})
+ """.format(", ".join(["%s"] * len(parent_items))), parent_items, as_dict=1) # nosec
+ else:
+ child_item_details = []
+
+ child_items = set()
+ for d in child_item_details:
+ if d.item_code != d.parent_item:
+ pb_details.setdefault(d.parent_item, []).append(d)
+ child_items.add(d.item_code)
+ item_details[d.item_code] = d
+
+ child_items = list(child_items)
+ return item_details, pb_details, parent_items, child_items
+
+
+def get_stock_balance(filters, items):
+ sle = get_stock_ledger_entries(filters, items)
+ stock_balance = frappe._dict()
+ for d in sle:
+ stock_balance.setdefault(d.item_code, frappe._dict())[d.warehouse] = d
+ return stock_balance
+
+
+def get_stock_ledger_entries(filters, items):
+ if not items:
+ return []
+
+ item_conditions_sql = ' and sle.item_code in ({})' \
+ .format(', '.join(['"' + frappe.db.escape(i, percent=False) + '"' for i in items]))
+
+ conditions = get_sle_conditions(filters)
+
+ return frappe.db.sql("""
+ select
+ sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company
+ from
+ `tabStock Ledger Entry` sle force index (posting_sort_index)
+ left join `tabStock Ledger Entry` sle2 on
+ sle.item_code = sle2.item_code and sle.warehouse = sle2.warehouse
+ and (sle.posting_date, sle.posting_time, sle.name) < (sle2.posting_date, sle2.posting_time, sle2.name)
+ where sle2.name is null and sle.docstatus < 2 %s %s""" % (item_conditions_sql, conditions), as_dict=1) # nosec
+
+
+def get_parent_item_conditions(filters):
+ conditions = []
+
+ if filters.get("item_code"):
+ conditions.append("item.item_code = %(item_code)s")
+ else:
+ if filters.get("brand"):
+ conditions.append("item.brand=%(brand)s")
+ if filters.get("item_group"):
+ conditions.append(get_item_group_condition(filters.get("item_group")))
+
+ conditions = " and ".join(conditions)
+ return "and {0}".format(conditions) if conditions else ""
+
+
+def get_sle_conditions(filters):
+ conditions = ""
+ if not filters.get("date"):
+ frappe.throw(_("'Date' is required"))
+
+ conditions += " and sle.posting_date <= '%s'" % frappe.db.escape(filters.get("date"))
+
+ if filters.get("warehouse"):
+ warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
+ if warehouse_details:
+ conditions += " and exists (select name from `tabWarehouse` wh \
+ where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" % (warehouse_details.lft, warehouse_details.rgt) # nosec
+
+ return conditions