feat: Inventory Dimension
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index e90a4f6..da7f5e2 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -18,6 +18,9 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
+ get_evaluated_inventory_dimension,
+)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -364,8 +367,16 @@
)
sl_dict.update(args)
+ if self.docstatus == 1:
+ self.update_inventory_dimensions(d, sl_dict)
+
return sl_dict
+ def update_inventory_dimensions(self, row, sl_dict) -> None:
+ dimension = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
+ if dimension:
+ sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
+
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.stock.stock_ledger import make_sl_entries
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 62abb74..421a63a 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -224,6 +224,32 @@
});
},
+ add_inventory_dimensions: function(report_name, index) {
+ let filters = frappe.query_reports[report_name].filters;
+
+ frappe.call({
+ method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_dimensions",
+ callback: function(r) {
+ if (r.message && r.message.length) {
+ r.message.forEach((dimension) => {
+ let found = filters.some(el => el.fieldname === dimension['fieldname']);
+
+ if (!found) {
+ filters.splice(index, 0, {
+ "fieldname": dimension["fieldname"],
+ "label": __(dimension["label"]),
+ "fieldtype": "MultiSelectList",
+ get_data: function(txt) {
+ return frappe.db.get_link_options(dimension["doctype"], txt);
+ },
+ });
+ }
+ });
+ }
+ }
+ });
+ },
+
make_subscription: function(doctype, docname) {
frappe.call({
method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat",
diff --git a/erpnext/stock/doctype/inventory_dimension/__init__.py b/erpnext/stock/doctype/inventory_dimension/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/inventory_dimension/__init__.py
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
new file mode 100644
index 0000000..1256659
--- /dev/null
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Inventory Dimension', {
+ setup(frm) {
+ frm.trigger('set_query_on_fields');
+ },
+
+ set_query_on_fields(frm) {
+ frm.set_query('reference_document', () => {
+ let invalid_doctypes = frappe.model.core_doctypes_list;
+ invalid_doctypes.push('Batch', 'Serial No', 'Warehouse', 'Item', 'Inventory Dimension',
+ 'Accounting Dimension', 'Accounting Dimension Filter');
+
+ return {
+ filters: {
+ 'istable': 0,
+ 'issingle': 0,
+ 'name': ['not in', invalid_doctypes]
+ }
+ };
+ });
+
+ frm.set_query('document_type', () => {
+ return {
+ query: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_inventory_documents',
+ };
+ });
+ },
+
+ onload(frm) {
+ frm.trigger('render_traget_field');
+ },
+
+ map_with_existing_field(frm) {
+ frm.trigger('render_traget_field');
+ },
+
+ render_traget_field(frm) {
+ if (frm.doc.map_with_existing_field && !frm.doc.disabled) {
+ frappe.call({
+ method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_source_fieldnames',
+ args: {
+ reference_document: frm.doc.reference_document,
+ ignore_document: frm.doc.name
+ },
+ callback: function(r) {
+ if (r.message && r.message.length) {
+ frm.set_df_property('stock_ledger_dimension', 'options', r.message);
+ } else {
+ frm.set_value("map_with_existing_field", 0);
+ frappe.msgprint(__('Inventory Dimensions not found'));
+ }
+ }
+ });
+ }
+ }
+});
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
new file mode 100644
index 0000000..7e5df42
--- /dev/null
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
@@ -0,0 +1,189 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:dimension_name",
+ "creation": "2022-06-17 13:04:16.554051",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "dimension_details_tab",
+ "dimension_name",
+ "reference_document",
+ "disabled",
+ "section_break_7",
+ "field_mapping_section",
+ "source_fieldname",
+ "target_fieldname",
+ "column_break_9",
+ "map_with_existing_field",
+ "stock_ledger_dimension",
+ "applicable_for_documents_tab",
+ "apply_to_all_doctypes",
+ "document_type",
+ "istable",
+ "type_of_transaction",
+ "column_break_16",
+ "condition"
+ ],
+ "fields": [
+ {
+ "fieldname": "dimension_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Dimension Details"
+ },
+ {
+ "fieldname": "reference_document",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Document",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "dimension_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Dimension Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "applicable_for_documents_tab",
+ "fieldtype": "Tab Break",
+ "label": "Applicable For Documents"
+ },
+ {
+ "depends_on": "eval:!doc.apply_to_all_doctypes",
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "label": "Applicable to Document",
+ "mandatory_depends_on": "eval:!doc.apply_to_all_doctypes",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.apply_to_all_doctypes && doc.document_type",
+ "fetch_from": "document_type.istable",
+ "fieldname": "istable",
+ "fieldtype": "Check",
+ "label": " Is Child Table",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:!doc.apply_to_all_doctypes",
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Applicable Condition"
+ },
+ {
+ "default": "1",
+ "fieldname": "apply_to_all_doctypes",
+ "fieldtype": "Check",
+ "label": "Apply to All Document Types"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "target_fieldname",
+ "fieldtype": "Data",
+ "label": "Target Fieldname (Stock Ledger Entry)",
+ "read_only": 1
+ },
+ {
+ "fieldname": "source_fieldname",
+ "fieldtype": "Data",
+ "label": "Source Fieldname",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "field_mapping_section",
+ "fieldtype": "Section Break",
+ "label": "Field Mapping"
+ },
+ {
+ "default": "0",
+ "fieldname": "map_with_existing_field",
+ "fieldtype": "Check",
+ "label": "Map with existing field"
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "map_with_existing_field",
+ "fieldname": "stock_ledger_dimension",
+ "fieldtype": "Select",
+ "label": "Stock Ledger Dimension"
+ },
+ {
+ "fieldname": "type_of_transaction",
+ "fieldtype": "Select",
+ "label": "Type of Transaction",
+ "options": "\nInward\nOutward"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2022-06-22 10:56:43.753713",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Inventory Dimension",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
new file mode 100644
index 0000000..fd143da
--- /dev/null
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -0,0 +1,163 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, scrub
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.model.document import Document
+
+
+class InventoryDimension(Document):
+ def validate(self):
+ self.reset_value()
+ self.validate_reference_document()
+ self.set_source_and_target_fieldname()
+
+ def reset_value(self):
+ if self.apply_to_all_doctypes:
+ self.istable = 0
+ for field in ["document_type", "parent_field", "condition", "type_of_transaction"]:
+ self.set(field, None)
+
+ def validate_reference_document(self):
+ if frappe.get_cached_value("DocType", self.reference_document, "istable") == 1:
+ frappe.throw(_(f"The reference document {self.reference_document} can not be child table."))
+
+ if self.reference_document in ["Batch", "Serial No", "Warehouse", "Item"]:
+ frappe.throw(
+ _(f"The reference document {self.reference_document} can not be an Inventory Dimension.")
+ )
+
+ def set_source_and_target_fieldname(self):
+ self.source_fieldname = scrub(self.dimension_name)
+ if not self.map_with_existing_field:
+ self.target_fieldname = self.source_fieldname
+
+ def on_update(self):
+ self.add_custom_fields()
+
+ def add_custom_fields(self):
+ dimension_field = dict(
+ fieldname=self.source_fieldname,
+ fieldtype="Link",
+ insert_after="warehouse",
+ options=self.reference_document,
+ label=self.dimension_name,
+ )
+
+ custom_fields = {}
+
+ if self.apply_to_all_doctypes:
+ for doctype in get_inventory_documents():
+ if not frappe.db.get_value(
+ "Custom Field", {"dt": doctype[0], "fieldname": self.source_fieldname}
+ ):
+ custom_fields.setdefault(doctype[0], dimension_field)
+ elif not frappe.db.get_value(
+ "Custom Field", {"dt": self.document_type, "fieldname": self.source_fieldname}
+ ):
+ custom_fields.setdefault(self.document_type, dimension_field)
+
+ if not frappe.db.get_value(
+ "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
+ ):
+ dimension_field["fieldname"] = self.target_fieldname
+ custom_fields["Stock Ledger Entry"] = dimension_field
+
+ create_custom_fields(custom_fields)
+
+
+@frappe.whitelist()
+def get_inventory_documents(
+ doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
+):
+ and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]]
+ or_filters = [
+ ["DocField", "options", "in", ["Batch", "Serial No"]],
+ ["DocField", "parent", "in", ["Putaway Rule"]],
+ ]
+
+ if txt:
+ and_filters.append(["DocField", "parent", "like", f"%{txt}%"])
+
+ return frappe.get_all(
+ "DocField",
+ fields=["distinct parent"],
+ filters=and_filters,
+ or_filters=or_filters,
+ start=start,
+ page_length=page_len,
+ as_list=1,
+ )
+
+
+def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None) -> dict:
+ dimensions = get_document_wise_inventory_dimensions(doc.doctype)
+ for row in dimensions:
+ if (
+ row.type_of_transaction == "Inward"
+ if doc.docstatus == 1
+ else row.type_of_transaction != "Inward"
+ ) and sl_dict.actual_qty < 0:
+ continue
+ elif (
+ row.type_of_transaction == "Outward"
+ if doc.docstatus == 1
+ else row.type_of_transaction != "Inward"
+ ) and sl_dict.actual_qty > 0:
+ continue
+
+ if frappe.safe_eval(row.condition, {"doc": doc, "parent_doc": parent_doc}):
+ return row
+
+
+def get_document_wise_inventory_dimensions(doctype) -> dict:
+ if not hasattr(frappe.local, "document_wise_inventory_dimensions"):
+ frappe.local.document_wise_inventory_dimensions = {}
+
+ if doctype not in frappe.local.document_wise_inventory_dimensions:
+ dimensions = frappe.get_all(
+ "Inventory Dimension",
+ fields=["name", "source_fieldname", "condition", "target_fieldname", "type_of_transaction"],
+ filters={"disabled": 0},
+ or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
+ )
+
+ frappe.local.document_wise_inventory_dimensions[doctype] = dimensions
+
+ return frappe.local.document_wise_inventory_dimensions[doctype]
+
+
+@frappe.whitelist()
+def get_source_fieldnames(reference_document, ignore_document):
+ return frappe.get_all(
+ "Inventory Dimension",
+ fields=["source_fieldname as value", "dimension_name as label"],
+ filters={
+ "disabled": 0,
+ "map_with_existing_field": 0,
+ "name": ("!=", ignore_document),
+ "reference_document": reference_document,
+ },
+ )
+
+
+@frappe.whitelist()
+def get_inventory_dimensions():
+ if not hasattr(frappe.local, "inventory_dimensions"):
+ frappe.local.inventory_dimensions = {}
+
+ if not frappe.local.inventory_dimensions:
+ dimensions = frappe.get_all(
+ "Inventory Dimension",
+ fields=[
+ "distinct target_fieldname as fieldname",
+ "dimension_name as label",
+ "reference_document as doctype",
+ ],
+ filters={"disabled": 0, "map_with_existing_field": 0},
+ )
+
+ frappe.local.inventory_dimensions = dimensions
+
+ return frappe.local.inventory_dimensions
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
new file mode 100644
index 0000000..8d727b2
--- /dev/null
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestInventoryDimension(FrappeTestCase):
+ pass
diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js
index ce6ffa0..9b3965d 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.js
+++ b/erpnext/stock/report/stock_balance/stock_balance.js
@@ -102,3 +102,5 @@
return value;
}
};
+
+erpnext.utils.add_inventory_dimensions('Stock Balance', 8);
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 6369f91..3492b05 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -13,6 +13,7 @@
from pypika.terms import ExistsCriterion
import erpnext
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
@@ -66,9 +67,14 @@
_func = itemgetter(1)
to_date = filters.get("to_date")
- for (company, item, warehouse) in sorted(iwb_map):
+
+ for group_by_key in iwb_map:
+ item = group_by_key[1]
+ warehouse = group_by_key[2]
+ company = group_by_key[0]
+
if item_map.get(item):
- qty_dict = iwb_map[(company, item, warehouse)]
+ qty_dict = iwb_map[group_by_key]
item_reorder_level = 0
item_reorder_qty = 0
if item + warehouse in item_reorder_detail_map:
@@ -135,88 +141,104 @@
"options": "Warehouse",
"width": 100,
},
- {
- "label": _("Stock UOM"),
- "fieldname": "stock_uom",
- "fieldtype": "Link",
- "options": "UOM",
- "width": 90,
- },
- {
- "label": _("Balance Qty"),
- "fieldname": "bal_qty",
- "fieldtype": "Float",
- "width": 100,
- "convertible": "qty",
- },
- {
- "label": _("Balance Value"),
- "fieldname": "bal_val",
- "fieldtype": "Currency",
- "width": 100,
- "options": "currency",
- },
- {
- "label": _("Opening Qty"),
- "fieldname": "opening_qty",
- "fieldtype": "Float",
- "width": 100,
- "convertible": "qty",
- },
- {
- "label": _("Opening Value"),
- "fieldname": "opening_val",
- "fieldtype": "Currency",
- "width": 110,
- "options": "currency",
- },
- {
- "label": _("In Qty"),
- "fieldname": "in_qty",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80},
- {
- "label": _("Out Qty"),
- "fieldname": "out_qty",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80},
- {
- "label": _("Valuation Rate"),
- "fieldname": "val_rate",
- "fieldtype": "Currency",
- "width": 90,
- "convertible": "rate",
- "options": "currency",
- },
- {
- "label": _("Reorder Level"),
- "fieldname": "reorder_level",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {
- "label": _("Reorder Qty"),
- "fieldname": "reorder_qty",
- "fieldtype": "Float",
- "width": 80,
- "convertible": "qty",
- },
- {
- "label": _("Company"),
- "fieldname": "company",
- "fieldtype": "Link",
- "options": "Company",
- "width": 100,
- },
]
+ for dimension in get_inventory_dimensions():
+ columns.append(
+ {
+ "label": _(dimension.label),
+ "fieldname": dimension.fieldname,
+ "fieldtype": "Link",
+ "options": dimension.doctype,
+ "width": 110,
+ }
+ )
+
+ columns.extend(
+ [
+ {
+ "label": _("Stock UOM"),
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "options": "UOM",
+ "width": 90,
+ },
+ {
+ "label": _("Balance Qty"),
+ "fieldname": "bal_qty",
+ "fieldtype": "Float",
+ "width": 100,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Balance Value"),
+ "fieldname": "bal_val",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "currency",
+ },
+ {
+ "label": _("Opening Qty"),
+ "fieldname": "opening_qty",
+ "fieldtype": "Float",
+ "width": 100,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Opening Value"),
+ "fieldname": "opening_val",
+ "fieldtype": "Currency",
+ "width": 110,
+ "options": "currency",
+ },
+ {
+ "label": _("In Qty"),
+ "fieldname": "in_qty",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80},
+ {
+ "label": _("Out Qty"),
+ "fieldname": "out_qty",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80},
+ {
+ "label": _("Valuation Rate"),
+ "fieldname": "val_rate",
+ "fieldtype": "Currency",
+ "width": 90,
+ "convertible": "rate",
+ "options": "currency",
+ },
+ {
+ "label": _("Reorder Level"),
+ "fieldname": "reorder_level",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Reorder Qty"),
+ "fieldname": "reorder_qty",
+ "fieldtype": "Float",
+ "width": 80,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "width": 100,
+ },
+ ]
+ )
+
if filters.get("show_stock_ageing_data"):
columns += [
{"label": _("Average Age"), "fieldname": "average_age", "width": 100},
@@ -296,11 +318,23 @@
.orderby(sle.actual_qty)
)
+ inventory_dimension_fields = get_inventory_dimension_fields()
+ if inventory_dimension_fields:
+ query = query.select(", ".join(inventory_dimension_fields))
+
+ for fieldname in inventory_dimension_fields:
+ if fieldname in filters and filters.get(fieldname):
+ query = query.where(sle[fieldname].isin(filters.get(fieldname)))
+
if items:
query = query.where(sle.item_code.isin(items))
query = apply_conditions(query, filters)
- return query.run(as_dict=True)
+ return query.run(as_dict=True, debug=1)
+
+
+def get_inventory_dimension_fields():
+ return [dimension.fieldname for dimension in get_inventory_dimensions()]
def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
@@ -310,10 +344,12 @@
float_precision = cint(frappe.db.get_default("float_precision")) or 3
+ inventory_dimensions = get_inventory_dimension_fields()
+
for d in sle:
- key = (d.company, d.item_code, d.warehouse)
- if key not in iwb_map:
- iwb_map[key] = frappe._dict(
+ group_by_key = get_group_by_key(d, inventory_dimensions)
+ if group_by_key not in iwb_map:
+ iwb_map[group_by_key] = frappe._dict(
{
"opening_qty": 0.0,
"opening_val": 0.0,
@@ -327,7 +363,9 @@
}
)
- qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)]
+ qty_dict = iwb_map[group_by_key]
+ for field in inventory_dimensions:
+ qty_dict[field] = d.get(field)
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty)
@@ -356,24 +394,36 @@
qty_dict.bal_qty += qty_diff
qty_dict.bal_val += value_diff
- iwb_map = filter_items_with_no_transactions(iwb_map, float_precision)
+ iwb_map = filter_items_with_no_transactions(iwb_map, float_precision, inventory_dimensions)
return iwb_map
-def filter_items_with_no_transactions(iwb_map, float_precision: float):
- for (company, item, warehouse) in sorted(iwb_map):
- qty_dict = iwb_map[(company, item, warehouse)]
+def get_group_by_key(row, inventory_dimension_fields) -> tuple:
+ group_by_key = [row.company, row.item_code, row.warehouse]
+
+ for fieldname in inventory_dimension_fields:
+ group_by_key.append(row.get(fieldname))
+
+ return tuple(group_by_key)
+
+
+def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory_dimensions: list):
+ for group_by_key in iwb_map:
+ qty_dict = iwb_map[group_by_key]
no_transactions = True
for key, val in qty_dict.items():
+ if key in inventory_dimensions:
+ continue
+
val = flt(val, float_precision)
qty_dict[key] = val
if key != "val_rate" and val:
no_transactions = False
if no_transactions:
- iwb_map.pop((company, item, warehouse))
+ iwb_map.pop(group_by_key)
return iwb_map
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index ef7c2cc..0def161 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -95,4 +95,6 @@
return value;
},
-}
+};
+
+erpnext.utils.add_inventory_dimensions('Stock Ledger', 10);
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index ef1642e..1104983 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -6,6 +6,7 @@
from frappe import _
from frappe.utils import cint, flt
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
from erpnext.stock.utils import (
@@ -17,7 +18,7 @@
def execute(filters=None):
is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
- columns = get_columns()
+ columns = get_columns(filters)
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom)
@@ -33,12 +34,14 @@
actual_qty = stock_value = 0
available_serial_nos = {}
+ inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
+
for sle in sl_entries:
item_detail = item_details[sle.item_code]
sle.update(item_detail)
- if filters.get("batch_no"):
+ if filters.get("batch_no") or inventory_dimension_filters_applied:
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
@@ -88,7 +91,7 @@
sle.balance_serial_no = "\n".join(existing_serial_no)
-def get_columns():
+def get_columns(filters):
columns = [
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150},
{
@@ -216,14 +219,28 @@
"options": "Project",
"width": 100,
},
+ ]
+
+ for dimension in get_inventory_dimensions():
+ columns.append(
+ {
+ "label": _(dimension.label),
+ "fieldname": dimension.fieldname,
+ "fieldtype": "Link",
+ "options": dimension.doctype,
+ "width": 110,
+ }
+ )
+
+ columns.append(
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 110,
- },
- ]
+ }
+ )
return columns
@@ -252,7 +269,7 @@
serial_no,
company,
project,
- stock_value_difference
+ stock_value_difference {get_dimension_fields}
FROM
`tabStock Ledger Entry` sle
WHERE
@@ -263,7 +280,9 @@
ORDER BY
posting_date asc, posting_time asc, creation asc
""".format(
- sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql
+ sle_conditions=get_sle_conditions(filters),
+ item_conditions_sql=item_conditions_sql,
+ get_dimension_fields=get_dimension_fields(),
),
filters,
as_dict=1,
@@ -272,6 +291,15 @@
return sl_entries
+def get_dimension_fields() -> str:
+ fields = ""
+
+ for dimension in get_inventory_dimensions():
+ fields += f", {dimension.fieldname}"
+
+ return fields
+
+
def get_items(filters):
conditions = []
if filters.get("item_code"):
@@ -341,6 +369,10 @@
if filters.get("project"):
conditions.append("project=%(project)s")
+ for dimension in get_inventory_dimensions():
+ if filters.get(dimension.fieldname):
+ conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s")
+
return "and {}".format(" and ".join(conditions)) if conditions else ""
@@ -401,3 +433,11 @@
)
return ""
+
+
+def check_inventory_dimension_filters_applied(filters) -> bool:
+ for dimension in get_inventory_dimensions():
+ if dimension.fieldname in filters and filters.get(dimension.fieldname):
+ return True
+
+ return False