mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-27 05:45:19 +00:00
feat(analytics): filter sales and purchase analytics by entity (#58402)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ccc19f2554
commit
3f29cdf8d2
@@ -10,6 +10,26 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
options: ["Supplier Group", "Supplier", "Item Group", "Item"],
|
||||
default: "Supplier",
|
||||
reqd: 1,
|
||||
on_change: function () {
|
||||
const entity_filter = frappe.query_report.get_filter("entity");
|
||||
if (entity_filter) {
|
||||
entity_filter.df.label = __(frappe.query_report.get_filter_value("tree_type"));
|
||||
entity_filter.set_value([]);
|
||||
entity_filter.refresh();
|
||||
}
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "entity",
|
||||
label: __("Entity"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
const tree_type = frappe.query_report.get_filter_value("tree_type");
|
||||
if (!tree_type || tree_type === "Order Type") return [];
|
||||
return frappe.db.get_link_options(tree_type, txt);
|
||||
},
|
||||
depends_on: "eval:doc.tree_type != 'Order Type'",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
@@ -65,6 +85,19 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
default: "Monthly",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "curves",
|
||||
label: __("Curves"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "select", label: __("Select") },
|
||||
{ value: "all", label: __("All") },
|
||||
{ value: "non-zeros", label: __("Non-Zeros") },
|
||||
{ value: "total", label: __("Total Only") },
|
||||
],
|
||||
default: "select",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_aggregate_value_from_subsidiary_companies",
|
||||
label: __("Show Aggregate Value from Subsidiary Companies"),
|
||||
|
||||
@@ -43,6 +43,44 @@ class TestPurchaseAnalytics(ERPNextTestSuite):
|
||||
company=COMPANY, supplier=SUPPLIER, qty=qty, rate=rate, transaction_date="2019-04-10"
|
||||
)
|
||||
|
||||
def test_supplier_entity_filter(self):
|
||||
filters = self._filters(tree_type="Supplier", entity=[SUPPLIER], curves="all")
|
||||
base_total = flt(self._rows(filters).get(SUPPLIER, {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po()
|
||||
columns, data, _message, chart, *_rest = execute(filters)
|
||||
|
||||
self.assertTrue(columns)
|
||||
self.assertEqual({row["entity"] for row in data}, {SUPPLIER})
|
||||
self.assertAlmostEqual(data[0]["total"] - base_total, flt(po.base_net_total), places=2)
|
||||
|
||||
supplier_name = frappe.db.get_value("Supplier", SUPPLIER, "supplier_name")
|
||||
self.assertEqual({dataset["name"] for dataset in chart["data"]["datasets"]}, {supplier_name})
|
||||
|
||||
def test_parent_supplier_group_filter_preserves_rollup(self):
|
||||
self.make_po()
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
unfiltered = self._rows(filters)
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=["All Supplier Groups"]))
|
||||
|
||||
self.assertEqual(set(filtered), {"All Supplier Groups"})
|
||||
self.assertAlmostEqual(
|
||||
filtered["All Supplier Groups"]["total"],
|
||||
unfiltered["All Supplier Groups"]["total"],
|
||||
places=2,
|
||||
)
|
||||
|
||||
def test_supplier_group_entity_filter(self):
|
||||
self.make_po()
|
||||
unfiltered = self._rows(self._filters(tree_type="Supplier Group"))
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=[SUPPLIER_GROUP]))
|
||||
|
||||
self.assertEqual(set(filtered), {SUPPLIER_GROUP})
|
||||
self.assertEqual(filtered[SUPPLIER_GROUP]["indent"], 0)
|
||||
self.assertAlmostEqual(
|
||||
filtered[SUPPLIER_GROUP]["total"], unfiltered[SUPPLIER_GROUP]["total"], places=2
|
||||
)
|
||||
|
||||
def test_supplier_group_tree_rolls_up_to_root(self):
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
base = self._rows(filters)
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Sales Analytics"] = {
|
||||
// "All" reports on every doctype at once and forces the tree to Customer
|
||||
entity_tree_type() {
|
||||
const doc_type = frappe.query_report.get_filter_value("doc_type");
|
||||
return doc_type === "All" ? "Customer" : frappe.query_report.get_filter_value("tree_type");
|
||||
},
|
||||
reset_entity_filter() {
|
||||
const entity_filter = frappe.query_report.get_filter("entity");
|
||||
if (!entity_filter) return;
|
||||
entity_filter.df.label = __(this.entity_tree_type());
|
||||
entity_filter.set_value([]);
|
||||
entity_filter.refresh();
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
fieldname: "tree_type",
|
||||
@@ -18,6 +30,21 @@ frappe.query_reports["Sales Analytics"] = {
|
||||
],
|
||||
default: "Customer",
|
||||
reqd: 1,
|
||||
on_change: function () {
|
||||
frappe.query_reports["Sales Analytics"].reset_entity_filter();
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "entity",
|
||||
label: __("Entity"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
const tree_type = frappe.query_reports["Sales Analytics"].entity_tree_type();
|
||||
if (!tree_type || tree_type === "Order Type") return [];
|
||||
return frappe.db.get_link_options(tree_type, txt);
|
||||
},
|
||||
depends_on: "eval:doc.tree_type != 'Order Type'",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
@@ -34,6 +61,10 @@ frappe.query_reports["Sales Analytics"] = {
|
||||
],
|
||||
default: "Sales Invoice",
|
||||
reqd: 1,
|
||||
on_change: function () {
|
||||
frappe.query_reports["Sales Analytics"].reset_entity_filter();
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "value_quantity",
|
||||
|
||||
@@ -53,6 +53,7 @@ def append_report(dt, org, new):
|
||||
class Analytics:
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
self.entities = self.filters.get("entity") or []
|
||||
if self.filters.doc_type == "Payment Entry" and self.filters.value_quantity == "Quantity":
|
||||
frappe.throw(_("Only Value available for Payment Entry"))
|
||||
self.date_field = (
|
||||
@@ -102,6 +103,7 @@ class Analytics:
|
||||
self.update_company_list_for_parent_company()
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
self.filter_data_by_entities()
|
||||
self.get_chart_data()
|
||||
|
||||
# Skipping total row for tree-view reports
|
||||
@@ -395,6 +397,23 @@ class Analytics:
|
||||
ignore_permissions=False,
|
||||
).run(as_dict=True)
|
||||
|
||||
def filter_data_by_entities(self):
|
||||
if not self.entities:
|
||||
return
|
||||
|
||||
entities = set(self.entities)
|
||||
selected_data = []
|
||||
for row in self.data:
|
||||
if row["entity"] not in entities:
|
||||
continue
|
||||
|
||||
row = row.copy()
|
||||
if "indent" in row:
|
||||
row["indent"] = 0
|
||||
selected_data.append(row)
|
||||
|
||||
self.data = selected_data
|
||||
|
||||
def get_rows(self):
|
||||
self.data = []
|
||||
self.get_periodic_data()
|
||||
|
||||
@@ -67,6 +67,44 @@ class TestSalesAnalytics(ERPNextTestSuite):
|
||||
def _row_by_entity(self, data):
|
||||
return {row["entity"]: row for row in data}
|
||||
|
||||
def test_customer_entity_filter(self):
|
||||
_columns, data, _message, chart, *_rest = execute(
|
||||
self._base_filters(tree_type="Customer", entity=[CUSTOMER], curves="all")
|
||||
)
|
||||
|
||||
self.assertEqual({row["entity"] for row in data}, {CUSTOMER})
|
||||
self.assertAlmostEqual(data[0]["total"], self._expected_value_total(), places=2)
|
||||
self.assertEqual({dataset["name"] for dataset in chart["data"]["datasets"]}, {CUSTOMER})
|
||||
|
||||
def test_parent_customer_group_filter_preserves_rollup(self):
|
||||
_columns, unfiltered_data, *_rest = execute(self._base_filters(tree_type="Customer Group"))
|
||||
_columns, filtered_data, *_rest = execute(
|
||||
self._base_filters(tree_type="Customer Group", entity=["All Customer Groups"])
|
||||
)
|
||||
|
||||
unfiltered = self._row_by_entity(unfiltered_data)
|
||||
filtered = self._row_by_entity(filtered_data)
|
||||
self.assertEqual(set(filtered), {"All Customer Groups"})
|
||||
self.assertAlmostEqual(
|
||||
filtered["All Customer Groups"]["total"],
|
||||
unfiltered["All Customer Groups"]["total"],
|
||||
places=2,
|
||||
)
|
||||
|
||||
def test_customer_group_entity_filter(self):
|
||||
_columns, unfiltered_data, *_rest = execute(self._base_filters(tree_type="Customer Group"))
|
||||
_columns, filtered_data, *_rest = execute(
|
||||
self._base_filters(tree_type="Customer Group", entity=[CUSTOMER_GROUP])
|
||||
)
|
||||
|
||||
unfiltered = self._row_by_entity(unfiltered_data)
|
||||
filtered = self._row_by_entity(filtered_data)
|
||||
self.assertEqual(set(filtered), {CUSTOMER_GROUP})
|
||||
self.assertEqual(filtered[CUSTOMER_GROUP]["indent"], 0)
|
||||
self.assertAlmostEqual(
|
||||
filtered[CUSTOMER_GROUP]["total"], unfiltered[CUSTOMER_GROUP]["total"], places=2
|
||||
)
|
||||
|
||||
def test_customer_group_tree_rolls_up_to_root(self):
|
||||
"""tree_type='Customer Group' drives get_groups (tree get_all ordered by lft)
|
||||
and get_rows_by_group, rolling child values up to the 'All Customer Groups' root."""
|
||||
|
||||
Reference in New Issue
Block a user