mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-17 10:36:31 +00:00
Merge branch 'develop' into fix/letterhead-footer-print-formats
This commit is contained in:
@@ -21,9 +21,6 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
)
|
||||
|
||||
target_doc.quotation_to = "Customer"
|
||||
target_doc.run_method("set_missing_values")
|
||||
target_doc.run_method("set_other_charges")
|
||||
target_doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
price_list, currency = frappe.db.get_value(
|
||||
"Customer", {"name": source_name}, ["default_price_list", "default_currency"]
|
||||
@@ -33,6 +30,10 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
target_doc.run_method("set_missing_values")
|
||||
target_doc.run_method("set_other_charges")
|
||||
target_doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
||||
@@ -14,12 +14,53 @@ from erpnext.selling.doctype.customer.customer import (
|
||||
get_customer_outstanding,
|
||||
)
|
||||
from erpnext.selling.doctype.customer.mapper import (
|
||||
make_quotation,
|
||||
parse_full_name,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCustomer(ERPNextTestSuite):
|
||||
def test_quotation_from_customer_uses_actual_exchange_rate(self):
|
||||
company = "_Test Company"
|
||||
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
foreign_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
|
||||
frappe.defaults.set_user_default("company", company)
|
||||
self.addCleanup(frappe.defaults.clear_user_default, "company")
|
||||
|
||||
# Seed a deterministic rate so the test does not depend on the live exchange-rate API.
|
||||
rate = 83.0
|
||||
exchange = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Currency Exchange",
|
||||
"date": nowdate(),
|
||||
"from_currency": foreign_currency,
|
||||
"to_currency": company_currency,
|
||||
"exchange_rate": rate,
|
||||
"for_selling": 1,
|
||||
"for_buying": 1,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
self.addCleanup(frappe.delete_doc, "Currency Exchange", exchange.name, force=1)
|
||||
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "_Test Customer FX Quotation",
|
||||
"customer_type": "Company",
|
||||
"default_currency": foreign_currency,
|
||||
}
|
||||
).insert()
|
||||
self.addCleanup(frappe.delete_doc, "Customer", customer.name, force=1)
|
||||
|
||||
quotation = make_quotation(customer.name)
|
||||
|
||||
self.assertEqual(quotation.currency, foreign_currency)
|
||||
self.assertNotEqual(flt(quotation.conversion_rate), 1.0)
|
||||
self.assertNotEqual(flt(quotation.conversion_rate), 0.0)
|
||||
self.assertEqual(flt(quotation.conversion_rate), rate)
|
||||
|
||||
def test_get_customer_name_dedupes_with_numeric_suffix(self):
|
||||
# When a customer name already exists, get_customer_name appends "- <max suffix + 1>". The
|
||||
# Postgres branch extracts the suffix with regexp_replace/NULLIF/CAST (pypika's Substring cannot
|
||||
|
||||
@@ -228,7 +228,7 @@ def _make_customer(source_name, ignore_permissions=False):
|
||||
|
||||
|
||||
def create_customer_from_lead(lead_name, ignore_permissions=False):
|
||||
from erpnext.crm.doctype.lead.lead import _make_customer
|
||||
from erpnext.crm.doctype.lead.mapper import _make_customer
|
||||
|
||||
customer = _make_customer(lead_name, ignore_permissions=ignore_permissions)
|
||||
customer.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.selling.report.customer_wise_item_price.customer_wise_item_price import execute
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
PRICE_LIST = "Standard Selling"
|
||||
|
||||
|
||||
class TestCustomerWiseItemPrice(ERPNextTestSuite):
|
||||
"""The report lists sales items with the selling rate from the customer's price
|
||||
list and the available stock (summed across warehouses)."""
|
||||
|
||||
def setUp(self):
|
||||
self.item = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name
|
||||
self.customer = self.create_customer()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"item_code": self.item,
|
||||
"price_list": PRICE_LIST,
|
||||
"selling": 1,
|
||||
"price_list_rate": 250,
|
||||
}
|
||||
).insert()
|
||||
make_stock_entry(item_code=self.item, to_warehouse="Stores - _TC", qty=10, rate=100)
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test CWIP Customer"
|
||||
if not frappe.db.exists("Customer", name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": name,
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
"default_price_list": PRICE_LIST,
|
||||
}
|
||||
).insert()
|
||||
return name
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"customer": self.customer})
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_customer_filter_is_mandatory(self):
|
||||
self.assertRaises(frappe.ValidationError, execute, frappe._dict({}))
|
||||
|
||||
def test_selling_rate_and_available_stock_for_item(self):
|
||||
rows = self.run_report(item=self.item)
|
||||
|
||||
row = next((r for r in rows if r["item_code"] == self.item), None)
|
||||
self.assertIsNotNone(row, "Sales item missing from report")
|
||||
self.assertEqual(row["item_name"], frappe.db.get_value("Item", self.item, "item_name"))
|
||||
self.assertEqual(row["selling_rate"], 250) # from the customer's price list
|
||||
self.assertEqual(row["available_stock"], 10) # stocked into Stores - _TC
|
||||
self.assertEqual(row["price_list"], PRICE_LIST)
|
||||
|
||||
def test_item_filter_scopes_to_single_item(self):
|
||||
other = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name
|
||||
|
||||
item_codes = {r["item_code"] for r in self.run_report(item=self.item)}
|
||||
self.assertIn(self.item, item_codes)
|
||||
self.assertNotIn(other, item_codes)
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
create_dn_against_so,
|
||||
make_sales_order,
|
||||
)
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestItemWiseSalesHistory(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 so_row(self, so_name, **extra):
|
||||
data = self.run_report(**extra)[1]
|
||||
return next(row for row in data if row["sales_order"] == so_name)
|
||||
|
||||
def test_sales_order_line_shown_with_values(self):
|
||||
so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01")
|
||||
|
||||
row = self.so_row(so.name)
|
||||
self.assertEqual(row["item_code"], "_Test Item")
|
||||
self.assertEqual(row["quantity"], 10)
|
||||
self.assertEqual(row["rate"], 100)
|
||||
self.assertEqual(row["amount"], 1000)
|
||||
self.assertEqual(row["customer"], "_Test Customer")
|
||||
|
||||
def test_draft_sales_order_excluded(self):
|
||||
so = make_sales_order(transaction_date="2026-06-01", do_not_submit=True)
|
||||
|
||||
names = {row["sales_order"] for row in self.run_report()[1]}
|
||||
self.assertNotIn(so.name, names)
|
||||
|
||||
def test_date_range_filters_on_transaction_date(self):
|
||||
so = make_sales_order(transaction_date="2026-06-01")
|
||||
|
||||
in_range = {
|
||||
row["sales_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]
|
||||
}
|
||||
self.assertIn(so.name, in_range)
|
||||
|
||||
out_of_range = {
|
||||
row["sales_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1]
|
||||
}
|
||||
self.assertNotIn(so.name, out_of_range)
|
||||
|
||||
def test_item_code_filter(self):
|
||||
so = make_sales_order(
|
||||
transaction_date="2026-06-01",
|
||||
item_list=[
|
||||
{"item_code": "_Test Item", "qty": 5, "rate": 100, "warehouse": "_Test Warehouse - _TC"},
|
||||
{"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"},
|
||||
],
|
||||
)
|
||||
|
||||
item_codes = {row["item_code"] for row in self.run_report(item_code="_Test Item 2")[1]}
|
||||
self.assertEqual(item_codes, {"_Test Item 2"})
|
||||
# the filtered-out line of the same order must not leak in
|
||||
self.assertTrue(
|
||||
all(row["sales_order"] == so.name for row in self.run_report(item_code="_Test Item 2")[1])
|
||||
)
|
||||
|
||||
def test_customer_filter(self):
|
||||
make_sales_order(customer="_Test Customer 1", transaction_date="2026-06-01")
|
||||
make_sales_order(customer="_Test Customer 2", transaction_date="2026-06-01")
|
||||
|
||||
customers = {row["customer"] for row in self.run_report(customer="_Test Customer 1")[1]}
|
||||
self.assertEqual(customers, {"_Test Customer 1"})
|
||||
|
||||
def test_delivered_quantity_reflects_delivery(self):
|
||||
so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01")
|
||||
create_dn_against_so(so.name, delivered_qty=4)
|
||||
|
||||
self.assertEqual(self.so_row(so.name)["delivered_quantity"], 4)
|
||||
|
||||
def test_billed_amount_reflects_invoice(self):
|
||||
so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01")
|
||||
si = make_sales_invoice(so.name)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
self.assertEqual(self.so_row(so.name)["billed_amount"], 1000)
|
||||
|
||||
def test_amounts_reported_in_company_currency(self):
|
||||
# a USD order must report rate/amount converted to the company's currency (base_* fields)
|
||||
so = make_sales_order(
|
||||
do_not_save=True,
|
||||
currency="USD",
|
||||
qty=10,
|
||||
rate=100,
|
||||
transaction_date="2026-06-01",
|
||||
)
|
||||
so.conversion_rate = 80
|
||||
so.insert()
|
||||
so.submit()
|
||||
|
||||
row = self.so_row(so.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):
|
||||
make_sales_order(item_code="_Test Item", qty=2, rate=100, transaction_date="2026-06-01")
|
||||
make_sales_order(item_code="_Test Item", qty=3, rate=100, 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*100 + 3*100 aggregated for the item
|
||||
self.assertEqual(values[labels.index("_Test Item")], 500)
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
|
||||
from erpnext.selling.report.quotation_trends.quotation_trends import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
FISCAL_YEAR = "_Test Fiscal Year 2026"
|
||||
TXN_DATE = "2026-06-01"
|
||||
|
||||
|
||||
class TestQuotationTrends(ERPNextTestSuite):
|
||||
"""The trends report buckets submitted Quotation quantities/amounts by period
|
||||
(Yearly/Monthly) for the chosen `based_on` dimension (Item, Customer, ...)."""
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": FISCAL_YEAR,
|
||||
"based_on": "Item",
|
||||
"period": "Yearly",
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
result = execute(filters)
|
||||
columns, data = result[0], result[1]
|
||||
labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns]
|
||||
return labels, data
|
||||
|
||||
def _cell(self, data, key_label, key_value, col_label, labels):
|
||||
"""Value at column `col_label` for the row whose `key_label` column equals
|
||||
`key_value`, or 0 when that row doesn't exist yet."""
|
||||
key_idx = labels.index(key_label)
|
||||
col_idx = labels.index(col_label)
|
||||
for row in data:
|
||||
if row[key_idx] == key_value:
|
||||
return row[col_idx] or 0
|
||||
return 0
|
||||
|
||||
def test_yearly_item_amount_and_total(self):
|
||||
# Yearly period => a single "<FY> (Qty)"/"(Amt)" bucket plus Total(Qty)/Total(Amt).
|
||||
labels, before = self.run_report()
|
||||
qty_col = f"{FISCAL_YEAR} (Qty)"
|
||||
amt_col = f"{FISCAL_YEAR} (Amt)"
|
||||
before_qty = self._cell(before, "Item", "_Test Item", qty_col, labels)
|
||||
before_amt = self._cell(before, "Item", "_Test Item", amt_col, labels)
|
||||
before_tot_qty = self._cell(before, "Item", "_Test Item", "Total(Qty)", labels)
|
||||
before_tot_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels)
|
||||
|
||||
make_quotation(item="_Test Item", qty=4, rate=200, transaction_date=TXN_DATE)
|
||||
|
||||
labels, after = self.run_report()
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", qty_col, labels) - before_qty, 4)
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", amt_col, labels) - before_amt, 800)
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Qty)", labels) - before_tot_qty, 4)
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot_amt, 800)
|
||||
|
||||
def test_monthly_lands_in_june_bucket(self):
|
||||
# Monthly period => one bucket per month; a 2026-06-01 quotation hits "Jun (Qty)"/"(Amt)".
|
||||
labels, before = self.run_report(period="Monthly")
|
||||
before_jun_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels)
|
||||
before_jun_amt = self._cell(before, "Item", "_Test Item", "Jun (Amt)", labels)
|
||||
before_may_qty = self._cell(before, "Item", "_Test Item", "May (Qty)", labels)
|
||||
|
||||
make_quotation(item="_Test Item", qty=3, rate=100, transaction_date=TXN_DATE)
|
||||
|
||||
labels, after = self.run_report(period="Monthly")
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_jun_qty, 3)
|
||||
# the amount path is a separate SUM(base_net_amount) case, so assert it too
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Amt)", labels) - before_jun_amt, 300)
|
||||
# nothing was quoted in May, so that bucket is unchanged
|
||||
self.assertEqual(self._cell(after, "Item", "_Test Item", "May (Qty)", labels) - before_may_qty, 0)
|
||||
|
||||
def test_based_on_customer_groups_amount_by_party(self):
|
||||
# based_on Customer keys rows on the "Party" column (the customer id)
|
||||
labels, before = self.run_report(based_on="Customer")
|
||||
amt_col = f"{FISCAL_YEAR} (Amt)"
|
||||
before_amt = self._cell(before, "Party", "_Test Customer", amt_col, labels)
|
||||
|
||||
make_quotation(
|
||||
party_name="_Test Customer", item="_Test Item", qty=2, rate=150, transaction_date=TXN_DATE
|
||||
)
|
||||
|
||||
labels, after = self.run_report(based_on="Customer")
|
||||
self.assertEqual(self._cell(after, "Party", "_Test Customer", amt_col, labels) - before_amt, 300)
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.report.sales_person_commission_summary.sales_person_commission_summary import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSalesPersonCommissionSummary(ERPNextTestSuite):
|
||||
"""The report joins a sales document (Sales Invoice/Order/Delivery Note) with its
|
||||
Sales Team rows, listing each sales person's contribution and commission."""
|
||||
|
||||
def setUp(self):
|
||||
# reuse the bootstrap sales persons (under the "Sales Team" group)
|
||||
self.sales_person = "_Test Sales Person"
|
||||
|
||||
def make_invoice_with_commission(self, percentage=100, commission_rate=5, incentives=50):
|
||||
si = create_sales_invoice(rate=1000, qty=1, do_not_save=True, posting_date="2026-06-01")
|
||||
si.append(
|
||||
"sales_team",
|
||||
{
|
||||
"sales_person": self.sales_person,
|
||||
"allocated_percentage": percentage,
|
||||
"commission_rate": commission_rate,
|
||||
"incentives": incentives,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
si.submit()
|
||||
si.reload() # reflect any values recomputed on submit
|
||||
return si
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doc_type": "Sales Invoice",
|
||||
"sales_person": self.sales_person,
|
||||
# scope to this test's posting date so the query isn't unbounded over
|
||||
# every invoice for the shared sales person
|
||||
"from_date": "2026-06-01",
|
||||
"to_date": "2026-06-01",
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_doc_type_is_mandatory(self):
|
||||
self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"}))
|
||||
|
||||
def test_commission_row_matches_sales_team_entry(self):
|
||||
si = self.make_invoice_with_commission(percentage=100, commission_rate=5, incentives=50)
|
||||
team = si.sales_team[0]
|
||||
|
||||
rows = self.run_report()
|
||||
row = next((r for r in rows if r[0] == si.name), None)
|
||||
self.assertIsNotNone(row, "Invoice with commission missing from report")
|
||||
|
||||
# row: name, customer, territory, posting_date, base_net_amount, sales_person,
|
||||
# allocated_percentage, commission_rate, allocated_amount, incentives
|
||||
self.assertEqual(row[1], si.customer)
|
||||
self.assertEqual(row[4], si.base_net_total)
|
||||
self.assertEqual(row[5], self.sales_person)
|
||||
self.assertEqual(row[6], team.allocated_percentage)
|
||||
self.assertEqual(row[7], team.commission_rate)
|
||||
self.assertEqual(row[8], team.allocated_amount)
|
||||
self.assertEqual(row[9], team.incentives)
|
||||
|
||||
def test_appends_total_row(self):
|
||||
self.make_invoice_with_commission()
|
||||
rows = self.run_report()
|
||||
# the report appends a blank total row after one or more real data rows
|
||||
self.assertGreaterEqual(len(rows), 2)
|
||||
self.assertTrue(any(r[0] for r in rows[:-1]), "expected real data rows before the total row")
|
||||
self.assertEqual(rows[-1], [""] * len(rows[0]))
|
||||
|
||||
def test_sales_person_filter_scopes_rows(self):
|
||||
si = self.make_invoice_with_commission()
|
||||
|
||||
filtered = self.run_report(sales_person="_Test Sales Person 1")
|
||||
self.assertNotIn(si.name, {r[0] for r in filtered if r[0]})
|
||||
@@ -183,8 +183,22 @@ def get_entries(filters):
|
||||
.as_("contribution_amt")
|
||||
)
|
||||
|
||||
# Only pass valid document-field filters to get_query; report-specific keys such as
|
||||
# doc_type / sales_person / item_group are handled separately below.
|
||||
doc_filters = {"docstatus": 1}
|
||||
for field in ["company", "customer", "territory"]:
|
||||
if filters.get(field):
|
||||
doc_filters[field] = filters.get(field)
|
||||
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
doc_filters[date_field] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
||||
elif filters.get("from_date"):
|
||||
doc_filters[date_field] = [">=", filters.get("from_date")]
|
||||
elif filters.get("to_date"):
|
||||
doc_filters[date_field] = ["<=", filters.get("to_date")]
|
||||
|
||||
query = (
|
||||
frappe.get_query(dt, filters=filters, ignore_permissions=False)
|
||||
frappe.get_query(dt, filters=doc_filters, ignore_permissions=False)
|
||||
.join(dt_item)
|
||||
.on(dt.name == dt_item.parent)
|
||||
.join(st)
|
||||
@@ -203,48 +217,29 @@ def get_entries(filters):
|
||||
contribution_amt_case,
|
||||
)
|
||||
.where(st.parenttype == doc_type)
|
||||
.where(dt.docstatus == 1)
|
||||
)
|
||||
|
||||
if filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"])
|
||||
sp = frappe.qb.DocType("Sales Person")
|
||||
query = query.where(
|
||||
st.sales_person.isin(frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt)))
|
||||
)
|
||||
|
||||
# only resolve items when an item_group/brand filter is set; otherwise get_items
|
||||
# would return every item in the system and add a huge IN() clause on each run
|
||||
if filters.get("item_group") or filters.get("brand"):
|
||||
items = get_items(filters)
|
||||
if not items:
|
||||
# the item_group/brand filter matched nothing -> no rows
|
||||
return []
|
||||
query = query.where(dt_item.item_code.isin([d[0] for d in items]))
|
||||
|
||||
query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, date_field):
|
||||
conditions = [""]
|
||||
values = []
|
||||
|
||||
for field in ["company", "customer", "territory"]:
|
||||
if filters.get(field):
|
||||
conditions.append(f"dt.{field}=%s")
|
||||
values.append(filters[field])
|
||||
|
||||
if filters.get("sales_person"):
|
||||
lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"])
|
||||
conditions.append(
|
||||
f"exists(select name from `tabSales Person` where lft >= {lft} and rgt <= {rgt} and name=st.sales_person)"
|
||||
)
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append(f"dt.{date_field}>=%s")
|
||||
values.append(filters["from_date"])
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append(f"dt.{date_field}<=%s")
|
||||
values.append(filters["to_date"])
|
||||
|
||||
items = get_items(filters)
|
||||
if items:
|
||||
conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items)))
|
||||
values += items
|
||||
else:
|
||||
# return empty result, if no items are fetched after filtering on 'item group' and 'brand'
|
||||
conditions.append("dt_item.item_code = Null")
|
||||
|
||||
return " and ".join(conditions), values
|
||||
|
||||
|
||||
def get_items(filters):
|
||||
item = qb.DocType("Item")
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.report.sales_person_wise_transaction_summary.sales_person_wise_transaction_summary import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSalesPersonWiseTransactionSummary(ERPNextTestSuite):
|
||||
"""Item-level summary joining a sales document with its Sales Team rows, showing
|
||||
each sales person's contributed qty and amount per item line."""
|
||||
|
||||
def setUp(self):
|
||||
self.sales_person = "_Test Sales Person"
|
||||
|
||||
def make_invoice_with_commission(self, qty=5, rate=200, percentage=100):
|
||||
si = create_sales_invoice(
|
||||
item="_Test Item", qty=qty, rate=rate, do_not_save=True, posting_date="2026-06-01"
|
||||
)
|
||||
si.append("sales_team", {"sales_person": self.sales_person, "allocated_percentage": percentage})
|
||||
si.insert()
|
||||
si.submit()
|
||||
return si
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{"company": "_Test Company", "doc_type": "Sales Invoice", "sales_person": self.sales_person}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_doc_type_is_mandatory(self):
|
||||
self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"}))
|
||||
|
||||
def test_invalid_doc_type_throws(self):
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
execute,
|
||||
frappe._dict({"company": "_Test Company", "doc_type": "Purchase Invoice"}),
|
||||
)
|
||||
|
||||
def test_item_line_contribution(self):
|
||||
si = self.make_invoice_with_commission(qty=5, rate=200, percentage=100)
|
||||
item = si.items[0]
|
||||
|
||||
rows = self.run_report()
|
||||
row = next((r for r in rows if r[0] == si.name and r[5] == "_Test Item"), None)
|
||||
self.assertIsNotNone(row, "Invoice item line missing from report")
|
||||
|
||||
# row: name, customer, territory, warehouse, posting_date, item_code, item_group,
|
||||
# brand, stock_qty, base_net_amount, sales_person, allocated_percentage,
|
||||
# contributed_qty, contribution_amt, currency
|
||||
self.assertEqual(row[1], si.customer)
|
||||
self.assertEqual(row[8], item.stock_qty)
|
||||
self.assertEqual(row[9], item.base_net_amount)
|
||||
self.assertEqual(row[10], self.sales_person)
|
||||
self.assertEqual(row[11], 100)
|
||||
self.assertEqual(row[12], item.stock_qty * 100 / 100) # contributed qty
|
||||
self.assertEqual(row[13], item.base_net_amount * 100 / 100) # contribution amount
|
||||
|
||||
def test_appends_total_row(self):
|
||||
self.make_invoice_with_commission()
|
||||
rows = self.run_report()
|
||||
self.assertTrue(rows)
|
||||
self.assertEqual(rows[-1], [""] * len(rows[0]))
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
|
||||
create_target_distribution,
|
||||
)
|
||||
from erpnext.selling.report.territory_target_variance_based_on_item_group.territory_target_variance_based_on_item_group import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTerritoryTargetVarianceBasedOnItemGroup(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
def test_achieved_target_and_variance(self):
|
||||
distribution = create_target_distribution(self.fiscal_year)
|
||||
territory = create_territory_with_target(
|
||||
"_Test Target Territory", self.fiscal_year, distribution.name, target_qty=50
|
||||
)
|
||||
|
||||
# a Sales Order in that territory contributes to the achieved quantity
|
||||
so = make_sales_order(rate=1000, qty=20, do_not_submit=True)
|
||||
so.territory = territory.name
|
||||
so.submit()
|
||||
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"doctype": "Sales Order",
|
||||
"period": "Yearly",
|
||||
"target_on": "Quantity",
|
||||
}
|
||||
)
|
||||
)[1]
|
||||
|
||||
# no item_group is set on the target, so the report emits exactly one row per
|
||||
# territory -- assert all three figures against that single row
|
||||
rows = [frappe._dict(r) for r in result if r.get("territory") == territory.name]
|
||||
self.assertEqual(len(rows), 1, "expected exactly one row for the target territory")
|
||||
row = rows[0]
|
||||
self.assertEqual(flt(row.total_target, 2), 50)
|
||||
self.assertEqual(flt(row.total_achieved, 2), 20)
|
||||
self.assertEqual(flt(row.total_variance, 2), -30)
|
||||
|
||||
|
||||
def create_territory_with_target(name, fiscal_year, distribution_id, target_qty=50):
|
||||
doc = frappe.new_doc("Territory")
|
||||
doc.territory_name = name
|
||||
doc.parent_territory = "All Territories"
|
||||
doc.is_group = 0
|
||||
doc.append(
|
||||
"targets",
|
||||
{
|
||||
"fiscal_year": fiscal_year,
|
||||
"target_qty": target_qty,
|
||||
"target_amount": 30000,
|
||||
"distribution_id": distribution_id,
|
||||
},
|
||||
)
|
||||
return doc.insert()
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
|
||||
from erpnext.selling.report.territory_wise_sales.territory_wise_sales import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
TERRITORY = "_Test Territory"
|
||||
|
||||
|
||||
class TestTerritoryWiseSales(ERPNextTestSuite):
|
||||
"""The report walks the Opportunity -> Quotation -> Sales Order -> Sales Invoice
|
||||
funnel and totals each stage's amount per territory.
|
||||
|
||||
These tests cover the Opportunity and Quotation stages; the Sales Order and
|
||||
Sales Invoice (order_amount / billing_amount) stages are not yet exercised."""
|
||||
|
||||
def make_opportunity(self, amount=5000):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Opportunity",
|
||||
"opportunity_from": "Customer",
|
||||
"party_name": "_Test Customer",
|
||||
"territory": TERRITORY,
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"opportunity_amount": amount,
|
||||
"transaction_date": "2026-06-01",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def make_quotation_for(self, opportunity, qty, rate):
|
||||
qo = make_quotation(item="_Test Item", qty=qty, rate=rate, do_not_save=True)
|
||||
qo.opportunity = opportunity.name
|
||||
qo.insert()
|
||||
qo.submit()
|
||||
return qo
|
||||
|
||||
def amount_for(self, territory, field):
|
||||
for row in execute(frappe._dict({"company": "_Test Company"}))[1]:
|
||||
if row["territory"] == territory:
|
||||
return row[field]
|
||||
return 0
|
||||
|
||||
def test_opportunity_amount_grouped_by_territory(self):
|
||||
before = self.amount_for(TERRITORY, "opportunity_amount")
|
||||
opp = self.make_opportunity(5000)
|
||||
self.assertEqual(opp.territory, TERRITORY)
|
||||
|
||||
after = self.amount_for(TERRITORY, "opportunity_amount")
|
||||
self.assertEqual(after - before, 5000)
|
||||
|
||||
def test_quotation_amount_flows_from_opportunity(self):
|
||||
before = self.amount_for(TERRITORY, "quotation_amount")
|
||||
|
||||
opp = self.make_opportunity()
|
||||
quotation = self.make_quotation_for(opp, qty=2, rate=500)
|
||||
|
||||
after = self.amount_for(TERRITORY, "quotation_amount")
|
||||
self.assertEqual(after - before, quotation.base_grand_total)
|
||||
@@ -622,9 +622,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-19 13:01:26.893303",
|
||||
"modified": "2026-06-14 13:44:07.820564",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"module_onboarding": "Selling Onboarding",
|
||||
"name": "Selling",
|
||||
"number_cards": [
|
||||
{
|
||||
@@ -648,6 +649,762 @@
|
||||
"roles": [],
|
||||
"sequence_id": 6.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Selling",
|
||||
"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": "Selling",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "receipt-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quotation",
|
||||
"link_to": "Quotation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "sell",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Order",
|
||||
"link_to": "Sales Order",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "receipt",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Invoice",
|
||||
"link_to": "Sales Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "computer",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "POS",
|
||||
"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": "POS",
|
||||
"link_to": "point-of-sale",
|
||||
"link_type": "Page",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Profile",
|
||||
"link_to": "POS Profile",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Invoice",
|
||||
"link_to": "POS Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Opening Entry",
|
||||
"link_to": "POS Opening Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Closing Entry",
|
||||
"link_to": "POS Closing Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Invoice Merge Log",
|
||||
"link_to": "POS Invoice Merge Log",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "POS Settings",
|
||||
"link_to": "POS Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Loyalty Program",
|
||||
"link_to": "Loyalty Program",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Loyalty Point Entry",
|
||||
"link_to": "Loyalty Point Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "stock",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Items & Pricing",
|
||||
"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": "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": "Item Group",
|
||||
"link_to": "Item Group",
|
||||
"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": "Item Price",
|
||||
"link_to": "Item Price",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Pricing Rule",
|
||||
"link_to": "Pricing Rule",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Promotional Scheme",
|
||||
"link_to": "Promotional Scheme",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Coupon Code",
|
||||
"link_to": "Coupon Code",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Blanket Order",
|
||||
"link_to": "Blanket Order",
|
||||
"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": "Customer",
|
||||
"link_to": "Customer",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer Group",
|
||||
"link_to": "Customer Group",
|
||||
"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": "Contact",
|
||||
"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": "Territory",
|
||||
"link_to": "Territory",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Campaign",
|
||||
"link_to": "Campaign",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Person",
|
||||
"link_to": "Sales Person",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Partner",
|
||||
"link_to": "Sales Partner",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Monthly Distribution",
|
||||
"link_to": "Monthly Distribution",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Terms Template",
|
||||
"link_to": "Terms and Conditions",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Template",
|
||||
"link_to": "Sales Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Product Bundle",
|
||||
"link_to": "Product Bundle",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "UTM Source",
|
||||
"link_to": "UTM Source",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Shipping Rule",
|
||||
"link_to": "Shipping Rule",
|
||||
"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": "Sales Register",
|
||||
"link_to": "Sales Register",
|
||||
"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 Sales Register",
|
||||
"link_to": "Item-wise Sales Register",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Analytics",
|
||||
"link_to": "Sales Analytics",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer Addresses And Contacts",
|
||||
"link_to": "Address And Contacts",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Inactive Customers",
|
||||
"link_to": "Inactive Customers",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Invoice Trends",
|
||||
"link_to": "Sales 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": "Customer Credit Balance",
|
||||
"link_to": "Customer Credit Balance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customers Without Any Sales Transactions",
|
||||
"link_to": "Customers Without Any Sales Transactions",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Partners Commission",
|
||||
"link_to": "Sales Partners Commission",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Available Stock for Packing Items",
|
||||
"link_to": "Available Stock for Packing Items",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Territory Target Variance Based On Item Group",
|
||||
"link_to": "Territory Target Variance Based On Item Group",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Person Target Variance Based On Item Group",
|
||||
"link_to": "Sales Person Target Variance Based On Item Group",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Partner Target Variance Based On Item Group",
|
||||
"link_to": "Sales Partner Target Variance based on Item Group",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Pending SO Items For Purchase Request",
|
||||
"link_to": "Pending SO Items For Purchase Request",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Funnel",
|
||||
"link_to": "sales-funnel",
|
||||
"link_type": "Page",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Order Analysis",
|
||||
"link_to": "Sales 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": "Customer Acquisition and Loyalty",
|
||||
"link_to": "Customer Acquisition and Loyalty",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quotation Trends",
|
||||
"link_to": "Quotation Trends",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Order Trends",
|
||||
"link_to": "Sales 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": "Item-wise Sales History",
|
||||
"link_to": "Item-wise Sales History",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Person-wise Transaction Summary",
|
||||
"link_to": "Sales Person-wise Transaction Summary",
|
||||
"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": "Selling Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Selling",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user