Merge branch 'develop' into fix/letterhead-footer-print-formats

This commit is contained in:
Khushi Rawat
2026-07-03 02:27:58 +05:30
committed by GitHub
175 changed files with 15525 additions and 985 deletions

View File

@@ -0,0 +1,130 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.buying.report.item_wise_purchase_history.item_wise_purchase_history import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestItemWisePurchaseHistory(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"company": "_Test Company",
"from_date": "2026-01-01",
"to_date": "2026-12-31",
**extra,
}
)
return execute(filters)
def po_row(self, po_name, **extra):
data = self.run_report(**extra)[1]
return next(row for row in data if row["purchase_order"] == po_name)
def test_purchase_order_line_shown_with_values(self):
po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01")
row = self.po_row(po.name)
self.assertEqual(row["item_code"], "_Test Item")
self.assertEqual(row["quantity"], 10)
self.assertEqual(row["rate"], 500)
self.assertEqual(row["amount"], 5000)
self.assertEqual(row["supplier"], "_Test Supplier")
def test_draft_purchase_order_excluded(self):
po = create_purchase_order(transaction_date="2026-06-01", do_not_submit=True)
names = {row["purchase_order"] for row in self.run_report()[1]}
self.assertNotIn(po.name, names)
def test_date_range_filters_on_transaction_date(self):
po = create_purchase_order(transaction_date="2026-06-01")
in_range = {
row["purchase_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]
}
self.assertIn(po.name, in_range)
out_of_range = {
row["purchase_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1]
}
self.assertNotIn(po.name, out_of_range)
def test_item_code_filter(self):
po = create_purchase_order(
transaction_date="2026-06-01",
rm_items=[
{"item_code": "_Test Item", "qty": 5, "rate": 500, "warehouse": "_Test Warehouse - _TC"},
{"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"},
],
)
rows = self.run_report(item_code="_Test Item 2")[1]
self.assertEqual({row["item_code"] for row in rows}, {"_Test Item 2"})
# the filtered-out line of the same order must not leak in
self.assertTrue(all(row["purchase_order"] == po.name for row in rows))
def test_item_group_filter(self):
# _Test Item is in _Test Item Group; _Test FG Item is in _Test Item Group Desktops
po_test_group = create_purchase_order(item_code="_Test Item", transaction_date="2026-06-01")
po_other_group = create_purchase_order(item_code="_Test FG Item", transaction_date="2026-06-01")
names = {row["purchase_order"] for row in self.run_report(item_group="_Test Item Group")[1]}
self.assertIn(po_test_group.name, names)
self.assertNotIn(po_other_group.name, names)
def test_supplier_filter(self):
create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01")
create_purchase_order(supplier="_Test Supplier 1", transaction_date="2026-06-01")
suppliers = {row["supplier"] for row in self.run_report(supplier="_Test Supplier")[1]}
self.assertEqual(suppliers, {"_Test Supplier"})
def test_received_quantity_reflects_receipt(self):
po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01")
create_pr_against_po(po.name, received_qty=4)
self.assertEqual(self.po_row(po.name)["received_qty"], 4)
def test_billed_amount_reflects_invoice(self):
po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01")
pi = make_purchase_invoice(po.name)
pi.insert()
pi.submit()
self.assertEqual(self.po_row(po.name)["billed_amt"], 5000)
def test_amounts_reported_in_company_currency(self):
# a USD order must report rate/amount converted to the company's currency (base_* fields)
po = create_purchase_order(
do_not_save=True,
currency="USD",
qty=10,
rate=100,
transaction_date="2026-06-01",
)
po.conversion_rate = 80
po.insert()
po.submit()
row = self.po_row(po.name)
self.assertEqual(row["rate"], 8000) # 100 USD * 80
self.assertEqual(row["amount"], 80000) # 10 * 100 USD * 80
def test_chart_aggregates_amount_per_item(self):
create_purchase_order(item_code="_Test Item", qty=2, rate=500, transaction_date="2026-06-01")
create_purchase_order(item_code="_Test Item", qty=3, rate=500, transaction_date="2026-06-01")
chart = self.run_report(item_code="_Test Item")[3]
labels = chart["data"]["labels"]
values = chart["data"]["datasets"][0]["values"]
self.assertIn("_Test Item", labels)
# 2*500 + 3*500 aggregated for the item
self.assertEqual(values[labels.index("_Test Item")], 2500)

View File

@@ -0,0 +1,93 @@
# 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_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
)

View File

@@ -0,0 +1,49 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.buying.report.subcontract_order_summary.subcontract_order_summary import execute
from erpnext.controllers.tests.test_subcontracting_controller import (
get_subcontracting_order,
make_bom_for_subcontracted_items,
make_raw_materials,
make_service_items,
make_subcontracted_items,
)
from erpnext.tests.utils import ERPNextTestSuite
FG_ITEM = "Subcontracted Item SA7"
class TestSubcontractOrderSummary(ERPNextTestSuite):
"""The report lists Subcontracting Order finished items with their ordered and
received quantities within the transaction-date window."""
def setUp(self):
make_subcontracted_items()
make_raw_materials()
make_service_items()
make_bom_for_subcontracted_items()
def run_report(self, **extra):
filters = frappe._dict(
{"company": "_Test Company", "from_date": add_days(today(), -1), "to_date": add_days(today(), 1)}
)
filters.update(extra)
return execute(filters)[1]
def test_subcontracting_order_is_listed(self):
sco = get_subcontracting_order()
rows = [r for r in self.run_report(name=sco.name) if r.get("item_code") == FG_ITEM]
self.assertTrue(rows, "Subcontracting Order finished item missing from report")
self.assertEqual(rows[0]["qty"], 10)
self.assertEqual(rows[0]["received_qty"], 0) # nothing received yet
def test_out_of_range_date_excludes_order(self):
sco = get_subcontracting_order()
data = self.run_report(name=sco.name, from_date="2019-01-01", to_date="2019-01-31")
self.assertEqual(data, [])

View File

@@ -0,0 +1,66 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison import execute
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
ITEM = "_Test Item"
class TestSupplierQuotationComparison(ERPNextTestSuite):
"""The report lists Supplier Quotation item lines so quotes for the same item can
be compared across suppliers."""
def make_quotation(self, supplier, qty, rate, uom=None):
item = {"item_code": ITEM, "qty": qty, "rate": rate, "warehouse": "_Test Warehouse - _TC"}
if uom:
item["uom"] = uom
sq = frappe.get_doc(
{
"doctype": "Supplier Quotation",
"supplier": supplier,
"company": COMPANY,
"currency": "INR",
"transaction_date": "2026-06-01",
"items": [item],
}
)
sq.insert()
sq.submit()
return sq
def run_report(self, **extra):
filters = frappe._dict({"company": COMPANY, "from_date": "2026-01-01", "to_date": "2026-12-31"})
filters.update(extra)
return execute(filters)[1]
def test_no_filters_returns_empty(self):
self.assertEqual(execute(None)[1], [])
def test_quotation_line_listed_with_price(self):
# _Test UOM 1 converts at 10 stock units per qty, so price_per_unit
# (amount / stock_qty) diverges from base_rate and the division path is tested
sq = self.make_quotation("_Test Supplier", qty=10, rate=100, uom="_Test UOM 1")
rows = [r for r in self.run_report(item_code=ITEM) if r.get("quotation") == sq.name]
self.assertTrue(rows, "Supplier Quotation line missing from report")
row = rows[0]
self.assertEqual(row["supplier_name"], "_Test Supplier")
self.assertEqual(row["qty"], 10)
self.assertEqual(row["base_rate"], 100)
self.assertEqual(row["base_amount"], 1000)
# 1000 amount / (10 qty * 10 conversion) = 10, distinct from the 100 base_rate
self.assertEqual(row["price_per_unit"], 10)
def test_compares_multiple_suppliers_for_item(self):
sq1 = self.make_quotation("_Test Supplier", qty=10, rate=100)
sq2 = self.make_quotation("_Test Supplier 1", qty=10, rate=120)
quotes = {r["quotation"]: r for r in self.run_report(item_code=ITEM)}
self.assertIn(sq1.name, quotes)
self.assertIn(sq2.name, quotes)
self.assertEqual(quotes[sq1.name]["base_rate"], 100)
self.assertEqual(quotes[sq2.name]["base_rate"], 120)

View File

@@ -341,17 +341,6 @@
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Item Wise Consumption",
"link_count": 0,
"link_to": "Item Wise Consumption",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
@@ -512,9 +501,10 @@
"type": "Link"
}
],
"modified": "2026-01-02 14:55:59.078773",
"modified": "2026-06-14 13:43:50.509039",
"modified_by": "Administrator",
"module": "Buying",
"module_onboarding": "Buying Onboarding",
"name": "Buying",
"number_cards": [
{
@@ -538,6 +528,403 @@
"roles": [],
"sequence_id": 5.0,
"shortcuts": [],
"sidebar_items": [
{
"child": 0,
"collapsible": 1,
"icon": "home",
"indent": 0,
"keep_closed": 0,
"label": "Home",
"link_to": "Buying",
"link_type": "Workspace",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "chart",
"indent": 0,
"keep_closed": 0,
"label": "Dashboard",
"link_to": "Buying",
"link_type": "Dashboard",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "notepad-text",
"indent": 0,
"keep_closed": 0,
"label": "Material Request",
"link_to": "Material Request",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "git-pull-request-arrow",
"indent": 0,
"keep_closed": 0,
"label": "Request for Quotation",
"link_to": "Request for Quotation",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "book-open-text",
"indent": 0,
"keep_closed": 0,
"label": "Supplier Quotation",
"link_to": "Supplier Quotation",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "receipt-text",
"indent": 0,
"keep_closed": 0,
"label": "Purchase Order",
"link_to": "Purchase Order",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "liabilities",
"indent": 0,
"keep_closed": 0,
"label": "Purchase Invoice",
"link_to": "Purchase Invoice",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "database",
"indent": 1,
"keep_closed": 1,
"label": "Setup",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Supplier",
"link_to": "Supplier",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Group",
"link_to": "Supplier Group",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Item",
"link_to": "Item",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Price List",
"link_to": "Price List",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Address",
"link_to": "Address",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Contacts",
"link_to": "Contact",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Scorecard",
"link_to": "Supplier Scorecard",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Criteria",
"link_to": "Supplier Scorecard Criteria",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Variable",
"link_to": "Supplier Scorecard Variable",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Standing",
"link_to": "Supplier Scorecard Standing",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "sheet",
"indent": 1,
"keep_closed": 1,
"label": "Reports",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Purchase Analytics",
"link_to": "Purchase Analytics",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Purchase Order Analysis",
"link_to": "Purchase Order Analysis",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Requested Items to Order and Receive",
"link_to": "Requested Items to Order and Receive",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Items To Be Requested",
"link_to": "Items To Be Requested",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Item-wise Purchase History",
"link_to": "Item-wise Purchase History",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Purchase Receipt Trends ",
"link_to": "Purchase Receipt Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Purchase Invoice Trends",
"link_to": "Purchase Invoice Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Purchase Order Trends",
"link_to": "Purchase Order Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Procurement Tracker",
"link_to": "Procurement Tracker",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Item Wise Consumption",
"link_to": "Item Wise Consumption",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Quotation Comparison",
"link_to": "Supplier Quotation Comparison",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier Addresses And Contacts",
"link_to": "Address And Contacts",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "settings",
"indent": 0,
"keep_closed": 0,
"label": "Settings",
"link_to": "Buying Settings",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
}
],
"standard": 1,
"title": "Buying",
"type": "Workspace"
}