feat: Inventory Dimension
This commit is contained in:
parent
409c2e98a9
commit
dbec5cff00
@ -18,6 +18,9 @@ from erpnext.accounts.general_ledger import (
|
|||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
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
|
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||||
|
|
||||||
|
|
||||||
@ -364,8 +367,16 @@ class StockController(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
sl_dict.update(args)
|
sl_dict.update(args)
|
||||||
|
if self.docstatus == 1:
|
||||||
|
self.update_inventory_dimensions(d, sl_dict)
|
||||||
|
|
||||||
return 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):
|
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
|
from erpnext.stock.stock_ledger import make_sl_entries
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
make_subscription: function(doctype, docname) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat",
|
method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat",
|
||||||
|
|||||||
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -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": []
|
||||||
|
}
|
||||||
163
erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
Normal file
163
erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
Normal file
@ -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
|
||||||
@ -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
|
||||||
@ -102,3 +102,5 @@ frappe.query_reports["Stock Balance"] = {
|
|||||||
return value;
|
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
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
import erpnext
|
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.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
|
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)
|
_func = itemgetter(1)
|
||||||
|
|
||||||
to_date = filters.get("to_date")
|
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):
|
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_level = 0
|
||||||
item_reorder_qty = 0
|
item_reorder_qty = 0
|
||||||
if item + warehouse in item_reorder_detail_map:
|
if item + warehouse in item_reorder_detail_map:
|
||||||
@ -135,6 +141,21 @@ def get_columns(filters: StockBalanceFilter):
|
|||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
"width": 100,
|
"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"),
|
"label": _("Stock UOM"),
|
||||||
"fieldname": "stock_uom",
|
"fieldname": "stock_uom",
|
||||||
@ -216,6 +237,7 @@ def get_columns(filters: StockBalanceFilter):
|
|||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if filters.get("show_stock_ageing_data"):
|
if filters.get("show_stock_ageing_data"):
|
||||||
columns += [
|
columns += [
|
||||||
@ -296,11 +318,23 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L
|
|||||||
.orderby(sle.actual_qty)
|
.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:
|
if items:
|
||||||
query = query.where(sle.item_code.isin(items))
|
query = query.where(sle.item_code.isin(items))
|
||||||
|
|
||||||
query = apply_conditions(query, filters)
|
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]):
|
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
|
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||||
|
|
||||||
|
inventory_dimensions = get_inventory_dimension_fields()
|
||||||
|
|
||||||
for d in sle:
|
for d in sle:
|
||||||
key = (d.company, d.item_code, d.warehouse)
|
group_by_key = get_group_by_key(d, inventory_dimensions)
|
||||||
if key not in iwb_map:
|
if group_by_key not in iwb_map:
|
||||||
iwb_map[key] = frappe._dict(
|
iwb_map[group_by_key] = frappe._dict(
|
||||||
{
|
{
|
||||||
"opening_qty": 0.0,
|
"opening_qty": 0.0,
|
||||||
"opening_val": 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:
|
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
|
||||||
qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty)
|
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_qty += qty_diff
|
||||||
qty_dict.bal_val += value_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
|
return iwb_map
|
||||||
|
|
||||||
|
|
||||||
def filter_items_with_no_transactions(iwb_map, float_precision: float):
|
def get_group_by_key(row, inventory_dimension_fields) -> tuple:
|
||||||
for (company, item, warehouse) in sorted(iwb_map):
|
group_by_key = [row.company, row.item_code, row.warehouse]
|
||||||
qty_dict = iwb_map[(company, item, 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
|
no_transactions = True
|
||||||
for key, val in qty_dict.items():
|
for key, val in qty_dict.items():
|
||||||
|
if key in inventory_dimensions:
|
||||||
|
continue
|
||||||
|
|
||||||
val = flt(val, float_precision)
|
val = flt(val, float_precision)
|
||||||
qty_dict[key] = val
|
qty_dict[key] = val
|
||||||
if key != "val_rate" and val:
|
if key != "val_rate" and val:
|
||||||
no_transactions = False
|
no_transactions = False
|
||||||
|
|
||||||
if no_transactions:
|
if no_transactions:
|
||||||
iwb_map.pop((company, item, warehouse))
|
iwb_map.pop(group_by_key)
|
||||||
|
|
||||||
return iwb_map
|
return iwb_map
|
||||||
|
|
||||||
|
|||||||
@ -95,4 +95,6 @@ frappe.query_reports["Stock Ledger"] = {
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
erpnext.utils.add_inventory_dimensions('Stock Ledger', 10);
|
||||||
@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt
|
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.serial_no.serial_no import get_serial_nos
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
|
||||||
from erpnext.stock.utils import (
|
from erpnext.stock.utils import (
|
||||||
@ -17,7 +18,7 @@ from erpnext.stock.utils import (
|
|||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
is_reposting_item_valuation_in_progress()
|
is_reposting_item_valuation_in_progress()
|
||||||
include_uom = filters.get("include_uom")
|
include_uom = filters.get("include_uom")
|
||||||
columns = get_columns()
|
columns = get_columns(filters)
|
||||||
items = get_items(filters)
|
items = get_items(filters)
|
||||||
sl_entries = get_stock_ledger_entries(filters, items)
|
sl_entries = get_stock_ledger_entries(filters, items)
|
||||||
item_details = get_item_details(items, sl_entries, include_uom)
|
item_details = get_item_details(items, sl_entries, include_uom)
|
||||||
@ -33,12 +34,14 @@ def execute(filters=None):
|
|||||||
actual_qty = stock_value = 0
|
actual_qty = stock_value = 0
|
||||||
|
|
||||||
available_serial_nos = {}
|
available_serial_nos = {}
|
||||||
|
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
|
||||||
|
|
||||||
for sle in sl_entries:
|
for sle in sl_entries:
|
||||||
item_detail = item_details[sle.item_code]
|
item_detail = item_details[sle.item_code]
|
||||||
|
|
||||||
sle.update(item_detail)
|
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)
|
actual_qty += flt(sle.actual_qty, precision)
|
||||||
stock_value += sle.stock_value_difference
|
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)
|
sle.balance_serial_no = "\n".join(existing_serial_no)
|
||||||
|
|
||||||
|
|
||||||
def get_columns():
|
def get_columns(filters):
|
||||||
columns = [
|
columns = [
|
||||||
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150},
|
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150},
|
||||||
{
|
{
|
||||||
@ -216,14 +219,28 @@ def get_columns():
|
|||||||
"options": "Project",
|
"options": "Project",
|
||||||
"width": 100,
|
"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"),
|
"label": _("Company"),
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"width": 110,
|
"width": 110,
|
||||||
},
|
}
|
||||||
]
|
)
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
@ -252,7 +269,7 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
serial_no,
|
serial_no,
|
||||||
company,
|
company,
|
||||||
project,
|
project,
|
||||||
stock_value_difference
|
stock_value_difference {get_dimension_fields}
|
||||||
FROM
|
FROM
|
||||||
`tabStock Ledger Entry` sle
|
`tabStock Ledger Entry` sle
|
||||||
WHERE
|
WHERE
|
||||||
@ -263,7 +280,9 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
posting_date asc, posting_time asc, creation asc
|
posting_date asc, posting_time asc, creation asc
|
||||||
""".format(
|
""".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,
|
filters,
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
@ -272,6 +291,15 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
return sl_entries
|
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):
|
def get_items(filters):
|
||||||
conditions = []
|
conditions = []
|
||||||
if filters.get("item_code"):
|
if filters.get("item_code"):
|
||||||
@ -341,6 +369,10 @@ def get_sle_conditions(filters):
|
|||||||
if filters.get("project"):
|
if filters.get("project"):
|
||||||
conditions.append("project=%(project)s")
|
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 ""
|
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||||
|
|
||||||
|
|
||||||
@ -401,3 +433,11 @@ def get_item_group_condition(item_group):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return ""
|
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