mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 23:39:28 +00:00
feat(analytics): filter sales and purchase analytics by entity (#58402)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 3f29cdf8d2)
# Conflicts:
# erpnext/buying/report/purchase_analytics/test_purchase_analytics.py
# erpnext/selling/report/sales_analytics/test_sales_analytics.py
This commit is contained in:
committed by
Mergify
parent
2cd865fffc
commit
655ed81575
@@ -10,6 +10,26 @@ frappe.query_reports["Purchase Analytics"] = {
|
|||||||
options: ["Supplier Group", "Supplier", "Item Group", "Item"],
|
options: ["Supplier Group", "Supplier", "Item Group", "Item"],
|
||||||
default: "Supplier",
|
default: "Supplier",
|
||||||
reqd: 1,
|
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",
|
fieldname: "doc_type",
|
||||||
@@ -65,6 +85,19 @@ frappe.query_reports["Purchase Analytics"] = {
|
|||||||
default: "Monthly",
|
default: "Monthly",
|
||||||
reqd: 1,
|
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",
|
fieldname: "show_aggregate_value_from_subsidiary_companies",
|
||||||
label: __("Show Aggregate Value from Subsidiary Companies"),
|
label: __("Show Aggregate Value from Subsidiary Companies"),
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
from erpnext.buying.report.purchase_analytics.purchase_analytics import execute
|
||||||
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
COMPANY = "_Test Company"
|
||||||
|
SUPPLIER = "_Test Supplier"
|
||||||
|
SUPPLIER_GROUP = "_Test Supplier Group"
|
||||||
|
# A historical window that ordinary test fixtures don't post into.
|
||||||
|
FROM_DATE = "2019-04-01"
|
||||||
|
TO_DATE = "2019-06-30"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPurchaseAnalytics(ERPNextTestSuite):
|
||||||
|
"""purchase_analytics reuses the shared Analytics engine; these tests lock its
|
||||||
|
wiring (doc_type=Purchase Order) across the Supplier Group / Item Group trees."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def _filters(self, **overrides):
|
||||||
|
filters = {
|
||||||
|
"doc_type": "Purchase Order",
|
||||||
|
"value_quantity": "Value",
|
||||||
|
"range": "Monthly",
|
||||||
|
"company": COMPANY,
|
||||||
|
"from_date": FROM_DATE,
|
||||||
|
"to_date": TO_DATE,
|
||||||
|
}
|
||||||
|
filters.update(overrides)
|
||||||
|
return frappe._dict(filters)
|
||||||
|
|
||||||
|
def _rows(self, filters):
|
||||||
|
return {row["entity"]: row for row in execute(filters)[1]}
|
||||||
|
|
||||||
|
def make_po(self, qty=4, rate=250):
|
||||||
|
return create_purchase_order(
|
||||||
|
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)
|
||||||
|
base_group = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0))
|
||||||
|
|
||||||
|
po = self.make_po(qty=4, rate=250)
|
||||||
|
rows = self._rows(filters)
|
||||||
|
|
||||||
|
# supplier is remapped to its group; the root sits at indent 0
|
||||||
|
self.assertIn(SUPPLIER_GROUP, rows)
|
||||||
|
self.assertIn("All Supplier Groups", rows)
|
||||||
|
self.assertNotIn(SUPPLIER, rows)
|
||||||
|
self.assertEqual(rows["All Supplier Groups"]["indent"], 0)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_group, flt(po.base_net_total), places=2)
|
||||||
|
self.assertGreaterEqual(flt(rows["All Supplier Groups"]["total"]), flt(po.base_net_total))
|
||||||
|
|
||||||
|
def test_item_group_tree_rolls_up_to_root(self):
|
||||||
|
item_group = frappe.db.get_value("Item", "_Test Item", "item_group")
|
||||||
|
filters = self._filters(tree_type="Item Group")
|
||||||
|
base = self._rows(filters)
|
||||||
|
base_group = flt(base.get(item_group, {}).get("total", 0.0))
|
||||||
|
|
||||||
|
po = self.make_po(qty=4, rate=250)
|
||||||
|
rows = self._rows(filters)
|
||||||
|
|
||||||
|
self.assertIn(item_group, rows)
|
||||||
|
self.assertIn("All Item Groups", rows)
|
||||||
|
# the raw item code must not leak as its own entity; the root sits at indent 0
|
||||||
|
self.assertNotIn("_Test Item", rows)
|
||||||
|
self.assertEqual(rows["All Item Groups"]["indent"], 0)
|
||||||
|
self.assertAlmostEqual(rows[item_group]["total"] - base_group, flt(po.base_net_total), places=2)
|
||||||
|
self.assertGreaterEqual(flt(rows["All Item Groups"]["total"]), flt(po.base_net_total))
|
||||||
|
|
||||||
|
def test_supplier_group_by_quantity(self):
|
||||||
|
filters = self._filters(tree_type="Supplier Group", value_quantity="Quantity")
|
||||||
|
base = self._rows(filters)
|
||||||
|
base_qty = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0))
|
||||||
|
base_root_qty = flt(base.get("All Supplier Groups", {}).get("total", 0.0))
|
||||||
|
|
||||||
|
po = self.make_po(qty=7, rate=100)
|
||||||
|
rows = self._rows(filters)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_qty, flt(po.total_qty), places=2)
|
||||||
|
# the quantity must roll up to the root too, not just the leaf group
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
rows["All Supplier Groups"]["total"] - base_root_qty, flt(po.total_qty), places=2
|
||||||
|
)
|
||||||
@@ -2,6 +2,18 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.query_reports["Sales Analytics"] = {
|
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: [
|
filters: [
|
||||||
{
|
{
|
||||||
fieldname: "tree_type",
|
fieldname: "tree_type",
|
||||||
@@ -18,6 +30,21 @@ frappe.query_reports["Sales Analytics"] = {
|
|||||||
],
|
],
|
||||||
default: "Customer",
|
default: "Customer",
|
||||||
reqd: 1,
|
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",
|
fieldname: "doc_type",
|
||||||
@@ -34,6 +61,10 @@ frappe.query_reports["Sales Analytics"] = {
|
|||||||
],
|
],
|
||||||
default: "Sales Invoice",
|
default: "Sales Invoice",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
on_change: function () {
|
||||||
|
frappe.query_reports["Sales Analytics"].reset_entity_filter();
|
||||||
|
frappe.query_report.refresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "value_quantity",
|
fieldname: "value_quantity",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def append_report(dt, org, new):
|
|||||||
class Analytics:
|
class Analytics:
|
||||||
def __init__(self, filters=None):
|
def __init__(self, filters=None):
|
||||||
self.filters = frappe._dict(filters or {})
|
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":
|
if self.filters.doc_type == "Payment Entry" and self.filters.value_quantity == "Quantity":
|
||||||
frappe.throw(_("Only Value available for Payment Entry"))
|
frappe.throw(_("Only Value available for Payment Entry"))
|
||||||
self.date_field = (
|
self.date_field = (
|
||||||
@@ -102,6 +103,7 @@ class Analytics:
|
|||||||
self.update_company_list_for_parent_company()
|
self.update_company_list_for_parent_company()
|
||||||
self.get_columns()
|
self.get_columns()
|
||||||
self.get_data()
|
self.get_data()
|
||||||
|
self.filter_data_by_entities()
|
||||||
self.get_chart_data()
|
self.get_chart_data()
|
||||||
|
|
||||||
# Skipping total row for tree-view reports
|
# Skipping total row for tree-view reports
|
||||||
@@ -395,6 +397,23 @@ class Analytics:
|
|||||||
ignore_permissions=False,
|
ignore_permissions=False,
|
||||||
).run(as_dict=True)
|
).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):
|
def get_rows(self):
|
||||||
self.data = []
|
self.data = []
|
||||||
self.get_periodic_data()
|
self.get_periodic_data()
|
||||||
|
|||||||
236
erpnext/selling/report/sales_analytics/test_sales_analytics.py
Normal file
236
erpnext/selling/report/sales_analytics/test_sales_analytics.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
from erpnext.selling.report.sales_analytics.sales_analytics import execute
|
||||||
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
# Bootstrap masters reused as-is (see erpnext/tests/utils.py):
|
||||||
|
# "_Test Customer" -> customer_group "_Test Customer Group", territory "_Test Territory"
|
||||||
|
# "_Test Supplier" -> supplier_group "_Test Supplier Group" (child of "All Supplier Groups")
|
||||||
|
# Sales Order.order_type defaults to "Sales" (reqd Select field)
|
||||||
|
COMPANY = "_Test Company"
|
||||||
|
CUSTOMER = "_Test Customer"
|
||||||
|
CUSTOMER_GROUP = "_Test Customer Group"
|
||||||
|
TERRITORY = "_Test Territory"
|
||||||
|
SUPPLIER = "_Test Supplier"
|
||||||
|
SUPPLIER_GROUP = "_Test Supplier Group"
|
||||||
|
FROM_DATE = "2019-04-01"
|
||||||
|
TO_DATE = "2019-06-30"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSalesAnalytics(ERPNextTestSuite):
|
||||||
|
def setUp(self):
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
# Two submitted Sales Orders for the bootstrap customer inside the report window.
|
||||||
|
# These roll up into the tree roots the converted tree/order-type queries build.
|
||||||
|
self.orders = [
|
||||||
|
make_sales_order(
|
||||||
|
company=COMPANY,
|
||||||
|
customer=CUSTOMER,
|
||||||
|
qty=5,
|
||||||
|
rate=100,
|
||||||
|
transaction_date="2019-04-10",
|
||||||
|
),
|
||||||
|
make_sales_order(
|
||||||
|
company=COMPANY,
|
||||||
|
customer=CUSTOMER,
|
||||||
|
qty=3,
|
||||||
|
rate=100,
|
||||||
|
transaction_date="2019-05-15",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _base_filters(self, **overrides):
|
||||||
|
filters = {
|
||||||
|
"doc_type": "Sales Order",
|
||||||
|
"value_quantity": "Value",
|
||||||
|
"range": "Monthly",
|
||||||
|
"company": COMPANY,
|
||||||
|
"from_date": FROM_DATE,
|
||||||
|
"to_date": TO_DATE,
|
||||||
|
}
|
||||||
|
filters.update(overrides)
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def _expected_value_total(self):
|
||||||
|
return sum(flt(so.base_net_total) for so in self.orders)
|
||||||
|
|
||||||
|
def _expected_qty_total(self):
|
||||||
|
return sum(flt(so.total_qty) for so in self.orders)
|
||||||
|
|
||||||
|
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."""
|
||||||
|
columns, data, *_ = execute(self._base_filters(tree_type="Customer Group"))
|
||||||
|
|
||||||
|
self.assertTrue(columns)
|
||||||
|
self.assertTrue(data)
|
||||||
|
|
||||||
|
rows = self._row_by_entity(data)
|
||||||
|
# The whole tree is returned, so both the root and the customer's own group appear.
|
||||||
|
self.assertIn("All Customer Groups", rows)
|
||||||
|
self.assertIn(CUSTOMER_GROUP, rows)
|
||||||
|
|
||||||
|
expected = self._expected_value_total()
|
||||||
|
self.assertGreater(expected, 0)
|
||||||
|
# Leaf group holds the orders; root receives the same total via roll-up.
|
||||||
|
self.assertAlmostEqual(rows[CUSTOMER_GROUP]["total"], expected, places=2)
|
||||||
|
self.assertAlmostEqual(rows["All Customer Groups"]["total"], expected, places=2)
|
||||||
|
# Roots of a tree report sit at indent 0.
|
||||||
|
self.assertEqual(rows["All Customer Groups"]["indent"], 0)
|
||||||
|
|
||||||
|
def test_territory_tree_rolls_up_to_root(self):
|
||||||
|
"""tree_type='Territory' exercises the same tree path against the Territory tree."""
|
||||||
|
columns, data, *_ = execute(self._base_filters(tree_type="Territory"))
|
||||||
|
|
||||||
|
self.assertTrue(columns)
|
||||||
|
rows = self._row_by_entity(data)
|
||||||
|
self.assertIn("All Territories", rows)
|
||||||
|
self.assertIn(TERRITORY, rows)
|
||||||
|
|
||||||
|
expected = self._expected_value_total()
|
||||||
|
self.assertAlmostEqual(rows[TERRITORY]["total"], expected, places=2)
|
||||||
|
self.assertAlmostEqual(rows["All Territories"]["total"], expected, places=2)
|
||||||
|
|
||||||
|
def test_order_type_synthetic_tree(self):
|
||||||
|
"""tree_type='Order Type' drives get_teams: distinct order_type rebuilt in Python
|
||||||
|
under a synthetic 'Order Types' root, then rolled up via get_rows_by_group."""
|
||||||
|
columns, data, *_ = execute(self._base_filters(tree_type="Order Type"))
|
||||||
|
|
||||||
|
self.assertTrue(columns)
|
||||||
|
rows = self._row_by_entity(data)
|
||||||
|
# Synthetic root plus the default order_type the bootstrap Sales Orders carry.
|
||||||
|
self.assertIn("Order Types", rows)
|
||||||
|
self.assertIn("Sales", rows)
|
||||||
|
self.assertEqual(rows["Order Types"]["indent"], 0)
|
||||||
|
|
||||||
|
expected = self._expected_value_total()
|
||||||
|
self.assertAlmostEqual(rows["Sales"]["total"], expected, places=2)
|
||||||
|
self.assertAlmostEqual(rows["Order Types"]["total"], expected, places=2)
|
||||||
|
|
||||||
|
def test_order_type_leaf_rows_in_sorted_order(self):
|
||||||
|
"""get_teams fetches distinct order_types; frappe drops the SQL ORDER BY for distinct queries on
|
||||||
|
postgres, so the report sorts the order-type rows in python (key=str.casefold) to keep them in a
|
||||||
|
deterministic, case-insensitive order identical on both engines."""
|
||||||
|
for order_type in ("Shopping Cart", "Maintenance", "Sales"): # created out of sorted order
|
||||||
|
so = make_sales_order(
|
||||||
|
company=COMPANY,
|
||||||
|
customer=CUSTOMER,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
transaction_date="2019-04-12",
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
so.order_type = order_type
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
columns, data, *_ = execute(self._base_filters(tree_type="Order Type"))
|
||||||
|
|
||||||
|
mine = {"Sales", "Maintenance", "Shopping Cart"}
|
||||||
|
leaves = [row["entity"] for row in data if row.get("entity") in mine]
|
||||||
|
# the order-type rows must appear in casefold-sorted order on both engines
|
||||||
|
self.assertEqual(leaves, sorted(leaves, key=str.casefold))
|
||||||
|
self.assertEqual(set(leaves), mine)
|
||||||
|
|
||||||
|
def test_customer_group_by_quantity(self):
|
||||||
|
"""value_quantity='Quantity' switches the selected value column (total_qty)."""
|
||||||
|
_columns, data, *_ = execute(
|
||||||
|
self._base_filters(tree_type="Customer Group", value_quantity="Quantity")
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = self._row_by_entity(data)
|
||||||
|
self.assertIn(CUSTOMER_GROUP, rows)
|
||||||
|
|
||||||
|
expected_qty = self._expected_qty_total()
|
||||||
|
self.assertGreater(expected_qty, 0)
|
||||||
|
self.assertAlmostEqual(rows[CUSTOMER_GROUP]["total"], expected_qty, places=2)
|
||||||
|
self.assertAlmostEqual(rows["All Customer Groups"]["total"], expected_qty, places=2)
|
||||||
|
|
||||||
|
def test_supplier_group_tree_maps_supplier_to_group(self):
|
||||||
|
"""tree_type='Supplier Group' (doc_type='Purchase Order') exercises
|
||||||
|
get_supplier_parent_child_map: the query selects 'supplier' as entity, then
|
||||||
|
get_periodic_data remaps each supplier to its group via the parent->child map
|
||||||
|
built by frappe.get_all('Supplier', ['name', 'supplier_group'], as_list=True).
|
||||||
|
The group total then rolls up into the 'All Supplier Groups' root."""
|
||||||
|
# Baseline the report before adding our Purchase Order so the assertion is
|
||||||
|
# robust to any pre-existing rows in the historical window.
|
||||||
|
base_filters = self._base_filters(tree_type="Supplier Group", doc_type="Purchase Order")
|
||||||
|
_columns, base_data, *_ = execute(base_filters)
|
||||||
|
base_rows = self._row_by_entity(base_data)
|
||||||
|
base_group_total = flt(base_rows.get(SUPPLIER_GROUP, {}).get("total", 0.0))
|
||||||
|
|
||||||
|
po = create_purchase_order(
|
||||||
|
company=COMPANY,
|
||||||
|
supplier=SUPPLIER,
|
||||||
|
qty=4,
|
||||||
|
rate=250,
|
||||||
|
transaction_date="2019-04-10",
|
||||||
|
)
|
||||||
|
po_value = flt(po.base_net_total)
|
||||||
|
self.assertGreater(po_value, 0)
|
||||||
|
|
||||||
|
columns, data, *_ = execute(base_filters)
|
||||||
|
|
||||||
|
self.assertTrue(columns)
|
||||||
|
self.assertTrue(data)
|
||||||
|
|
||||||
|
rows = self._row_by_entity(data)
|
||||||
|
# The supplier was remapped to its group; both the leaf group and the tree
|
||||||
|
# root appear as entities (no raw supplier name leaks into the output).
|
||||||
|
self.assertIn(SUPPLIER_GROUP, rows)
|
||||||
|
self.assertIn("All Supplier Groups", rows)
|
||||||
|
self.assertNotIn(SUPPLIER, rows)
|
||||||
|
# Roots of a tree report sit at indent 0.
|
||||||
|
self.assertEqual(rows["All Supplier Groups"]["indent"], 0)
|
||||||
|
|
||||||
|
# The new PO lands in the supplier's group via the parent->child map.
|
||||||
|
self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_group_total, po_value, places=2)
|
||||||
|
# Roll-up: the root aggregates every group, so it covers at least this PO.
|
||||||
|
self.assertGreaterEqual(flt(rows["All Supplier Groups"]["total"]), po_value)
|
||||||
Reference in New Issue
Block a user