From 6798b900ef3ef10a0582c074cc535210df72e550 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 9 May 2023 16:07:14 +0530 Subject: [PATCH] fix: inventory dimension for material transfer not working --- erpnext/controllers/stock_controller.py | 24 ++- .../inventory_dimension.py | 139 +++++++++++++++--- .../test_inventory_dimension.py | 97 ++++++++++++ 3 files changed, 237 insertions(+), 23 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a27e34819d..796a069651 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -442,7 +442,29 @@ class StockController(AccountsController): if not dimension: continue - if row.get(dimension.source_fieldname): + if self.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Sales Invoice", + "Delivery Note", + "Stock Entry", + ]: + if (sl_dict.actual_qty > 0 and self.doctype in ["Purchase Invoice", "Purchase Receipt"]) or ( + sl_dict.actual_qty < 0 and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"] + ): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + else: + fieldname_start_with = "to" + if self.doctype in ["Purchase Invoice", "Purchase Receipt"]: + fieldname_start_with = "from" + + fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}" + sl_dict[dimension.target_fieldname] = row.get(fieldname) + + if not sl_dict.get(dimension.target_fieldname): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + + elif row.get(dimension.source_fieldname): sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index db2b5d0a6b..8bff4d5147 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -75,7 +75,16 @@ class InventoryDimension(Document): self.delete_custom_fields() def delete_custom_fields(self): - filters = {"fieldname": self.source_fieldname} + filters = { + "fieldname": ( + "in", + [ + self.source_fieldname, + f"to_{self.source_fieldname}", + f"from_{self.source_fieldname}", + ], + ) + } if self.document_type: filters["dt"] = self.document_type @@ -88,6 +97,8 @@ class InventoryDimension(Document): def reset_value(self): if self.apply_to_all_doctypes: + self.type_of_transaction = "" + self.istable = 0 for field in ["document_type", "condition"]: self.set(field, None) @@ -111,12 +122,35 @@ class InventoryDimension(Document): def on_update(self): self.add_custom_fields() - def add_custom_fields(self): - dimension_fields = [ + @staticmethod + def get_insert_after_fieldname(doctype): + return frappe.get_all( + "DocField", + fields=["fieldname"], + filters={"parent": doctype}, + order_by="idx desc", + limit=1, + )[0].fieldname + + def get_dimension_fields(self, doctype=None): + if not doctype: + doctype = self.document_type + + label_start_with = "" + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + label_start_with = "Target" + elif doctype in ["Sales Invoice Item", "Delivery Note Item", "Stock Entry Detail"]: + label_start_with = "Source" + + label = self.dimension_name + if label_start_with: + label = f"{label_start_with} {self.dimension_name}" + + return [ dict( fieldname="inventory_dimension", fieldtype="Section Break", - insert_after="warehouse", + insert_after=self.get_insert_after_fieldname(doctype), label="Inventory Dimension", collapsible=1, ), @@ -125,24 +159,37 @@ class InventoryDimension(Document): fieldtype="Link", insert_after="inventory_dimension", options=self.reference_document, - label=self.dimension_name, + label=label, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), ] + def add_custom_fields(self): custom_fields = {} + dimension_fields = [] if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if not field_exists(doctype[0], self.source_fieldname): - custom_fields.setdefault(doctype[0], dimension_fields) + if field_exists(doctype[0], self.source_fieldname): + continue + + dimension_fields = self.get_dimension_fields(doctype[0]) + self.add_transfer_field(doctype[0], dimension_fields) + custom_fields.setdefault(doctype[0], dimension_fields) elif not field_exists(self.document_type, self.source_fieldname): + dimension_fields = self.get_dimension_fields() + + self.add_transfer_field(self.document_type, dimension_fields) custom_fields.setdefault(self.document_type, dimension_fields) - if not frappe.db.get_value( - "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} - ) and not field_exists("Stock Ledger Entry", self.target_fieldname): + if ( + dimension_fields + and not frappe.db.get_value( + "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} + ) + and not field_exists("Stock Ledger Entry", self.target_fieldname) + ): dimension_field = dimension_fields[1] dimension_field["mandatory_depends_on"] = "" dimension_field["reqd"] = 0 @@ -152,6 +199,53 @@ class InventoryDimension(Document): if custom_fields: create_custom_fields(custom_fields) + def add_transfer_field(self, doctype, dimension_fields): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + return + + fieldname_start_with = "to" + label_start_with = "Target" + display_depends_on = "" + + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + fieldname_start_with = "from" + label_start_with = "Source" + display_depends_on = "eval:parent.is_internal_supplier == 1" + elif doctype != "Stock Entry Detail": + display_depends_on = "eval:parent.is_internal_customer == 1" + elif doctype == "Stock Entry Detail": + display_depends_on = "eval:parent.purpose != 'Material Issue'" + + fieldname = f"{fieldname_start_with}_{self.source_fieldname}" + label = f"{label_start_with} {self.dimension_name}" + + if field_exists(doctype, fieldname): + return + + dimension_fields.extend( + [ + dict( + fieldname="inventory_dimension_col_break", + fieldtype="Column Break", + insert_after=self.source_fieldname, + ), + dict( + fieldname=fieldname, + fieldtype="Link", + insert_after="inventory_dimension_col_break", + options=self.reference_document, + label=label, + depends_on=display_depends_on, + ), + ] + ) + def field_exists(doctype, fieldname) -> str or None: return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @@ -185,18 +279,19 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None): dimensions = get_document_wise_inventory_dimensions(doc.doctype) filter_dimensions = [] 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 != "Outward" - ) and sl_dict.actual_qty > 0: - continue + if row.type_of_transaction: + 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 != "Outward" + ) and sl_dict.actual_qty > 0: + continue evals = {"doc": doc} if parent_doc: diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 28b1ed96f0..ae5f521f2b 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -12,6 +12,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( DoNotChangeError, delete_dimension, ) +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -20,6 +21,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestInventoryDimension(FrappeTestCase): def setUp(self): prepare_test_data() + create_store_dimension() def test_validate_inventory_dimension(self): # Can not be child doc @@ -73,6 +75,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertFalse(custom_field) def test_inventory_dimension(self): + frappe.local.document_wise_inventory_dimensions = {} + warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -143,6 +147,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertRaises(DoNotChangeError, inv_dim1.save) def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + frappe.local.document_wise_inventory_dimensions = {} + inv_dimension = create_inventory_dimension( reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 ) @@ -250,6 +256,97 @@ class TestInventoryDimension(FrappeTestCase): ) ) + def test_for_purchase_sales_and_stock_transaction(self): + create_inventory_dimension( + reference_document="Store", + type_of_transaction="Outward", + dimension_name="Store", + apply_to_all_doctypes=1, + ) + + item_code = "Test Inventory Dimension Item" + create_item(item_code) + warehouse = create_warehouse("Store Warehouse") + + # Purchase Receipt -> Inward in Store 1 + pr_doc = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + ) + + pr_doc.items[0].store = "Store 1" + pr_doc.save() + pr_doc.submit() + + entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 1") + + # Stock Entry -> Transfer from Store 1 to Store 2 + se_doc = make_stock_entry( + item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True + ) + + se_doc.items[0].store = "Store 1" + se_doc.items[0].to_store = "Store 2" + + se_doc.save() + se_doc.submit() + + entries = get_voucher_sl_entries( + se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"] + ) + + for entry in entries: + self.assertEqual(entry.warehouse, warehouse) + if entry.actual_qty > 0: + self.assertEqual(entry.store, "Store 2") + self.assertEqual(entry.incoming_rate, 100.0) + else: + self.assertEqual(entry.store, "Store 1") + + # Delivery Note -> Outward from Store 2 + + dn_doc = create_delivery_note(item_code=item_code, qty=10, warehouse=warehouse, do_not_save=True) + + dn_doc.items[0].store = "Store 2" + dn_doc.save() + dn_doc.submit() + + entries = get_voucher_sl_entries(dn_doc.name, ["warehouse", "store", "actual_qty"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 2") + self.assertEqual(entries[0].actual_qty, -10.0) + + +def get_voucher_sl_entries(voucher_no, fields): + return frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": voucher_no}, fields=fields, order_by="creation" + ) + + +def create_store_dimension(): + if not frappe.db.exists("DocType", "Store"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Store", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:store_name", + "fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for store in ["Store 1", "Store 2"]: + if not frappe.db.exists("Store", store): + frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"):