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 0000000000..e69de29bb2 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 0000000000..4458a7245f --- /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 = $(`${value}`); + var $value = $(value).css("font-weight", "bold"); + value = $value.wrap("

").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 0000000000..420df59889 --- /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 0000000000..276e42ee43 --- /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) 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