test: test cases for inventory dimension

This commit is contained in:
Rohit Waghchaure 2022-06-30 19:12:06 +05:30
parent dbec5cff00
commit e576f7f07e
7 changed files with 198 additions and 77 deletions

View File

@ -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):

View File

@ -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");
}
});
}

View File

@ -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": "<table class=\"table table-bordered table-condensed\">\n<thead>\n <tr>\n <th class=\"table-sr\" style=\"width: 50%;\">Child Document</th>\n <th class=\"table-sr\" style=\"width: 50%;\">Non Child Document</th>\n </tr>\n</thead>\n<tbody>\n<tr>\n <td>\n <p> To access parent document field use parent.fieldname and to access child table document field use doc.fieldname </p>\n\n </td>\n <td>\n <p>To access document field use doc.fieldname </p>\n </td>\n</tr>\n<tr>\n <td>\n <p><b>Example: </b> parent.doctype == \"Stock Entry\" and doc.item_code == \"Test\" </p>\n\n </td>\n <td>\n <p><b>Example: </b> doc.doctype == \"Stock Entry\" and doc.purpose == \"Manufacture\"</p> \n </td>\n</tr>\n\n</tbody>\n</table>\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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,