From e576f7f07e41f82cd831885770add3b20754606c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Jun 2022 19:12:06 +0530 Subject: [PATCH] test: test cases for inventory dimension --- erpnext/controllers/stock_controller.py | 5 +- .../inventory_dimension.js | 26 ++--- .../inventory_dimension.json | 41 ++++--- .../inventory_dimension.py | 76 ++++++++----- .../test_inventory_dimension.py | 107 +++++++++++++++++- .../report/stock_balance/stock_balance.py | 18 +-- .../stock/report/stock_ledger/stock_ledger.py | 2 +- 7 files changed, 198 insertions(+), 77 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index da7f5e23cbb..40bc1aa2072 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -367,14 +367,13 @@ class StockController(AccountsController): ) sl_dict.update(args) - if self.docstatus == 1: - self.update_inventory_dimensions(d, sl_dict) + 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: + if dimension and row.get(dimension.source_fieldname): 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): diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 12566592359..91a21f4e722 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -32,25 +32,15 @@ frappe.ui.form.on('Inventory Dimension', { frm.trigger('render_traget_field'); }, - map_with_existing_field(frm) { - frm.trigger('render_traget_field'); - }, + refresh(frm) { + if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger + && frm.doc.__onload.has_stock_ledger.length) { + let msg = __('Stock transactions exists against this dimension, user can not update document.'); + frm.dashboard.add_comment(msg, 'blue', true); - 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')); - } + frm.fields.forEach((field) => { + if (field.df.fieldname !== 'disabled') { + frm.set_df_property(field.df.fieldname, "read_only", "1"); } }); } diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 7e5df423813..cfac5cdfae5 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -10,21 +10,22 @@ "dimension_details_tab", "dimension_name", "reference_document", + "column_break_4", "disabled", "section_break_7", "field_mapping_section", "source_fieldname", - "target_fieldname", "column_break_9", - "map_with_existing_field", - "stock_ledger_dimension", + "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", "document_type", "istable", "type_of_transaction", "column_break_16", - "condition" + "condition", + "applicable_condition_example_section", + "html_19" ], "fields": [ { @@ -81,7 +82,7 @@ "label": "Applicable Condition" }, { - "default": "1", + "default": "0", "fieldname": "apply_to_all_doctypes", "fieldtype": "Check", "label": "Apply to All Document Types" @@ -114,32 +115,36 @@ "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" + }, + { + "fieldname": "html_19", + "fieldtype": "HTML", + "options": "\n\n \n \n \n \n\n\n\n \n \n\n\n \n \n\n\n\n
Child DocumentNon Child Document
\n

To access parent document field use parent.fieldname and to access child table document field use doc.fieldname

\n\n
\n

To access document field use doc.fieldname

\n
\n

Example: parent.doctype == \"Stock Entry\" and doc.item_code == \"Test\"

\n\n
\n

Example: doc.doctype == \"Stock Entry\" and doc.purpose == \"Manufacture\"

\n
\n\n\n\n\n\n\n" + }, + { + "collapsible": 1, + "depends_on": "eval:!doc.apply_to_all_doctypes", + "fieldname": "applicable_condition_example_section", + "fieldtype": "Section Break", + "label": "Applicable Condition Examples" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-22 10:56:43.753713", + "modified": "2022-07-05 15:33:37.270373", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index fd143dafda2..3b9a84a3f22 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -2,36 +2,67 @@ # For license information, please see license.txt import frappe -from frappe import _, scrub +from frappe import _, bold, scrub from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.model.document import Document +class DoNotChangeError(frappe.ValidationError): + pass + + class InventoryDimension(Document): + def onload(self): + if not self.is_new() and frappe.db.has_column("Stock Ledger Entry", self.target_fieldname): + self.set_onload("has_stock_ledger", self.has_stock_ledger()) + + def has_stock_ledger(self) -> str: + if not self.target_fieldname: + return + + return frappe.get_all( + "Stock Ledger Entry", filters={self.target_fieldname: ("is", "set"), "is_cancelled": 0}, limit=1 + ) + def validate(self): + self.do_not_update_document() self.reset_value() self.validate_reference_document() self.set_source_and_target_fieldname() + def do_not_update_document(self): + if self.is_new() or not self.has_stock_ledger(): + return + + old_doc = self._doc_before_save + for field in frappe.get_meta("Inventory Dimension").fields: + if field.fieldname != "disabled" and old_doc.get(field.fieldname) != self.get(field.fieldname): + msg = f"""The user can not change value of the field {bold(field.label)} because + stock transactions exists against the dimension {bold(self.name)}.""" + + frappe.throw(_(msg), DoNotChangeError) + def reset_value(self): if self.apply_to_all_doctypes: self.istable = 0 - for field in ["document_type", "parent_field", "condition", "type_of_transaction"]: + for field in ["document_type", "condition"]: 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.")) + msg = f"The reference document {self.reference_document} can not be child table." + frappe.throw(_(msg)) 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.") - ) + msg = f"The reference document {self.reference_document} can not be an Inventory Dimension." + frappe.throw(_(msg)) - 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 set_source_and_target_fieldname(self) -> None: + if not self.source_fieldname: + self.source_fieldname = scrub(self.dimension_name) + + if not self.target_fieldname: + self.target_fieldname = scrub(self.reference_document) def on_update(self): self.add_custom_fields() @@ -107,7 +138,11 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None) -> dict: ) and sl_dict.actual_qty > 0: continue - if frappe.safe_eval(row.condition, {"doc": doc, "parent_doc": parent_doc}): + evals = {"doc": doc} + if parent_doc: + evals["parent"] = parent_doc + + if frappe.safe_eval(row.condition, evals): return row @@ -115,7 +150,7 @@ 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: + if not frappe.local.document_wise_inventory_dimensions.get(doctype): dimensions = frappe.get_all( "Inventory Dimension", fields=["name", "source_fieldname", "condition", "target_fieldname", "type_of_transaction"], @@ -128,20 +163,6 @@ def get_document_wise_inventory_dimensions(doctype) -> dict: 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"): @@ -152,10 +173,9 @@ def get_inventory_dimensions(): "Inventory Dimension", fields=[ "distinct target_fieldname as fieldname", - "dimension_name as label", "reference_document as doctype", ], - filters={"disabled": 0, "map_with_existing_field": 0}, + filters={"disabled": 0}, ) frappe.local.inventory_dimensions = dimensions diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 8d727b21755..a79de1a7925 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -1,9 +1,112 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + class TestInventoryDimension(FrappeTestCase): - pass + def setUp(self): + prepare_test_data() + + def test_inventory_dimension(self): + warehouse = "Shelf Warehouse - _TC" + item_code = "_Test Item" + + create_inventory_dimension( + reference_document="Shelf", + type_of_transaction="Outward", + dimension_name="Shelf", + apply_to_all_doctypes=0, + document_type="Stock Entry Detail", + condition="parent.purpose == 'Material Issue'", + ) + + create_inventory_dimension( + reference_document="Shelf", + type_of_transaction="Inward", + dimension_name="To Shelf", + apply_to_all_doctypes=0, + document_type="Stock Entry Detail", + condition="parent.purpose == 'Material Receipt'", + ) + + inward = make_stock_entry( + item_code=item_code, + target=warehouse, + qty=5, + basic_rate=10, + do_not_save=True, + purpose="Material Receipt", + ) + + inward.items[0].to_shelf = "Shelf 1" + inward.save() + inward.submit() + inward.load_from_db() + print(inward.name) + + sle_data = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": inward.name}, ["shelf", "warehouse"], as_dict=1 + ) + + self.assertEqual(inward.items[0].to_shelf, "Shelf 1") + self.assertEqual(sle_data.warehouse, warehouse) + self.assertEqual(sle_data.shelf, "Shelf 1") + + outward = make_stock_entry( + item_code=item_code, + source=warehouse, + qty=3, + basic_rate=10, + do_not_save=True, + purpose="Material Issue", + ) + + outward.items[0].shelf = "Shelf 1" + outward.save() + outward.submit() + outward.load_from_db() + + sle_shelf = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": outward.name}, "shelf") + self.assertEqual(sle_shelf, "Shelf 1") + + +def prepare_test_data(): + if not frappe.db.exists("DocType", "Shelf"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Shelf", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:shelf_name", + "fields": [{"label": "Shelf Name", "fieldname": "shelf_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for shelf in ["Shelf 1", "Shelf 2"]: + if not frappe.db.exists("Shelf", shelf): + frappe.get_doc({"doctype": "Shelf", "shelf_name": shelf}).insert(ignore_permissions=True) + + create_warehouse("Shelf Warehouse") + + +def create_inventory_dimension(**args): + args = frappe._dict(args) + + if frappe.db.exists("Inventory Dimension", args.dimension_name): + return frappe.get_doc("Inventory Dimension", args.dimension_name) + + doc = frappe.new_doc("Inventory Dimension") + doc.update(args) + doc.insert(ignore_permissions=True) + + return doc diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 3492b054ec4..a1e1030d9e7 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -146,7 +146,7 @@ def get_columns(filters: StockBalanceFilter): for dimension in get_inventory_dimensions(): columns.append( { - "label": _(dimension.label), + "label": _(dimension.doctype), "fieldname": dimension.fieldname, "fieldtype": "Link", "options": dimension.doctype, @@ -320,9 +320,8 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L inventory_dimension_fields = get_inventory_dimension_fields() if inventory_dimension_fields: - query = query.select(", ".join(inventory_dimension_fields)) - for fieldname in inventory_dimension_fields: + query = query.select(fieldname) if fieldname in filters and filters.get(fieldname): query = query.where(sle[fieldname].isin(filters.get(fieldname))) @@ -347,7 +346,7 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): inventory_dimensions = get_inventory_dimension_fields() for d in sle: - group_by_key = get_group_by_key(d, inventory_dimensions) + group_by_key = get_group_by_key(d, filters, inventory_dimensions) if group_by_key not in iwb_map: iwb_map[group_by_key] = frappe._dict( { @@ -399,16 +398,18 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): return iwb_map -def get_group_by_key(row, inventory_dimension_fields) -> tuple: +def get_group_by_key(row, filters, 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)) + if filters.get(fieldname): + 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): + pop_keys = [] for group_by_key in iwb_map: qty_dict = iwb_map[group_by_key] @@ -423,7 +424,10 @@ def filter_items_with_no_transactions(iwb_map, float_precision: float, inventory no_transactions = False if no_transactions: - iwb_map.pop(group_by_key) + pop_keys.append(group_by_key) + + for key in pop_keys: + iwb_map.pop(key) return iwb_map diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 11049832487..807b800c7ce 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -224,7 +224,7 @@ def get_columns(filters): for dimension in get_inventory_dimensions(): columns.append( { - "label": _(dimension.label), + "label": _(dimension.doctype), "fieldname": dimension.fieldname, "fieldtype": "Link", "options": dimension.doctype,