diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e90a4f6241..da7f5e23cb 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -18,6 +18,9 @@ from erpnext.accounts.general_ledger import ( 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 @@ class StockController(AccountsController): ) 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 62abb74c34..421a63ae6b 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -224,6 +224,32 @@ $.extend(erpnext.utils, { }); }, + 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 0000000000..e69de29bb2 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 0000000000..1256659235 --- /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 0000000000..7e5df42381 --- /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 0000000000..fd143dafda --- /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 0000000000..8d727b2175 --- /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 ce6ffa0b91..9b3965d0d6 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -102,3 +102,5 @@ frappe.query_reports["Stock Balance"] = { 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 6369f910a4..3492b054ec 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -13,6 +13,7 @@ from frappe.utils.nestedset import get_descendants_of 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 @@ def execute(filters: Optional[StockBalanceFilter] = None): _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 @@ def get_columns(filters: StockBalanceFilter): "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 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L .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 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): 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 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): } ) - 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 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): 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 ef7c2cc7d9..0def161d28 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -95,4 +95,6 @@ frappe.query_reports["Stock Ledger"] = { 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 ef1642e1f9..1104983248 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -6,6 +6,7 @@ import frappe 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 @@ from erpnext.stock.utils import ( 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 @@ def execute(filters=None): 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 @@ def update_available_serial_nos(available_serial_nos, sle): 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 @@ def get_columns(): "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 @@ def get_stock_ledger_entries(filters, items): serial_no, company, project, - stock_value_difference + stock_value_difference {get_dimension_fields} FROM `tabStock Ledger Entry` sle WHERE @@ -263,7 +280,9 @@ def get_stock_ledger_entries(filters, items): 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 @@ def get_stock_ledger_entries(filters, items): 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 @@ def get_sle_conditions(filters): 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 @@ def get_item_group_condition(item_group): ) 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