Merge pull request #31424 from rohitwaghchaure/feat-inventory-dimension
feat: Inventory Dimension
This commit is contained in:
commit
5a3999e236
@ -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)
|
||||
self.update_inventory_dimensions(d, sl_dict)
|
||||
|
||||
return sl_dict
|
||||
|
||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||
dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
|
||||
for dimension in dimensions:
|
||||
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):
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
|
||||
|
@ -213,6 +213,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",
|
||||
|
@ -0,0 +1,48 @@
|
||||
// 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');
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
frm.fields.forEach((field) => {
|
||||
if (field.df.fieldname !== 'disabled') {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,194 @@
|
||||
{
|
||||
"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",
|
||||
"column_break_4",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"field_mapping_section",
|
||||
"source_fieldname",
|
||||
"column_break_9",
|
||||
"target_fieldname",
|
||||
"applicable_for_documents_tab",
|
||||
"apply_to_all_doctypes",
|
||||
"document_type",
|
||||
"istable",
|
||||
"type_of_transaction",
|
||||
"column_break_16",
|
||||
"condition",
|
||||
"applicable_condition_example_section",
|
||||
"html_19"
|
||||
],
|
||||
"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": "0",
|
||||
"fieldname": "apply_to_all_doctypes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply to All Inventory 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"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"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-07-19 21:06:11.824976",
|
||||
"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": []
|
||||
}
|
196
erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
Normal file
196
erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
Normal file
@ -0,0 +1,196 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
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 CanNotBeChildDoc(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class CanNotBeDefaultDimension(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", "condition"]:
|
||||
self.set(field, None)
|
||||
|
||||
def validate_reference_document(self):
|
||||
if frappe.get_cached_value("DocType", self.reference_document, "istable") == 1:
|
||||
msg = f"The reference document {self.reference_document} can not be child table."
|
||||
frappe.throw(_(msg), CanNotBeChildDoc)
|
||||
|
||||
if self.reference_document in ["Batch", "Serial No", "Warehouse", "Item"]:
|
||||
msg = f"The reference document {self.reference_document} can not be an Inventory Dimension."
|
||||
frappe.throw(_(msg), CanNotBeDefaultDimension)
|
||||
|
||||
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()
|
||||
|
||||
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):
|
||||
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 != "Inward"
|
||||
) and sl_dict.actual_qty > 0:
|
||||
continue
|
||||
|
||||
evals = {"doc": doc}
|
||||
if parent_doc:
|
||||
evals["parent"] = parent_doc
|
||||
|
||||
if row.condition and frappe.safe_eval(row.condition, evals):
|
||||
filter_dimensions.append(row)
|
||||
else:
|
||||
filter_dimensions.append(row)
|
||||
|
||||
return filter_dimensions
|
||||
|
||||
|
||||
def get_document_wise_inventory_dimensions(doctype) -> dict:
|
||||
if not hasattr(frappe.local, "document_wise_inventory_dimensions"):
|
||||
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"],
|
||||
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_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",
|
||||
"reference_document as doctype",
|
||||
],
|
||||
filters={"disabled": 0},
|
||||
)
|
||||
|
||||
frappe.local.inventory_dimensions = dimensions
|
||||
|
||||
return frappe.local.inventory_dimensions
|
@ -0,0 +1,149 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
CanNotBeChildDoc,
|
||||
CanNotBeDefaultDimension,
|
||||
DoNotChangeError,
|
||||
)
|
||||
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):
|
||||
def setUp(self):
|
||||
prepare_test_data()
|
||||
|
||||
def test_validate_inventory_dimension(self):
|
||||
# Can not be child doc
|
||||
inv_dim1 = create_inventory_dimension(
|
||||
reference_document="Stock Entry Detail",
|
||||
type_of_transaction="Outward",
|
||||
dimension_name="Stock Entry",
|
||||
apply_to_all_doctypes=0,
|
||||
istable=0,
|
||||
document_type="Stock Entry",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertRaises(CanNotBeChildDoc, inv_dim1.insert)
|
||||
|
||||
inv_dim1 = create_inventory_dimension(
|
||||
reference_document="Batch",
|
||||
type_of_transaction="Outward",
|
||||
dimension_name="Batch",
|
||||
apply_to_all_doctypes=0,
|
||||
document_type="Stock Entry Detail",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertRaises(CanNotBeDefaultDimension, inv_dim1.insert)
|
||||
|
||||
def test_inventory_dimension(self):
|
||||
warehouse = "Shelf Warehouse - _TC"
|
||||
item_code = "_Test Item"
|
||||
|
||||
inv_dim1 = 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()
|
||||
|
||||
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")
|
||||
|
||||
inv_dim1.load_from_db()
|
||||
inv_dim1.apply_to_all_doctypes = 1
|
||||
|
||||
self.assertTrue(inv_dim1.has_stock_ledger())
|
||||
self.assertRaises(DoNotChangeError, inv_dim1.save)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if not args.do_not_save:
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
@ -478,10 +478,10 @@ class StockEntry(StockController):
|
||||
if not d.s_warehouse:
|
||||
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
|
||||
|
||||
if (
|
||||
cstr(d.s_warehouse) == cstr(d.t_warehouse)
|
||||
and not self.purpose == "Material Transfer for Manufacture"
|
||||
):
|
||||
if cstr(d.s_warehouse) == cstr(d.t_warehouse) and self.purpose not in [
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Transfer",
|
||||
]:
|
||||
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
|
||||
|
||||
if not (d.s_warehouse or d.t_warehouse):
|
||||
|
@ -102,3 +102,5 @@ frappe.query_reports["Stock Balance"] = {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.utils.add_inventory_dimensions('Stock Balance', 8);
|
@ -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.doctype),
|
||||
"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,6 +318,13 @@ 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:
|
||||
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)))
|
||||
|
||||
if items:
|
||||
query = query.where(sle.item_code.isin(items))
|
||||
|
||||
@ -303,6 +332,10 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_inventory_dimension_fields():
|
||||
return [dimension.fieldname for dimension in get_inventory_dimensions()]
|
||||
|
||||
|
||||
def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
|
||||
iwb_map = {}
|
||||
from_date = getdate(filters.get("from_date"))
|
||||
@ -310,10 +343,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, filters, 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 +362,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 +393,41 @@ 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, filters, inventory_dimension_fields) -> tuple:
|
||||
group_by_key = [row.company, row.item_code, row.warehouse]
|
||||
|
||||
for fieldname in inventory_dimension_fields:
|
||||
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]
|
||||
|
||||
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))
|
||||
pop_keys.append(group_by_key)
|
||||
|
||||
for key in pop_keys:
|
||||
iwb_map.pop(key)
|
||||
|
||||
return iwb_map
|
||||
|
||||
|
@ -95,4 +95,6 @@ frappe.query_reports["Stock Ledger"] = {
|
||||
|
||||
return value;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.utils.add_inventory_dimensions('Stock Ledger', 10);
|
@ -4,8 +4,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.utils import cint, flt
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
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 +20,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 +36,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
|
||||
|
||||
@ -67,7 +72,7 @@ def update_available_serial_nos(available_serial_nos, sle):
|
||||
key = (sle.item_code, sle.warehouse)
|
||||
if key not in available_serial_nos:
|
||||
stock_balance = get_stock_balance_for(
|
||||
sle.item_code, sle.warehouse, sle.date.split(" ")[0], sle.date.split(" ")[1]
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time
|
||||
)
|
||||
serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else []
|
||||
available_serial_nos.setdefault(key, serials)
|
||||
@ -88,7 +93,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},
|
||||
{
|
||||
@ -106,170 +111,210 @@ def get_columns():
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("In Qty"),
|
||||
"fieldname": "in_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Out Qty"),
|
||||
"fieldname": "out_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Balance Qty"),
|
||||
"fieldname": "qty_after_transaction",
|
||||
"fieldtype": "Float",
|
||||
"width": 100,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"options": "Brand",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Description"), "fieldname": "description", "width": 200},
|
||||
{
|
||||
"label": _("Incoming Rate"),
|
||||
"fieldname": "incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
"label": _("Balance Value"),
|
||||
"fieldname": "stock_value",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("Value Change"),
|
||||
"fieldname": "stock_value_difference",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Batch"),
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"options": "Batch",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Link",
|
||||
"options": "Serial No",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||
{
|
||||
"label": _("Project"),
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 110,
|
||||
},
|
||||
]
|
||||
|
||||
for dimension in get_inventory_dimensions():
|
||||
columns.append(
|
||||
{
|
||||
"label": _(dimension.doctype),
|
||||
"fieldname": dimension.fieldname,
|
||||
"fieldtype": "Link",
|
||||
"options": dimension.doctype,
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("In Qty"),
|
||||
"fieldname": "in_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Out Qty"),
|
||||
"fieldname": "out_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Balance Qty"),
|
||||
"fieldname": "qty_after_transaction",
|
||||
"fieldtype": "Float",
|
||||
"width": 100,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"options": "Brand",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Description"), "fieldname": "description", "width": 200},
|
||||
{
|
||||
"label": _("Incoming Rate"),
|
||||
"fieldname": "incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
},
|
||||
{
|
||||
"label": _("Balance Value"),
|
||||
"fieldname": "stock_value",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("Value Change"),
|
||||
"fieldname": "stock_value_difference",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Batch"),
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"options": "Batch",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Link",
|
||||
"options": "Serial No",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||
{
|
||||
"label": _("Project"),
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 110,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_stock_ledger_entries(filters, items):
|
||||
item_conditions_sql = ""
|
||||
if items:
|
||||
item_conditions_sql = "and sle.item_code in ({})".format(
|
||||
", ".join(frappe.db.escape(i) for i in items)
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(
|
||||
sle.item_code,
|
||||
CombineDatetime(sle.posting_date, sle.posting_time).as_("date"),
|
||||
sle.warehouse,
|
||||
sle.posting_date,
|
||||
sle.posting_time,
|
||||
sle.actual_qty,
|
||||
sle.incoming_rate,
|
||||
sle.valuation_rate,
|
||||
sle.company,
|
||||
sle.voucher_type,
|
||||
sle.qty_after_transaction,
|
||||
sle.stock_value_difference,
|
||||
sle.voucher_no,
|
||||
sle.stock_value,
|
||||
sle.batch_no,
|
||||
sle.serial_no,
|
||||
sle.project,
|
||||
)
|
||||
|
||||
sl_entries = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
concat_ws(' ', posting_date, posting_time) AS date,
|
||||
item_code,
|
||||
warehouse,
|
||||
actual_qty,
|
||||
qty_after_transaction,
|
||||
incoming_rate,
|
||||
valuation_rate,
|
||||
stock_value,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
batch_no,
|
||||
serial_no,
|
||||
company,
|
||||
project,
|
||||
stock_value_difference
|
||||
FROM
|
||||
`tabStock Ledger Entry` sle
|
||||
WHERE
|
||||
company = %(company)s
|
||||
AND is_cancelled = 0 AND posting_date BETWEEN %(from_date)s AND %(to_date)s
|
||||
{sle_conditions}
|
||||
{item_conditions_sql}
|
||||
ORDER BY
|
||||
posting_date asc, posting_time asc, creation asc
|
||||
""".format(
|
||||
sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
.where(
|
||||
(sle.docstatus < 2)
|
||||
& (sle.is_cancelled == 0)
|
||||
& (sle.posting_date[filters.from_date : filters.to_date])
|
||||
)
|
||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
||||
.orderby(sle.creation)
|
||||
)
|
||||
|
||||
return sl_entries
|
||||
inventory_dimension_fields = get_inventory_dimension_fields()
|
||||
if 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)))
|
||||
|
||||
if items:
|
||||
query = query.where(sle.item_code.isin(items))
|
||||
|
||||
for field in ["voucher_no", "batch_no", "project", "company"]:
|
||||
if filters.get(field):
|
||||
query = query.where(sle[field] == filters.get(field))
|
||||
|
||||
if warehouse := filters.get("warehouse"):
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
|
||||
warehouse_table = frappe.qb.DocType("Warehouse")
|
||||
chilren_subquery = (
|
||||
frappe.qb.from_(warehouse_table)
|
||||
.select(warehouse_table.name)
|
||||
.where(
|
||||
(warehouse_table.lft >= lft)
|
||||
& (warehouse_table.rgt <= rgt)
|
||||
& (warehouse_table.name == sle.warehouse)
|
||||
)
|
||||
)
|
||||
query = query.where(ExistsCriterion(chilren_subquery))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_inventory_dimension_fields():
|
||||
return [dimension.fieldname for dimension in get_inventory_dimensions()]
|
||||
|
||||
|
||||
def get_items(filters):
|
||||
@ -341,6 +386,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 ""
|
||||
|
||||
|
||||
@ -363,7 +412,7 @@ def get_opening_balance(filters, columns, sl_entries):
|
||||
for sle in sl_entries:
|
||||
if (
|
||||
sle.get("voucher_type") == "Stock Reconciliation"
|
||||
and sle.get("date").split()[0] == filters.from_date
|
||||
and sle.posting_date == filters.from_date
|
||||
and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock"
|
||||
):
|
||||
last_entry = sle
|
||||
@ -401,3 +450,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
|
||||
|
Loading…
x
Reference in New Issue
Block a user