mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-03 21:50:53 +00:00
Compare commits
155 Commits
mergify/co
...
l10n_devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abff82a4b2 | ||
|
|
3a63c74382 | ||
|
|
6bd2f29ab5 | ||
|
|
e3e3d97a72 | ||
|
|
42c6768b4c | ||
|
|
2e13691ffa | ||
|
|
ae80d29dcf | ||
|
|
a33f7ead24 | ||
|
|
817ecaa92f | ||
|
|
8fd98ccbe2 | ||
|
|
bc82197bd1 | ||
|
|
9c53a91b82 | ||
|
|
21a9f2754e | ||
|
|
f0434cadd4 | ||
|
|
0ba43a17c1 | ||
|
|
4feaacc649 | ||
|
|
7034dc71e7 | ||
|
|
ed72732bb2 | ||
|
|
b12725c4b2 | ||
|
|
20928bd600 | ||
|
|
15862566a8 | ||
|
|
e446f54f2e | ||
|
|
cf9f16a921 | ||
|
|
6cffa0faeb | ||
|
|
a075437db7 | ||
|
|
9f4914e08f | ||
|
|
c9d712fa49 | ||
|
|
3345336a5c | ||
|
|
ceadc4f269 | ||
|
|
caa4358057 | ||
|
|
36f56fa1c3 | ||
|
|
5b738b7b0d | ||
|
|
f85f6be3cf | ||
|
|
6591ae195d | ||
|
|
7229957107 | ||
|
|
50b6f50b88 | ||
|
|
0888405640 | ||
|
|
087fb29d51 | ||
|
|
2bab709ac4 | ||
|
|
5298438905 | ||
|
|
e0b0926dff | ||
|
|
a3d22f4a51 | ||
|
|
3f9b8fe37e | ||
|
|
820f5498e7 | ||
|
|
71f02d412a | ||
|
|
f9ac05f4a1 | ||
|
|
c7fed29569 | ||
|
|
5514c64b7c | ||
|
|
2a8d26c0a7 | ||
|
|
56e7690e64 | ||
|
|
6e57bd325f | ||
|
|
8d70385019 | ||
|
|
f95baa54de | ||
|
|
3092c920ff | ||
|
|
55646667be | ||
|
|
9865f63613 | ||
|
|
08876ae07a | ||
|
|
489a799bc4 | ||
|
|
0790d2e6df | ||
|
|
04b94ed61f | ||
|
|
334b8ab09a | ||
|
|
7592f568ae | ||
|
|
bd21f506a1 | ||
|
|
2eec826219 | ||
|
|
4716084a41 | ||
|
|
81e838c4f8 | ||
|
|
c8eebd3a96 | ||
|
|
b6bdf81ce8 | ||
|
|
64db8072d8 | ||
|
|
cabdb7417d | ||
|
|
b4d3a879d2 | ||
|
|
040b33070b | ||
|
|
dae3a21b61 | ||
|
|
0769484fd6 | ||
|
|
683ef19b8a | ||
|
|
0e8ae7548d | ||
|
|
7f05b8ce58 | ||
|
|
e92a9c706b | ||
|
|
18d1947154 | ||
|
|
a69590b609 | ||
|
|
5adbc7baba | ||
|
|
7e7fd610cb | ||
|
|
ece8c9538d | ||
|
|
b77f6168d9 | ||
|
|
14f862f80c | ||
|
|
f1e91b6be6 | ||
|
|
835a050cfb | ||
|
|
2d3a1f5fab | ||
|
|
14091a8996 | ||
|
|
cc9d94efe8 | ||
|
|
c17517d22a | ||
|
|
4c9520bb1f | ||
|
|
7248053c6a | ||
|
|
c98ca6d2cc | ||
|
|
0a05dd4426 | ||
|
|
99fbd61bd9 | ||
|
|
b9e321c106 | ||
|
|
27f5235e67 | ||
|
|
694328aab6 | ||
|
|
cb4f3588fa | ||
|
|
7835f11f96 | ||
|
|
2e72c13aee | ||
|
|
898a70d340 | ||
|
|
435998cc4e | ||
|
|
ca7c6ca6da | ||
|
|
d10504af03 | ||
|
|
63cf379dbf | ||
|
|
65e3394481 | ||
|
|
8928b42d5d | ||
|
|
c47a95a4d2 | ||
|
|
5f83887334 | ||
|
|
04468c3c33 | ||
|
|
f9029f8644 | ||
|
|
8b3c3d9fef | ||
|
|
2333afcd1e | ||
|
|
4062e92c17 | ||
|
|
7023817a71 | ||
|
|
c1fb7d0545 | ||
|
|
cab1b129c0 | ||
|
|
0f812e0686 | ||
|
|
171f12c2eb | ||
|
|
9cea43b006 | ||
|
|
15adc92e76 | ||
|
|
ba1e8f0005 | ||
|
|
3eaea74a51 | ||
|
|
d84eb9a97b | ||
|
|
48aef307f9 | ||
|
|
e5569f681a | ||
|
|
145a0b154e | ||
|
|
9e5b492db1 | ||
|
|
b09889643f | ||
|
|
4e88157ed7 | ||
|
|
8b7780d494 | ||
|
|
c38363c16d | ||
|
|
75ba81c79a | ||
|
|
376a5a2aee | ||
|
|
47ee1d126d | ||
|
|
e0bf3713ea | ||
|
|
eadaf37606 | ||
|
|
baae9bfb22 | ||
|
|
4f3dcd9e39 | ||
|
|
7a9e901e5e | ||
|
|
bb184f90a7 | ||
|
|
0e8b152c68 | ||
|
|
e9aac23913 | ||
|
|
2c3285286c | ||
|
|
8e560f1d1c | ||
|
|
2a1461c754 | ||
|
|
cccfdc72c9 | ||
|
|
293f737e4a | ||
|
|
088b8ff69b | ||
|
|
3b25878d71 | ||
|
|
55afd95b20 | ||
|
|
5a32866b93 | ||
|
|
90aba582ec |
@@ -37,6 +37,10 @@
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets"
|
||||
},
|
||||
"Stock Delivered But Not Billed": {
|
||||
"account_type": "Stock Delivered But Not Billed",
|
||||
"account_category": "Stock Assets"
|
||||
},
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets"
|
||||
},
|
||||
@@ -223,10 +227,6 @@
|
||||
"Stock Received But Not Billed": {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_category": "Trade Payables"
|
||||
},
|
||||
"Stock Delivered But Not Billed": {
|
||||
"account_type": "Stock Delivered But Not Billed",
|
||||
"account_category": "Trade Payables"
|
||||
}
|
||||
},
|
||||
"Duties and Taxes": {
|
||||
|
||||
@@ -29,7 +29,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
refresh(frm) {
|
||||
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
|
||||
frm.set_read_only();
|
||||
erpnext.journal_entry.lock_reversal_entry(frm);
|
||||
}
|
||||
|
||||
erpnext.toggle_naming_series();
|
||||
@@ -232,6 +232,13 @@ Object.assign(erpnext.journal_entry, {
|
||||
}
|
||||
},
|
||||
|
||||
lock_reversal_entry(frm) {
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
|
||||
frm.set_df_property("accounts", "read_only", 1);
|
||||
},
|
||||
|
||||
add_custom_buttons(frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
|
||||
@@ -360,12 +360,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
# Passed posting_date is after PCV end date, so cancellation should not fail.
|
||||
make_reverse_gl_entries(
|
||||
voucher_type="Journal Entry",
|
||||
voucher_no=jv.name,
|
||||
posting_date="2022-01-01",
|
||||
)
|
||||
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", "2021-12-31")
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(
|
||||
voucher_type="Journal Entry",
|
||||
voucher_no=jv.name,
|
||||
)
|
||||
finally:
|
||||
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", None)
|
||||
|
||||
totals_after_cancel = frappe.get_all(
|
||||
"GL Entry",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
|
||||
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
|
||||
@@ -33,9 +33,11 @@ class SalesInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
|
||||
disable_sdbnb_in_sr = frappe.get_cached_value("Company", doc.company, "disable_sdbnb_in_sr")
|
||||
disable_sdbnb_in_sr, is_sdbnb_enabled = frappe.get_cached_value(
|
||||
"Company", doc.company, ["disable_sdbnb_in_sr", "enable_stock_delivered_but_not_billed"]
|
||||
)
|
||||
|
||||
if not (doc.is_return and disable_sdbnb_in_sr):
|
||||
if is_sdbnb_enabled and not (doc.is_return and disable_sdbnb_in_sr):
|
||||
self.stock_delivered_but_not_billed_gl_entries(gl_entries)
|
||||
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
@@ -1576,14 +1576,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
|
||||
|
||||
def test_stock_delivered_but_not_billed_gl_on_invoice(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
company = "_Test SDBNB Company"
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
make_purchase_receipt(
|
||||
company=company,
|
||||
item_code="_Test FG Item",
|
||||
warehouse="Stores - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
warehouse="Stores - _TSDBNB",
|
||||
cost_center="Main - _TSDBNB",
|
||||
qty=5,
|
||||
rate=100,
|
||||
)
|
||||
@@ -1591,13 +1591,13 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
dn = create_delivery_note(
|
||||
company=company,
|
||||
item_code="_Test FG Item",
|
||||
warehouse="Stores - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
warehouse="Stores - _TSDBNB",
|
||||
cost_center="Main - _TSDBNB",
|
||||
qty=2,
|
||||
rate=300,
|
||||
)
|
||||
# A perpetual-inventory Delivery Note books the cost to the SDBNB account
|
||||
self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - TCP1")
|
||||
self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - _TSDBNB")
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
si.insert()
|
||||
@@ -1609,9 +1609,9 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
fields=["account", "debit", "credit"],
|
||||
)
|
||||
sdbnb_credit = sum(
|
||||
row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - TCP1"
|
||||
row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - _TSDBNB"
|
||||
)
|
||||
cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - TCP1")
|
||||
cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - _TSDBNB")
|
||||
|
||||
# Billing reverses SDBNB and recognises the cost in COGS for an equal amount
|
||||
self.assertTrue(sdbnb_credit > 0)
|
||||
|
||||
@@ -640,13 +640,15 @@ def make_reverse_gl_entries(
|
||||
partial_cancel=partial_cancel,
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
|
||||
# For reverse entries, use the posting_date parameter if provided and valid
|
||||
# Otherwise fall back to original posting_date
|
||||
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
||||
if immutable_ledger_enabled:
|
||||
validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate()
|
||||
else:
|
||||
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
||||
|
||||
check_freezing_date(validation_date, gl_entries[0]["company"], adv_adj)
|
||||
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
|
||||
|
||||
if partial_cancel:
|
||||
@@ -715,7 +717,7 @@ def make_reverse_gl_entries(
|
||||
|
||||
if immutable_ledger_enabled:
|
||||
new_gle["is_cancelled"] = 0
|
||||
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||
new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate()
|
||||
elif posting_date:
|
||||
new_gle["posting_date"] = posting_date
|
||||
|
||||
|
||||
@@ -174,7 +174,17 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
},
|
||||
|
||||
get_datatable_options(options) {
|
||||
return Object.assign(options, { checkboxColumn: true });
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
events: {
|
||||
onCheckRow: () => erpnext.accounts.toggle_create_pe_primary_action(frappe.query_report),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
after_refresh: function (report) {
|
||||
report.datatable?.rowmanager?.checkAll(false);
|
||||
report.page.clear_primary_action();
|
||||
},
|
||||
|
||||
onload: function (report) {
|
||||
@@ -186,20 +196,27 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||
}
|
||||
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
report.page.add_inner_button(
|
||||
__("Create Payment Entries"),
|
||||
function () {
|
||||
erpnext.accounts.create_payment_entries_from_payable_report(report);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
frappe.provide("erpnext.accounts");
|
||||
|
||||
erpnext.accounts.toggle_create_pe_primary_action = function (report) {
|
||||
if (!report || !report.datatable || !frappe.model.can_create("Payment Entry")) return;
|
||||
|
||||
const has_purchase_invoice = report.datatable.rowmanager
|
||||
.getCheckedRows()
|
||||
.some((i) => report.datatable.datamanager.data[i]?.voucher_type === "Purchase Invoice");
|
||||
|
||||
if (has_purchase_invoice) {
|
||||
report.page.set_primary_action(__("Create Payment Entries"), () =>
|
||||
erpnext.accounts.create_payment_entries_from_payable_report(report)
|
||||
);
|
||||
} else {
|
||||
report.page.clear_primary_action();
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.accounts.create_payment_entries_from_payable_report = function (report) {
|
||||
const datatable = report.datatable;
|
||||
if (!datatable) return;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.accounts_payable_summary.accounts_payable_summary import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsPayableSummary(ERPNextTestSuite):
|
||||
"""Payable Summary is a thin wrapper over AccountsReceivableSummary with
|
||||
account_type=Payable; these tests lock the supplier-side output: invoiced,
|
||||
advance, paid, outstanding, ageing buckets and the optional GL-balance /
|
||||
future-payment columns."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
self.maxDiff = None
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
def _filters(self, **overrides):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"supplier": self.supplier,
|
||||
"posting_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
filters.update(overrides)
|
||||
return filters
|
||||
|
||||
def _make_invoice(self, rate=200):
|
||||
return make_purchase_invoice(
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
qty=1,
|
||||
rate=rate,
|
||||
price_list_rate=rate,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
def _expected_row(self, pi, **overrides):
|
||||
supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group")
|
||||
row = {
|
||||
"party_type": "Supplier",
|
||||
"advance": 0,
|
||||
"party": self.supplier,
|
||||
"invoiced": 200.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 200.0,
|
||||
"range1": 200.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 200.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"currency": pi.currency,
|
||||
"supplier_group": supplier_group,
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
def test_01_payable_summary_output(self):
|
||||
"""Invoiced -> advance -> partial payment progression for a single supplier."""
|
||||
filters = self._filters()
|
||||
pi = self._make_invoice()
|
||||
|
||||
expected = self._expected_row(pi)
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
# advance payment: pay 50 but allocate nothing against the invoice
|
||||
pe = get_payment_entry(pi.doctype, pi.name)
|
||||
pe.paid_amount = 50
|
||||
pe.references[0].allocated_amount = 0
|
||||
pe.save().submit()
|
||||
|
||||
expected.update({"advance": 50.0, "outstanding": 150.0, "range1": 150.0, "total_due": 150.0})
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
# partial payment allocated against the invoice
|
||||
pe = get_payment_entry(pi.doctype, pi.name)
|
||||
pe.paid_amount = 125
|
||||
pe.references[0].allocated_amount = 125
|
||||
pe.save().submit()
|
||||
|
||||
expected.update(
|
||||
{"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
|
||||
)
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Buying Settings", {"supp_master_name": "Naming Series"})
|
||||
def test_02_gl_balance_and_future_payment_columns(self):
|
||||
"""Naming-series naming adds party_name; show_gl_balance / show_future_payments
|
||||
add their columns; a fully-paid invoice drops out of the report."""
|
||||
filters = self._filters()
|
||||
pi = self._make_invoice()
|
||||
|
||||
pe = get_payment_entry(pi.doctype, pi.name)
|
||||
pe.paid_amount = 150
|
||||
pe.references[0].allocated_amount = 150
|
||||
pe.save().submit()
|
||||
|
||||
expected = self._expected_row(
|
||||
pi,
|
||||
party_name=frappe.db.get_value("Supplier", self.supplier, "supplier_name"),
|
||||
paid=150.0,
|
||||
outstanding=50.0,
|
||||
range1=50.0,
|
||||
total_due=50.0,
|
||||
)
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
# GL balance reconciliation columns
|
||||
filters.update({"show_gl_balance": True})
|
||||
expected.update({"gl_balance": 50.0, "diff": 0.0})
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
# future payment columns
|
||||
filters.update({"show_future_payments": True})
|
||||
expected.update({"remaining_balance": 50.0})
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertDictEqual(rows[0], expected)
|
||||
|
||||
# clear the remaining balance -> supplier drops out of the summary entirely
|
||||
get_payment_entry(pi.doctype, pi.name).save().submit()
|
||||
rows = execute(filters)[1]
|
||||
self.assertEqual(len(rows), 0)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.report.bank_clearance_summary.bank_clearance_summary import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
BANK_ACCOUNT = "_Test Bank - _TC"
|
||||
|
||||
|
||||
class TestBankClearanceSummary(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"account": BANK_ACCOUNT,
|
||||
"company": "_Test Company",
|
||||
"from_date": "2026-01-01",
|
||||
"to_date": "2026-12-31",
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def find_row(self, data, payment_entry):
|
||||
for row in data:
|
||||
if row[1] == payment_entry:
|
||||
return row
|
||||
return None
|
||||
|
||||
def test_uncleared_then_cleared_journal_entry(self):
|
||||
je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 5000, submit=True, posting_date="2026-06-01")
|
||||
|
||||
# Uncleared: the bank row appears with the debit amount and no clearance date
|
||||
row = self.find_row(self.run_report(), je.name)
|
||||
self.assertIsNotNone(row, "Journal Entry not listed in Bank Clearance Summary")
|
||||
self.assertEqual(row[0], "Journal Entry")
|
||||
self.assertEqual(frappe.utils.getdate(row[2]), frappe.utils.getdate("2026-06-01"))
|
||||
self.assertIsNone(row[4]) # clearance_date empty -> uncleared
|
||||
self.assertEqual(row[5], "Sales - _TC") # against account
|
||||
self.assertEqual(row[6], 5000) # debit - credit on the bank account
|
||||
|
||||
# Cleared: set the clearance date on the Journal Entry and re-run
|
||||
frappe.db.set_value("Journal Entry", je.name, "clearance_date", "2026-06-05")
|
||||
|
||||
row = self.find_row(self.run_report(), je.name)
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(frappe.utils.getdate(row[4]), frappe.utils.getdate("2026-06-05"))
|
||||
self.assertEqual(row[6], 5000)
|
||||
|
||||
def test_date_filter_excludes_out_of_range_entries(self):
|
||||
je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 3000, submit=True, posting_date="2026-06-10")
|
||||
|
||||
# Within range: present
|
||||
self.assertIsNotNone(self.find_row(self.run_report(), je.name))
|
||||
|
||||
# Window entirely after the posting date (from_date lower bound): excluded
|
||||
after = self.run_report(from_date="2026-07-01", to_date="2026-12-31")
|
||||
self.assertIsNone(self.find_row(after, je.name))
|
||||
|
||||
# Window ending before the posting date (to_date upper bound): excluded
|
||||
before = self.run_report(from_date="2026-01-01", to_date="2026-06-09")
|
||||
self.assertIsNone(self.find_row(before, je.name))
|
||||
@@ -31,7 +31,7 @@ def get_report_filters(report_filters):
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
filters.append(["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]])
|
||||
filters.append(["Purchase Invoice", "name", "=", report_filters.get("purchase_invoice")])
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.billed_items_to_be_received.billed_items_to_be_received import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBilledItemsToBeReceived(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"posting_date": today(),
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def get_rows_for(self, data, pi_name):
|
||||
return [row for row in data if row.get("name") == pi_name]
|
||||
|
||||
def test_billed_but_not_received_item_appears(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier",
|
||||
item_code="_Test Item",
|
||||
qty=5,
|
||||
rate=200,
|
||||
update_stock=0,
|
||||
)
|
||||
|
||||
rows = self.get_rows_for(self.run_report(), pi.name)
|
||||
self.assertEqual(len(rows), 1)
|
||||
|
||||
row = rows[0]
|
||||
self.assertEqual(row.get("supplier"), "_Test Supplier")
|
||||
self.assertEqual(row.get("company"), "_Test Company")
|
||||
self.assertEqual(row.get("item_code"), "_Test Item")
|
||||
self.assertEqual(row.get("qty"), 5)
|
||||
self.assertEqual(row.get("received_qty"), 0)
|
||||
self.assertEqual(row.get("rate"), 200)
|
||||
self.assertEqual(row.get("amount"), 1000)
|
||||
|
||||
def test_stock_updating_invoice_is_excluded(self):
|
||||
"""update_stock=1 means the item is already received; it must not appear."""
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier",
|
||||
item_code="_Test Item",
|
||||
qty=5,
|
||||
rate=200,
|
||||
update_stock=1,
|
||||
)
|
||||
|
||||
rows = self.get_rows_for(self.run_report(), pi.name)
|
||||
self.assertEqual(len(rows), 0)
|
||||
|
||||
def test_fully_received_invoice_drops_off(self):
|
||||
"""When per_received reaches 100 the invoice is fully received and drops off."""
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier",
|
||||
item_code="_Test Item",
|
||||
qty=5,
|
||||
rate=200,
|
||||
update_stock=0,
|
||||
)
|
||||
|
||||
# Present while nothing has been received.
|
||||
self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 1)
|
||||
|
||||
frappe.db.set_value("Purchase Invoice", pi.name, "per_received", 100)
|
||||
|
||||
# Absent once fully received.
|
||||
self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 0)
|
||||
|
||||
def test_posting_date_upper_bound_filter(self):
|
||||
"""A PI posted after the filter's posting_date must be excluded."""
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier",
|
||||
item_code="_Test Item",
|
||||
qty=5,
|
||||
rate=200,
|
||||
update_stock=0,
|
||||
)
|
||||
|
||||
rows = self.get_rows_for(self.run_report(posting_date="2000-01-01"), pi.name)
|
||||
self.assertEqual(len(rows), 0)
|
||||
|
||||
def test_purchase_invoice_filter_scopes_to_that_invoice(self):
|
||||
"""The optional purchase_invoice filter must narrow to that invoice only."""
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier", item_code="_Test Item", qty=5, rate=200, update_stock=0
|
||||
)
|
||||
other = make_purchase_invoice(
|
||||
supplier="_Test Supplier", item_code="_Test Item", qty=3, rate=200, update_stock=0
|
||||
)
|
||||
|
||||
names = {row.get("name") for row in self.run_report(purchase_invoice=pi.name)}
|
||||
self.assertEqual(names, {pi.name})
|
||||
self.assertNotIn(other.name, names)
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.test_budget import make_budget
|
||||
from erpnext.accounts.doctype.budget.test_budget import make_budget, set_total_expense_zero
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -33,7 +33,12 @@ class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
return execute(filters)[1]
|
||||
|
||||
def report_row(self, data, dimension, account=ACCOUNT):
|
||||
return next(row for row in data if row["budget_against"] == dimension and row["account"] == account)
|
||||
row = next(
|
||||
(r for r in data if r["budget_against"] == dimension and r["account"] == account),
|
||||
None,
|
||||
)
|
||||
self.assertIsNotNone(row, f"No report row for {dimension} / {account}")
|
||||
return row
|
||||
|
||||
def field(self, label):
|
||||
return frappe.scrub(f"{label} {self.fy}")
|
||||
@@ -55,6 +60,8 @@ class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
self.assertTrue(columns)
|
||||
|
||||
def test_budget_amount_shown_with_zero_actual(self):
|
||||
# neutralise any committed actuals so the exact Actual/Variance assertions hold
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
make_budget(
|
||||
budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1
|
||||
)
|
||||
@@ -65,6 +72,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
self.assertEqual(row[self.field("Variance")], 120000)
|
||||
|
||||
def test_actual_expense_updates_actual_and_variance(self):
|
||||
# zero out pre-committed actuals: keeps Actual exact and avoids the budget's
|
||||
# "Stop" action rejecting the journal entry when prior actuals already exist
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
make_budget(
|
||||
budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1
|
||||
)
|
||||
@@ -88,6 +98,8 @@ class TestBudgetVarianceReport(ERPNextTestSuite):
|
||||
self.assertEqual(dimensions, {COST_CENTER})
|
||||
|
||||
def test_monthly_period_totals(self):
|
||||
# zero out pre-committed actuals so total_actual reflects only this test's entry
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
make_budget(
|
||||
budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1
|
||||
)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils.formatters import format_value
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCalculatedDiscountMismatch(ERPNextTestSuite):
|
||||
"""Integrity detector: flag transactions whose stored ``discount_amount`` was tampered
|
||||
after the fact (a Version records the change) while ``additional_discount_percentage``
|
||||
stayed the same, so the stored amount no longer matches the percentage-derived value.
|
||||
"""
|
||||
|
||||
def run_report(self, docname: str) -> dict | None:
|
||||
"""Run the (filter-less) report and return the row for ``docname``, if any."""
|
||||
_columns, data = execute(frappe._dict({}))
|
||||
return next((row for row in data if row["docname"] == docname), None)
|
||||
|
||||
def create_discounted_invoice(self) -> "frappe.Document":
|
||||
"""Draft Sales Invoice (rate 1000) with a 10% additional discount.
|
||||
|
||||
The controller derives ``discount_amount`` = 10% of the grand total = 100.00,
|
||||
so the stored amount is consistent with the percentage.
|
||||
"""
|
||||
invoice = create_sales_invoice(rate=1000, qty=1, do_not_submit=1)
|
||||
invoice.additional_discount_percentage = 10
|
||||
invoice.save()
|
||||
invoice.reload()
|
||||
return invoice
|
||||
|
||||
def test_consistent_discount_is_not_flagged(self):
|
||||
"""A submitted invoice whose discount_amount matches its percentage is not reported."""
|
||||
invoice = self.create_discounted_invoice()
|
||||
invoice.submit()
|
||||
invoice.reload()
|
||||
|
||||
self.assertEqual(invoice.discount_amount, 100.0)
|
||||
self.assertIsNone(self.run_report(invoice.name))
|
||||
|
||||
def test_tampered_discount_is_flagged(self):
|
||||
"""Directly overwriting discount_amount (leaving the percentage intact) is reported.
|
||||
|
||||
This reproduces the real-world integrity breach: a Version records the
|
||||
``discount_amount`` change, its ``new`` value equals the current stored amount, and
|
||||
``additional_discount_percentage`` was not touched -- exactly the shape the report
|
||||
queries for.
|
||||
"""
|
||||
invoice = self.create_discounted_invoice()
|
||||
consistent_amount = invoice.discount_amount # 100.00, matches the 10% percentage
|
||||
tampered_amount = 250.0
|
||||
|
||||
discount_field = frappe.get_meta("Sales Invoice").get_field("discount_amount")
|
||||
# Format exactly as the report does so version.new == format_value(current amount).
|
||||
suspected = format_value(consistent_amount, df=discount_field, currency=invoice.currency)
|
||||
actual = format_value(tampered_amount, df=discount_field, currency=invoice.currency)
|
||||
|
||||
# Tamper the stored amount directly, bypassing the controller that would recompute it.
|
||||
frappe.db.set_value("Sales Invoice", invoice.name, "discount_amount", tampered_amount)
|
||||
self.record_discount_change(invoice.name, suspected, actual)
|
||||
|
||||
row = self.run_report(invoice.name)
|
||||
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(row["doctype"], "Sales Invoice")
|
||||
self.assertEqual(row["actual_discount_percentage"], 10.0)
|
||||
self.assertEqual(row["actual_discount_amount"], actual)
|
||||
self.assertEqual(row["suspected_discount_amount"], suspected)
|
||||
|
||||
def record_discount_change(self, docname: str, old: str, new: str) -> None:
|
||||
"""Insert the Version audit row a direct discount_amount edit would have produced."""
|
||||
version = frappe.new_doc("Version")
|
||||
version.ref_doctype = "Sales Invoice"
|
||||
version.docname = docname
|
||||
version.data = json.dumps({"changed": [["discount_amount", old, new]]}, separators=(",", ":"))
|
||||
version.flags.ignore_version = True
|
||||
version.insert(ignore_permissions=True)
|
||||
@@ -582,7 +582,12 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
total += flt(row[company])
|
||||
|
||||
row["has_value"] = has_value
|
||||
row["total"] = total
|
||||
# when accumulating into the group company, that company's column already consolidates its
|
||||
# descendants, so summing every company column would double-count; use the group total directly.
|
||||
if filters.get("accumulated_in_group_company"):
|
||||
row["total"] = flt(row.get(filters.company, 0.0), 3)
|
||||
else:
|
||||
row["total"] = total
|
||||
|
||||
data.append(row)
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
PARENT_COMPANY = "Parent Group Company India"
|
||||
CHILD_COMPANY = "Child Company India"
|
||||
|
||||
|
||||
class TestConsolidatedFinancialStatement(ERPNextTestSuite):
|
||||
"""Consolidation is exercised via the bootstrap group of companies
|
||||
(`Parent Group Company India` with child `Child Company India`). Income and
|
||||
expense posted in the child company must surface in the report that is run
|
||||
for the parent (group) company."""
|
||||
|
||||
def setUp(self):
|
||||
self.fiscal_year = get_fiscal_year(today(), company=PARENT_COMPANY)[0]
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": PARENT_COMPANY,
|
||||
"filter_based_on": "Fiscal Year",
|
||||
"from_fiscal_year": self.fiscal_year,
|
||||
"to_fiscal_year": self.fiscal_year,
|
||||
"periodicity": "Yearly",
|
||||
"include_default_book_entries": 1,
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def post_journal_entry(self, debit_account, credit_account, amount):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = today()
|
||||
je.company = CHILD_COMPANY
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{"account": debit_account, "debit_in_account_currency": amount},
|
||||
{"account": credit_account, "credit_in_account_currency": amount},
|
||||
],
|
||||
)
|
||||
je.save()
|
||||
je.submit()
|
||||
return je
|
||||
|
||||
def get_row(self, data, account_name_fragment, last_match=False):
|
||||
"""Return the first (or last) row whose account_name contains the fragment.
|
||||
|
||||
Pass ``last_match=True`` to get the leaf/most-specific match when the fragment
|
||||
is also a prefix of a parent group account (parents precede children in tree order).
|
||||
"""
|
||||
found = None
|
||||
for row in data:
|
||||
if account_name_fragment in str(row.get("account_name") or ""):
|
||||
if not last_match:
|
||||
return row
|
||||
found = row
|
||||
return found
|
||||
|
||||
def test_profit_and_loss_reflects_child_company_income(self):
|
||||
amount = 7000
|
||||
self.post_journal_entry("Cash - CCI", "Sales - CCI", amount)
|
||||
|
||||
data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0)
|
||||
|
||||
self.assertTrue(data, "Report returned no rows")
|
||||
|
||||
# child's Sales account is mapped onto the parent chart (Sales - PGCI)
|
||||
sales_row = self.get_row(data, "Sales", last_match=True)
|
||||
self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L")
|
||||
# >= so a pre-existing Sales balance in the fiscal year doesn't make this brittle
|
||||
self.assertGreaterEqual(flt(sales_row.get(CHILD_COMPANY)), amount)
|
||||
|
||||
total_income_row = self.get_row(data, "Total Income (Credit)")
|
||||
self.assertIsNotNone(total_income_row, "Total Income row missing")
|
||||
self.assertGreaterEqual(flt(total_income_row.get("total")), amount)
|
||||
|
||||
def test_profit_and_loss_reflects_child_company_expense(self):
|
||||
amount = 3000
|
||||
self.post_journal_entry("Marketing Expenses - CCI", "Cash - CCI", amount)
|
||||
|
||||
data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0)
|
||||
|
||||
expense_row = self.get_row(data, "Marketing Expenses", last_match=True)
|
||||
self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L")
|
||||
self.assertGreaterEqual(flt(expense_row.get(CHILD_COMPANY)), amount)
|
||||
|
||||
total_expense_row = self.get_row(data, "Total Expense (Debit)")
|
||||
self.assertIsNotNone(total_expense_row, "Total Expense row missing")
|
||||
self.assertGreaterEqual(flt(total_expense_row.get("total")), amount)
|
||||
|
||||
def test_accumulated_in_group_company_rolls_up_to_parent(self):
|
||||
"""With `accumulated_in_group_company`, the child's amount is also
|
||||
accumulated into the parent company column."""
|
||||
amount = 5000
|
||||
self.post_journal_entry("Cash - CCI", "Sales - CCI", amount)
|
||||
|
||||
data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1)
|
||||
|
||||
sales_row = self.get_row(data, "Sales", last_match=True)
|
||||
self.assertIsNotNone(sales_row)
|
||||
child_value = flt(sales_row.get(CHILD_COMPANY))
|
||||
self.assertGreaterEqual(child_value, amount)
|
||||
# parent column picks up the child value when accumulated
|
||||
self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), child_value)
|
||||
# the total equals the consolidated (group) value, not the sum of parent + child
|
||||
# columns -- this is the regression guard for the double-count fix
|
||||
self.assertEqual(flt(sales_row.get("total")), child_value)
|
||||
|
||||
def test_balance_sheet_executes_and_returns_rows(self):
|
||||
# posting income leaves a balancing entry in the child's Cash (Asset) account
|
||||
amount = 4000
|
||||
self.post_journal_entry("Cash - CCI", "Sales - CCI", amount)
|
||||
|
||||
data = self.run_report(report="Balance Sheet", accumulated_in_group_company=0)
|
||||
|
||||
self.assertTrue(data, "Balance Sheet returned no rows")
|
||||
cash_row = self.get_row(data, "Cash")
|
||||
self.assertIsNotNone(cash_row, "Cash asset row missing from consolidated Balance Sheet")
|
||||
self.assertGreaterEqual(flt(cash_row.get(CHILD_COMPANY)), amount)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.report.custom_financial_statement.custom_financial_statement import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCustomFinancialStatement(ERPNextTestSuite):
|
||||
"""The report renders a Financial Report Template through FinancialReportEngine.
|
||||
These tests exercise its own entry point: a template with an account-data row
|
||||
and a calculated row, and the guard that returns nothing without a template."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
self.company = "_Test Company"
|
||||
self.expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
self.cash_account = "Cash - _TC"
|
||||
|
||||
def _make_template(self):
|
||||
# rows filter by exact account name so the value is isolated from other data
|
||||
template_name = f"Test Custom FS {frappe.generate_hash()[:8]}"
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Financial Report Template",
|
||||
"template_name": template_name,
|
||||
"report_type": "Profit and Loss Statement",
|
||||
"rows": [
|
||||
{
|
||||
"reference_code": "EXP",
|
||||
"display_name": "Test Expense",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"calculation_formula": f'["name", "=", "{self.expense_account}"]',
|
||||
},
|
||||
{
|
||||
"reference_code": "EXP_X2",
|
||||
"display_name": "Expense Doubled",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Calculated Amount",
|
||||
"calculation_formula": "EXP * 2",
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
def _filters(self, template_name):
|
||||
return frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"report_template": template_name,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2024-12-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Yearly",
|
||||
"accumulated_values": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def test_account_and_calculated_rows(self):
|
||||
make_journal_entry(
|
||||
self.expense_account,
|
||||
self.cash_account,
|
||||
2000,
|
||||
posting_date="2024-06-15",
|
||||
company=self.company,
|
||||
submit=True,
|
||||
)
|
||||
template = self._make_template()
|
||||
|
||||
columns, data = execute(self._filters(template.template_name))[:2]
|
||||
self.assertTrue(columns)
|
||||
|
||||
rows = {row.get("account_name"): row for row in data}
|
||||
self.assertIn("Test Expense", rows)
|
||||
self.assertIn("Expense Doubled", rows)
|
||||
|
||||
period_keys = rows["Test Expense"].get("_segment_info", {}).get("period_keys", [])
|
||||
self.assertTrue(period_keys, "expected at least one period key in _segment_info")
|
||||
period_key = period_keys[0]
|
||||
|
||||
# the account-data row picks up the posted expense; the calculated row doubles it
|
||||
self.assertEqual(flt(rows["Test Expense"][period_key]), 2000.0)
|
||||
self.assertEqual(flt(rows["Expense Doubled"][period_key]), 4000.0)
|
||||
|
||||
def test_no_template_returns_nothing(self):
|
||||
"""Without a report_template the report short-circuits and returns None."""
|
||||
self.assertIsNone(execute(frappe._dict({"company": self.company})))
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.report.dimension_wise_accounts_balance_report.dimension_wise_accounts_balance_report import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestDimensionWiseAccountsBalance(ERPNextTestSuite):
|
||||
"""Balances accounts one column per value of an accounting dimension (here
|
||||
Cost Center). Locks the two behaviours that matter: an entry lands in its
|
||||
own dimension column as debit - credit, and children roll up into parents."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
self.company = "_Test Company"
|
||||
self.expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
self.cash_account = "Cash - _TC"
|
||||
|
||||
def _make_cost_center(self, name):
|
||||
full_name = f"{name} - _TC"
|
||||
if not frappe.db.exists("Cost Center", full_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": name,
|
||||
"parent_cost_center": "_Test Company - _TC",
|
||||
"company": self.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
).insert()
|
||||
return full_name
|
||||
|
||||
def _filters(self, **overrides):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"dimension": "Cost Center",
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
}
|
||||
)
|
||||
filters.update(overrides)
|
||||
return filters
|
||||
|
||||
def test_dimension_column_and_rollup(self):
|
||||
# a dedicated cost center isolates our column from any other posted data
|
||||
cost_center = self._make_cost_center("Test Dimension CC")
|
||||
make_journal_entry(
|
||||
self.expense_account,
|
||||
self.cash_account,
|
||||
300,
|
||||
cost_center=cost_center,
|
||||
posting_date=today(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
columns, data = execute(self._filters())
|
||||
column = frappe.scrub(cost_center)
|
||||
self.assertIn(column, [c["fieldname"] for c in columns])
|
||||
|
||||
rows = {row["account"]: row for row in data}
|
||||
|
||||
# the entry shows as debit - credit under its own dimension column
|
||||
self.assertEqual(rows[self.expense_account][column], 300.0)
|
||||
self.assertEqual(rows[self.cash_account][column], -300.0)
|
||||
|
||||
# and rolls up into each account's parent (isolated to our cost center)
|
||||
expense_parent = frappe.db.get_value("Account", self.expense_account, "parent_account")
|
||||
cash_parent = frappe.db.get_value("Account", self.cash_account, "parent_account")
|
||||
self.assertEqual(rows[expense_parent][column], 300.0)
|
||||
self.assertEqual(rows[cash_parent][column], -300.0)
|
||||
|
||||
def test_requires_fiscal_year(self):
|
||||
filters = self._filters()
|
||||
filters.pop("fiscal_year")
|
||||
self.assertRaises(frappe.ValidationError, execute, filters)
|
||||
@@ -21,7 +21,12 @@ def execute(filters=None):
|
||||
entries = get_entries(filters)
|
||||
invoice_details = get_invoice_posting_date_map(filters)
|
||||
|
||||
report = ReceivablePayableReport(filters)
|
||||
# Only four range columns are defined (range1-range4, the last being "90 Above").
|
||||
# Three thresholds yield exactly four buckets, so payments more than 90 days after
|
||||
# the invoice land in range4 instead of an unread range5.
|
||||
report_filters = frappe._dict(filters)
|
||||
report_filters.range = "30, 60, 90"
|
||||
report = ReceivablePayableReport(report_filters)
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
|
||||
@@ -32,15 +32,15 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite):
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)
|
||||
columns, data = execute(filters)
|
||||
fieldnames = [c["fieldname"] for c in columns]
|
||||
# Map each positional row to a dict keyed by column fieldname so assertions
|
||||
# stay correct even if a column is inserted or reordered.
|
||||
return columns, [dict(zip(fieldnames, row, strict=False)) for row in data]
|
||||
|
||||
def find_payment_row(self, data, payment_name):
|
||||
# Row shape (positional): payment_document, payment_entry(voucher_no),
|
||||
# party_type, party, posting_date, invoice(against_voucher_no),
|
||||
# invoice_posting_date, due_date, amount, remarks, age,
|
||||
# range1, range2, range3, range4, [delay_in_payment]
|
||||
for row in data:
|
||||
if row[1] == payment_name:
|
||||
if row["payment_entry"] == payment_name:
|
||||
return row
|
||||
return None
|
||||
|
||||
@@ -57,42 +57,60 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite):
|
||||
invoice = create_sales_invoice(customer="_Test Customer", rate=1000, posting_date="2026-06-01")
|
||||
payment = self.pay_invoice(invoice, "2026-06-20")
|
||||
|
||||
columns, data = self.run_report()
|
||||
_columns, data = self.run_report()
|
||||
|
||||
row = self.find_payment_row(data, payment.name)
|
||||
self.assertIsNotNone(row, "Payment row not found in report output")
|
||||
|
||||
# Positional assertions on the row shape.
|
||||
self.assertEqual(row[2], "Customer")
|
||||
self.assertEqual(row[4], getdate("2026-06-20")) # payment posting date
|
||||
self.assertEqual(row[5], invoice.name) # against invoice
|
||||
self.assertEqual(row[6], getdate("2026-06-01")) # invoice posting date
|
||||
self.assertEqual(row[8], 1000) # amount
|
||||
self.assertEqual(row[10], 19) # age = payment date - invoice date
|
||||
self.assertEqual(row["party_type"], "Customer")
|
||||
self.assertEqual(row["posting_date"], getdate("2026-06-20"))
|
||||
self.assertEqual(row["invoice"], invoice.name)
|
||||
self.assertEqual(row["invoice_posting_date"], getdate("2026-06-01"))
|
||||
self.assertEqual(row["amount"], 1000)
|
||||
self.assertEqual(row["age"], 19) # age = payment date - invoice date
|
||||
|
||||
# Buckets: 0-30 filled, others empty.
|
||||
self.assertEqual(row[11], 1000) # range1 (0-30)
|
||||
self.assertEqual(row[12], 0) # range2 (30-60)
|
||||
self.assertEqual(row[13], 0) # range3 (60-90)
|
||||
self.assertEqual(row[14], 0) # range4 (90 Above)
|
||||
self.assertEqual(row["range1"], 1000) # 0-30
|
||||
self.assertEqual(row["range2"], 0) # 30-60
|
||||
self.assertEqual(row["range3"], 0) # 60-90
|
||||
self.assertEqual(row["range4"], 0) # 90 Above
|
||||
|
||||
def test_paid_amount_lands_in_30_60_bucket(self):
|
||||
# invoice 2026-06-01, paid 2026-07-16 -> 45 days after -> 30-60 bucket
|
||||
invoice = create_sales_invoice(customer="_Test Customer 1", rate=1000, posting_date="2026-06-01")
|
||||
payment = self.pay_invoice(invoice, "2026-07-16")
|
||||
|
||||
columns, data = self.run_report()
|
||||
_columns, data = self.run_report()
|
||||
|
||||
row = self.find_payment_row(data, payment.name)
|
||||
self.assertIsNotNone(row, "Payment row not found in report output")
|
||||
|
||||
self.assertEqual(row[8], 1000) # amount
|
||||
self.assertEqual(row[10], 45) # age = payment date - invoice date
|
||||
self.assertEqual(row["amount"], 1000)
|
||||
self.assertEqual(row["age"], 45)
|
||||
# Buckets: 30-60 filled, others empty.
|
||||
self.assertEqual(row[11], 0) # range1 (0-30)
|
||||
self.assertEqual(row[12], 1000) # range2 (30-60)
|
||||
self.assertEqual(row[13], 0) # range3 (60-90)
|
||||
self.assertEqual(row[14], 0) # range4 (90 Above)
|
||||
self.assertEqual(row["range1"], 0)
|
||||
self.assertEqual(row["range2"], 1000)
|
||||
self.assertEqual(row["range3"], 0)
|
||||
self.assertEqual(row["range4"], 0)
|
||||
|
||||
def test_payment_over_90_days_lands_in_90_above_bucket(self):
|
||||
# invoice 2026-01-01, paid 2026-06-01 -> 151 days after -> "90 Above" bucket.
|
||||
# Regression guard: with four range columns, a payment older than the last
|
||||
# threshold must fall into range4 rather than an unread range5 (showing 0).
|
||||
invoice = create_sales_invoice(customer="_Test Customer 2", rate=1000, posting_date="2026-01-01")
|
||||
payment = self.pay_invoice(invoice, "2026-06-01")
|
||||
|
||||
_columns, data = self.run_report()
|
||||
|
||||
row = self.find_payment_row(data, payment.name)
|
||||
self.assertIsNotNone(row, "Payment row not found in report output")
|
||||
|
||||
self.assertEqual(row["amount"], 1000)
|
||||
self.assertEqual(row["age"], 151)
|
||||
self.assertEqual(row["range1"], 0)
|
||||
self.assertEqual(row["range2"], 0)
|
||||
self.assertEqual(row["range3"], 0)
|
||||
self.assertEqual(row["range4"], 1000) # 90 Above captures the full amount
|
||||
|
||||
def test_columns_expose_expected_age_buckets(self):
|
||||
columns, _data = self.run_report()
|
||||
|
||||
@@ -46,8 +46,9 @@ class TestProfitabilityAnalysis(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def test_income_expense_and_gross_profit(self):
|
||||
# bootstrap leaf cost center; clean of committed GL so exact assertions hold
|
||||
cc = "_Test Cost Center - _TC"
|
||||
# a dedicated leaf cost center keeps these exact assertions free of GL that
|
||||
# other tests may book against a shared cost center in the same fiscal year
|
||||
cc = self.make_cc("_Test PA Income Expense")
|
||||
self.book_income(cc, 10000)
|
||||
self.book_expense(cc, 4000)
|
||||
|
||||
@@ -74,7 +75,7 @@ class TestProfitabilityAnalysis(ERPNextTestSuite):
|
||||
self.assertEqual(parent_row["gross_profit_loss"], 7000)
|
||||
|
||||
def test_date_range_excludes_out_of_period_entries(self):
|
||||
cc = "_Test Cost Center 2 - _TC"
|
||||
cc = self.make_cc("_Test PA Date Range")
|
||||
self.book_income(cc, 10000, posting_date="2025-06-01")
|
||||
|
||||
# the 2025 income must not appear in a 2026 report (zero-value rows are dropped)
|
||||
@@ -97,7 +98,8 @@ class TestProfitabilityAnalysis(ERPNextTestSuite):
|
||||
data = self.run_report()
|
||||
# the report appends a blank separator row and a totals row at the end
|
||||
total_row = data[-1]
|
||||
self.assertEqual(total_row["account"], "'Total'")
|
||||
# the report wraps the (possibly translated) "Total" label in single quotes
|
||||
self.assertEqual(total_row["account"], "'" + frappe._("Total") + "'")
|
||||
# total is built from direct (non-accumulated) values, so it stays internally consistent
|
||||
self.assertEqual(total_row["gross_profit_loss"], total_row["income"] - total_row["expense"])
|
||||
# and it includes this test's bookings
|
||||
|
||||
@@ -22,7 +22,7 @@ def execute(filters=None):
|
||||
else:
|
||||
share_type, no_of_shares, rate, amount = 1, 2, 3, 4
|
||||
|
||||
all_shares = get_all_shares(filters.get("shareholder"), filters.get("date"))
|
||||
all_shares = get_all_shares(filters.get("shareholder"), filters.get("date"), filters.get("company"))
|
||||
for share_entry in all_shares:
|
||||
row = False
|
||||
for datum in data:
|
||||
@@ -61,16 +61,35 @@ def get_columns(filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_all_shares(shareholder, date):
|
||||
def get_all_shares(shareholder, date, company=None):
|
||||
"""Share movements for the shareholder up to (and including) `date`, signed by direction:
|
||||
shares received are positive, shares transferred/sold out are negative."""
|
||||
transfers = frappe.get_all(
|
||||
"Share Transfer",
|
||||
filters={"docstatus": 1, "date": ("<=", date)},
|
||||
fields=["share_type", "no_of_shares", "rate", "amount", "from_shareholder", "to_shareholder"],
|
||||
order_by="date",
|
||||
shares received are positive, shares transferred/sold out are negative.
|
||||
|
||||
The shareholder and company predicates are pushed into the query so only the
|
||||
relevant transfers are fetched instead of scanning the whole table."""
|
||||
share_transfer = frappe.qb.DocType("Share Transfer")
|
||||
query = (
|
||||
frappe.qb.from_(share_transfer)
|
||||
.select(
|
||||
share_transfer.share_type,
|
||||
share_transfer.no_of_shares,
|
||||
share_transfer.rate,
|
||||
share_transfer.amount,
|
||||
share_transfer.from_shareholder,
|
||||
share_transfer.to_shareholder,
|
||||
)
|
||||
.where((share_transfer.docstatus == 1) & (share_transfer.date <= date))
|
||||
.where(
|
||||
(share_transfer.to_shareholder == shareholder) | (share_transfer.from_shareholder == shareholder)
|
||||
)
|
||||
.orderby(share_transfer.date)
|
||||
)
|
||||
|
||||
if company:
|
||||
query = query.where(share_transfer.company == company)
|
||||
|
||||
transfers = query.run(as_dict=True)
|
||||
|
||||
shares = []
|
||||
for transfer in transfers:
|
||||
if transfer.to_shareholder == shareholder:
|
||||
|
||||
@@ -42,6 +42,30 @@ class TestShareBalanceReport(ERPNextTestSuite):
|
||||
self.assertEqual(row[3], 10) # average rate
|
||||
self.assertEqual(row[4], 1000) # amount = 100 * 10
|
||||
|
||||
def test_company_filter_scopes_transfers(self):
|
||||
# the transfer is booked under `_Test Company`
|
||||
create_share_transfer(
|
||||
transfer_type="Issue",
|
||||
to_shareholder=self.shareholder,
|
||||
share_type=self.share_type,
|
||||
from_no=1,
|
||||
to_no=100,
|
||||
no_of_shares=100,
|
||||
rate=10,
|
||||
date="2026-06-01",
|
||||
)
|
||||
|
||||
# matching company: the holding shows up
|
||||
self.assertEqual(self.get_row(date="2026-06-05")[2], 100)
|
||||
|
||||
# a different company must not surface this shareholder's transfer
|
||||
other_company_data = execute(
|
||||
frappe._dict(
|
||||
{"date": "2026-06-05", "company": "_Test Company 1", "shareholder": self.shareholder}
|
||||
)
|
||||
)[1]
|
||||
self.assertEqual(other_company_data, [])
|
||||
|
||||
def test_balance_increases_on_second_issue(self):
|
||||
create_share_transfer(
|
||||
transfer_type="Issue",
|
||||
|
||||
171
erpnext/accounts/report/share_ledger/test_share_ledger.py
Normal file
171
erpnext/accounts/report/share_ledger/test_share_ledger.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.report.share_ledger.share_ledger import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
|
||||
# The report returns legacy positional columns (no fieldnames); name the indices once
|
||||
# here so a column reorder needs a single edit instead of silently shifting assertions.
|
||||
COL_SHAREHOLDER = 0
|
||||
COL_DATE = 1
|
||||
COL_TRANSFER_TYPE = 2
|
||||
COL_SHARE_TYPE = 3
|
||||
COL_NO_OF_SHARES = 4
|
||||
COL_RATE = 5
|
||||
COL_AMOUNT = 6
|
||||
COL_COMPANY = 7
|
||||
COL_SHARE_TRANSFER = 8
|
||||
|
||||
|
||||
class TestShareLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.shareholder = self.create_shareholder("_Test Share Ledger Holder")
|
||||
# Issue 100 shares on 2026-06-01, then another 50 on 2026-06-10.
|
||||
self.first = self.issue_shares(date="2026-06-01", from_no=1, to_no=100, rate=10)
|
||||
self.second = self.issue_shares(date="2026-06-10", from_no=101, to_no=150, rate=12)
|
||||
|
||||
def test_ledger_lists_all_transfers_upto_date(self):
|
||||
data = self.run_report(shareholder=self.shareholder, date="2026-06-30")
|
||||
|
||||
self.assertEqual(len(data), 2)
|
||||
|
||||
first_row, second_row = data
|
||||
self.assertEqual(first_row[COL_SHAREHOLDER], self.shareholder)
|
||||
self.assertEqual(first_row[COL_DATE], frappe.utils.getdate("2026-06-01"))
|
||||
self.assertEqual(first_row[COL_TRANSFER_TYPE], "Issue")
|
||||
self.assertEqual(first_row[COL_SHARE_TYPE], "Equity")
|
||||
self.assertEqual(first_row[COL_NO_OF_SHARES], 100)
|
||||
self.assertEqual(first_row[COL_RATE], 10)
|
||||
self.assertEqual(first_row[COL_AMOUNT], 1000)
|
||||
self.assertEqual(first_row[COL_COMPANY], COMPANY)
|
||||
self.assertEqual(first_row[COL_SHARE_TRANSFER], self.first)
|
||||
|
||||
self.assertEqual(second_row[COL_DATE], frappe.utils.getdate("2026-06-10"))
|
||||
self.assertEqual(second_row[COL_NO_OF_SHARES], 50)
|
||||
self.assertEqual(second_row[COL_RATE], 12)
|
||||
self.assertEqual(second_row[COL_AMOUNT], 600)
|
||||
self.assertEqual(second_row[COL_SHARE_TRANSFER], self.second)
|
||||
|
||||
def test_running_balance_of_shares(self):
|
||||
data = self.run_report(shareholder=self.shareholder, date="2026-06-30")
|
||||
|
||||
# The ledger records each transfer's raw no_of_shares (always positive); it does
|
||||
# not sign by direction. With only incoming "Issue" rows here, summing them is a
|
||||
# valid running total. (Directional balances are the Share Balance report's job.)
|
||||
running = 0
|
||||
balances = []
|
||||
for row in data:
|
||||
running += row[COL_NO_OF_SHARES]
|
||||
balances.append(running)
|
||||
|
||||
self.assertEqual(balances, [100, 150])
|
||||
|
||||
def test_as_on_date_between_transfers_shows_only_first(self):
|
||||
data = self.run_report(shareholder=self.shareholder, date="2026-06-05")
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0][COL_SHARE_TRANSFER], self.first)
|
||||
self.assertEqual(data[0][COL_NO_OF_SHARES], 100)
|
||||
|
||||
def test_transfer_type_label_when_shareholder_is_seller(self):
|
||||
buyer = self.create_shareholder("_Test Share Ledger Buyer")
|
||||
transfer = self.make_transfer(
|
||||
from_shareholder=self.shareholder,
|
||||
to_shareholder=buyer,
|
||||
date="2026-06-15",
|
||||
from_no=1,
|
||||
to_no=40,
|
||||
rate=10,
|
||||
)
|
||||
|
||||
row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer)
|
||||
# seller side: the label names the counterparty it went "to"
|
||||
self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer to {buyer}")
|
||||
|
||||
def test_transfer_type_label_when_shareholder_is_buyer(self):
|
||||
seller = self.create_shareholder("_Test Share Ledger Seller")
|
||||
# the seller must own shares before it can transfer them
|
||||
self.issue_shares(date="2026-06-12", from_no=201, to_no=300, rate=10, shareholder=seller)
|
||||
transfer = self.make_transfer(
|
||||
from_shareholder=seller,
|
||||
to_shareholder=self.shareholder,
|
||||
date="2026-06-15",
|
||||
from_no=201,
|
||||
to_no=240,
|
||||
rate=10,
|
||||
)
|
||||
|
||||
row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer)
|
||||
# buyer side: the label names the counterparty it came "from"
|
||||
self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer from {seller}")
|
||||
|
||||
def test_missing_date_throws(self):
|
||||
self.assertRaises(frappe.ValidationError, execute, frappe._dict(shareholder=self.shareholder))
|
||||
|
||||
def test_missing_shareholder_returns_no_rows(self):
|
||||
data = self.run_report(date="2026-06-30")
|
||||
self.assertEqual(data, [])
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"company": COMPANY, **extra})
|
||||
return execute(filters)[1]
|
||||
|
||||
def transfer_row(self, data, transfer_name):
|
||||
row = next((r for r in data if r[COL_SHARE_TRANSFER] == transfer_name), None)
|
||||
self.assertIsNotNone(row, f"Share Transfer {transfer_name} missing from ledger")
|
||||
return row
|
||||
|
||||
def create_shareholder(self, title):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shareholder",
|
||||
"title": title,
|
||||
"company": COMPANY,
|
||||
}
|
||||
).insert()
|
||||
return doc.name
|
||||
|
||||
def issue_shares(self, date, from_no, to_no, rate, shareholder=None):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Share Transfer",
|
||||
"transfer_type": "Issue",
|
||||
"date": date,
|
||||
"to_shareholder": shareholder or self.shareholder,
|
||||
"share_type": "Equity",
|
||||
"from_no": from_no,
|
||||
"to_no": to_no,
|
||||
"no_of_shares": to_no - from_no + 1,
|
||||
"rate": rate,
|
||||
"company": COMPANY,
|
||||
"asset_account": "Cash - _TC",
|
||||
"equity_or_liability_account": "Creditors - _TC",
|
||||
}
|
||||
)
|
||||
doc.submit()
|
||||
return doc.name
|
||||
|
||||
def make_transfer(self, from_shareholder, to_shareholder, date, from_no, to_no, rate):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Share Transfer",
|
||||
"transfer_type": "Transfer",
|
||||
"date": date,
|
||||
"from_shareholder": from_shareholder,
|
||||
"to_shareholder": to_shareholder,
|
||||
"share_type": "Equity",
|
||||
"from_no": from_no,
|
||||
"to_no": to_no,
|
||||
"no_of_shares": to_no - from_no + 1,
|
||||
"rate": rate,
|
||||
"company": COMPANY,
|
||||
"asset_account": "Cash - _TC",
|
||||
"equity_or_liability_account": "Creditors - _TC",
|
||||
}
|
||||
)
|
||||
doc.submit()
|
||||
return doc.name
|
||||
329
erpnext/accounts/workspace/accounts_setup/accounts_setup.json
Normal file
329
erpnext/accounts/workspace/accounts_setup/accounts_setup.json
Normal file
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-14 12:44:31.994274",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "database",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Accounts Setup",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 13:43:50.138704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Accounts Setup",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 55.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "database",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Setup",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Chart of Accounts",
|
||||
"link_to": "Account",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Chart of Cost Centers",
|
||||
"link_to": "Cost Center",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Account Category",
|
||||
"link_to": "Account Category",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounting Dimension",
|
||||
"link_to": "Accounting Dimension",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Currency",
|
||||
"link_to": "Currency",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Currency Exchange",
|
||||
"link_to": "Currency Exchange",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Finance Book",
|
||||
"link_to": "Finance Book",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Mode of Payment",
|
||||
"link_to": "Mode of Payment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Term",
|
||||
"link_to": "Payment Term",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Journal Entry Template",
|
||||
"link_to": "Journal Entry Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Terms and Conditions",
|
||||
"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": "Company",
|
||||
"link_to": "Company",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Fiscal Year",
|
||||
"link_to": "Fiscal Year",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Taxes",
|
||||
"link_to": "Sales Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "lock-keyhole-open",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Opening & Closing",
|
||||
"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": "COA Importer",
|
||||
"link_to": "Chart of Accounts Importer",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Opening Invoice Tool",
|
||||
"link_to": "Opening Invoice Creation Tool",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounting Period",
|
||||
"link_to": "Accounting Period",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "FX Revaluation",
|
||||
"link_to": "Exchange Rate Revaluation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Period Closing Voucher",
|
||||
"link_to": "Period Closing Voucher",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "settings",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Settings",
|
||||
"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": "Accounts Settings",
|
||||
"link_to": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Currency Exchange Settings",
|
||||
"link_to": "Currency Exchange Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Accounts Setup",
|
||||
"type": "Workspace"
|
||||
}
|
||||
222
erpnext/accounts/workspace/banking/banking.json
Normal file
222
erpnext/accounts/workspace/banking/banking.json
Normal file
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-11 11:51:22.767176",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "circle-dollar-sign",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Banking",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 13:43:50.924019",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Banking",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 49.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "book-open-check",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Clearance",
|
||||
"link_to": "Bank Clearance",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "tool",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Reconciliation",
|
||||
"link_to": "Bank Reconciliation Tool",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "clipboard-check",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Reconciliation Statement",
|
||||
"link_to": "Bank Reconciliation Statement",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "split",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Unreconcile Payment",
|
||||
"link_to": "Unreconcile Payment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "link",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Process Payment Reconciliation",
|
||||
"link_to": "Process Payment Reconciliation",
|
||||
"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": "Bank",
|
||||
"link_to": "Bank",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Account",
|
||||
"link_to": "Bank Account",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Account Type",
|
||||
"link_to": "Bank Account Type",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Account Subtype",
|
||||
"link_to": "Bank Account Subtype",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Guarantee",
|
||||
"link_to": "Bank Guarantee",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Plaid Settings",
|
||||
"link_to": "Plaid Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "scroll-text",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Dunning",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dunning",
|
||||
"link_to": "Dunning",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dunning Type",
|
||||
"link_to": "Dunning Type",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Banking",
|
||||
"type": "Workspace"
|
||||
}
|
||||
104
erpnext/accounts/workspace/budgeting/budgeting.json
Normal file
104
erpnext/accounts/workspace/budgeting/budgeting.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-14 14:38:20.315394",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "accounting",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Budgeting",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-02 04:24:48.116724",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budgeting",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 57.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "briefcase-business",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Budget",
|
||||
"link_to": "Budget",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "badge-cent",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Cost Center",
|
||||
"link_to": "Cost Center",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "accounting",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounting Dimension",
|
||||
"link_to": "Accounting Dimension",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "notepad-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Cost Center Allocation",
|
||||
"link_to": "Cost Center Allocation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "sheet",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Budget Variance",
|
||||
"link_to": "Budget Variance Report",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Budgeting",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "table",
|
||||
"icon": "sheet",
|
||||
"idx": 1,
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
@@ -266,9 +266,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-18 09:49:45.138296",
|
||||
"modified": "2026-06-14 13:44:08.095321",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Financial Reports",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
@@ -279,6 +280,417 @@
|
||||
"roles": [],
|
||||
"sequence_id": 5.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "accounting",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Financial 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": "Balance Sheet",
|
||||
"link_to": "Balance Sheet",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Profit and Loss",
|
||||
"link_to": "Profit and Loss Statement",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Cash Flow",
|
||||
"link_to": "Cash Flow",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Trial Balance",
|
||||
"link_to": "Trial Balance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Consolidated Report",
|
||||
"link_to": "Consolidated Financial Statement",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Custom Financial Statement",
|
||||
"link_to": "Custom Financial Statement",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Financial Report Template",
|
||||
"link_to": "Financial Report Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "book-text",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Ledgers",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "General Ledger",
|
||||
"link_to": "General Ledger",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer Ledger",
|
||||
"link_to": "Customer Ledger Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Supplier Ledger",
|
||||
"link_to": "Supplier Ledger Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Registers",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Receivable",
|
||||
"link_to": "Accounts Receivable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Payable",
|
||||
"link_to": "Accounts Payable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "AR Summary",
|
||||
"link_to": "Accounts Receivable Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "AP Summary",
|
||||
"link_to": "Accounts Payable Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"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": "Purchase Register",
|
||||
"link_to": "Purchase 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": "Item-wise Purchase Register",
|
||||
"link_to": "Item-wise Purchase Register",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "dollar-sign",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Profitability",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Gross Profit",
|
||||
"link_to": "Gross Profit",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Profitability Analysis",
|
||||
"link_to": "Profitability Analysis",
|
||||
"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": "Purchase Invoice Trends",
|
||||
"link_to": "Purchase Invoice Trends",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "scroll-text",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Other 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": "Trial Balance for Party",
|
||||
"link_to": "Trial Balance for Party",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Period Based On Invoice Date",
|
||||
"link_to": "Payment Period Based On Invoice Date",
|
||||
"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": "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": "Sales Payment Summary",
|
||||
"link_to": "Sales Payment Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Address 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": "UAE VAT 201",
|
||||
"link_to": "UAE VAT 201",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Financial Reports",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -587,9 +587,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-23 11:05:47.246213",
|
||||
"modified": "2026-06-14 13:44:08.471142",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Invoicing",
|
||||
"number_cards": [
|
||||
{
|
||||
@@ -617,6 +618,354 @@
|
||||
"roles": [],
|
||||
"sequence_id": 2.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Invoicing",
|
||||
"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": "Accounts",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "list-tree",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Chart of Accounts",
|
||||
"link_to": "Account",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "arrow-left-to-line",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Receivables",
|
||||
"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": "Sales Invoice",
|
||||
"link_to": "Sales Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Credit Note",
|
||||
"link_to": "Sales Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"route_options": "{\"is_return\": 1}",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Receivable",
|
||||
"link_to": "Accounts Receivable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "arrow-right-from-line",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Payables",
|
||||
"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": "Purchase Invoice",
|
||||
"link_to": "Purchase Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Debit Note",
|
||||
"link_to": "Purchase Invoice",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"route_options": "{\"is_return\": 1}",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Payable",
|
||||
"link_to": "Accounts Payable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "money-coins-1",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Payments",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Entry",
|
||||
"link_to": "Payment Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Journal Entry",
|
||||
"link_to": "Journal Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Request",
|
||||
"link_to": "Payment Request",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Order",
|
||||
"link_to": "Payment Order",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Reconciliation",
|
||||
"link_to": "Payment Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Unreconcile Payment",
|
||||
"link_to": "Unreconcile Payment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Process Payment Reconciliation",
|
||||
"link_to": "Process Payment Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Repost Accounting Ledger",
|
||||
"link_to": "Repost Accounting Ledger",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Repost Payment Ledger",
|
||||
"link_to": "Repost Payment Ledger",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "sheet",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"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": "General Ledger",
|
||||
"link_to": "General Ledger",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Trial Balance",
|
||||
"link_to": "Trial Balance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Financial Reports",
|
||||
"link_to": "Financial Reports",
|
||||
"link_type": "Workspace",
|
||||
"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": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Invoicing",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
240
erpnext/accounts/workspace/payments/payments.json
Normal file
240
erpnext/accounts/workspace/payments/payments.json
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-11 11:51:21.886461",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "receipt-text",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Payments",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 13:43:50.184761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Payments",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 47.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
"link_to": "Payments",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "money-coins-1",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Payments",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Entry",
|
||||
"link_to": "Payment Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Journal Entry",
|
||||
"link_to": "Journal Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Request",
|
||||
"link_to": "Payment Request",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Order",
|
||||
"link_to": "Payment Order",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Reconciliation",
|
||||
"link_to": "Payment Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Unreconcile Payment",
|
||||
"link_to": "Unreconcile Payment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Process Payment Reconciliation",
|
||||
"link_to": "Process Payment Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Repost Accounting Ledger",
|
||||
"link_to": "Repost Accounting Ledger",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Repost Payment Ledger",
|
||||
"link_to": "Repost Payment Ledger",
|
||||
"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": "Accounts Receivable",
|
||||
"link_to": "Accounts Receivable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Payable",
|
||||
"link_to": "Accounts Payable",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "General Ledger",
|
||||
"link_to": "General Ledger",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Trial Balance",
|
||||
"link_to": "Trial Balance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Financial Reports",
|
||||
"link_to": "Financial Reports",
|
||||
"link_type": "Workspace",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Payments",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-11 11:51:22.831729",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "money-coins-1",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Share Management",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 13:43:51.040978",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Share Management",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 50.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "customer",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Shareholder",
|
||||
"link_to": "Shareholder",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "move-horizontal",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Share Transfer",
|
||||
"link_to": "Share Transfer",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "list",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Share Ledger",
|
||||
"link_to": "Share Ledger",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "notepad-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Share Balance",
|
||||
"link_to": "Share Balance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Share Management",
|
||||
"type": "Workspace"
|
||||
}
|
||||
121
erpnext/accounts/workspace/subscriptions/subscriptions.json
Normal file
121
erpnext/accounts/workspace/subscriptions/subscriptions.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-14 14:08:36.817393",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "accounting",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Subscriptions",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 14:08:36.999272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscriptions",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 56.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "circle-dollar-sign",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Subscription",
|
||||
"link_to": "Subscription",
|
||||
"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": "Subscription Plan",
|
||||
"link_to": "Subscription Plan",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "settings",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Subscription Settings",
|
||||
"link_to": "Subscription Settings",
|
||||
"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,
|
||||
"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": "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": "Item",
|
||||
"link_to": "Item",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Subscriptions",
|
||||
"type": "Workspace"
|
||||
}
|
||||
188
erpnext/accounts/workspace/taxes/taxes.json
Normal file
188
erpnext/accounts/workspace/taxes/taxes.json
Normal file
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-11 11:51:22.649582",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "money-coins-1",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Taxes",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-14 13:43:50.894825",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Taxes",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 48.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "panel-bottom-close",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Tax Template",
|
||||
"link_to": "Sales Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"navigate_to_tab": "",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "panel-top-close",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Purchase Tax Template",
|
||||
"link_to": "Purchase Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "stock",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Item Tax Template",
|
||||
"link_to": "Item Tax Template",
|
||||
"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": "triangle",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Category",
|
||||
"link_to": "Tax Category",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "book-open-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Rule",
|
||||
"link_to": "Tax Rule",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "book-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Withholding Category",
|
||||
"link_to": "Tax Withholding Category",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Withholding Group",
|
||||
"link_to": "Tax Withholding Group",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "notebook-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Deduction Certificate",
|
||||
"link_to": "Lower Deduction Certificate",
|
||||
"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_to": "",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "TDS Computation Summary",
|
||||
"link_to": "TDS Computation Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Tax Withholding Details",
|
||||
"link_to": "Tax Withholding Details",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Taxes",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -199,9 +199,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-31 16:22:38.132729",
|
||||
"modified": "2026-06-14 13:44:08.417956",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"module_onboarding": "Asset Onboarding",
|
||||
"name": "Assets",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
@@ -212,6 +213,294 @@
|
||||
"roles": [],
|
||||
"sequence_id": 7.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Assets",
|
||||
"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": "Asset",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "laptop",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset",
|
||||
"link_to": "Asset",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "trending-down",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Depreciation Schedule",
|
||||
"link_to": "Asset Depreciation Schedule",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "sprout",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Capitalization",
|
||||
"link_to": "Asset Capitalization",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "move-horizontal",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Movement",
|
||||
"link_to": "Asset Movement",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Maintenance",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Maintenance Team",
|
||||
"link_to": "Asset Maintenance Team",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Maintenance",
|
||||
"link_to": "Asset Maintenance",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Maintenance Log",
|
||||
"link_to": "Asset Maintenance Log",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Value Adjustment",
|
||||
"link_to": "Asset Value Adjustment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Repair",
|
||||
"link_to": "Asset Repair",
|
||||
"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": "Fixed Asset Register",
|
||||
"link_to": "Fixed Asset Register",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Depreciation Ledger",
|
||||
"link_to": "Asset Depreciation Ledger",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Depreciations and Balances",
|
||||
"link_to": "Asset Depreciations and Balances",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Maintenance",
|
||||
"link_to": "Asset Maintenance",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Asset Activity",
|
||||
"link_to": "Asset Activity",
|
||||
"link_type": "Report",
|
||||
"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,
|
||||
"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": "Asset Category",
|
||||
"link_to": "Asset Category",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Location",
|
||||
"link_to": "Location",
|
||||
"link_type": "DocType",
|
||||
"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": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"navigate_to_tab": "assets_tab",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Assets",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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, [])
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.crm.report.lead_owner_efficiency.lead_owner_efficiency import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestLeadOwnerEfficiency(ERPNextTestSuite):
|
||||
"""Groups leads by their owner and counts the opportunity/quotation/order funnel
|
||||
derived from those leads."""
|
||||
|
||||
def setUp(self):
|
||||
# a unique owner keeps the per-owner counts isolated from other tests' leads
|
||||
self.owner = self.make_user()
|
||||
|
||||
def make_user(self):
|
||||
email = f"lead_owner_{frappe.generate_hash(length=8)}@example.com"
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": email, "first_name": "Lead Owner", "send_welcome_email": 0}
|
||||
).insert()
|
||||
return email
|
||||
|
||||
def make_lead(self):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Lead",
|
||||
"lead_name": f"Lead {frappe.generate_hash(length=6)}",
|
||||
"lead_owner": self.owner,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"from_date": add_days(today(), -1), "to_date": today()})
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def owner_row(self, data):
|
||||
return next((r for r in data if r["lead_owner"] == self.owner), None)
|
||||
|
||||
def test_lead_count_grouped_by_owner(self):
|
||||
self.make_lead()
|
||||
self.make_lead()
|
||||
|
||||
row = self.owner_row(self.run_report())
|
||||
self.assertIsNotNone(row, "Lead owner missing from report")
|
||||
self.assertEqual(row["lead_count"], 2)
|
||||
self.assertEqual(row["opp_count"], 0)
|
||||
self.assertEqual(row["opp_lead"], 0.0)
|
||||
|
||||
def test_opportunity_from_lead_is_counted(self):
|
||||
lead = self.make_lead()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Opportunity",
|
||||
"opportunity_from": "Lead",
|
||||
"party_name": lead.name,
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
}
|
||||
).insert()
|
||||
|
||||
row = self.owner_row(self.run_report())
|
||||
self.assertIsNotNone(row, "Lead owner missing from report")
|
||||
self.assertEqual(row["lead_count"], 1)
|
||||
self.assertEqual(row["opp_count"], 1)
|
||||
# one opportunity from one lead -> 100% opp/lead conversion
|
||||
self.assertEqual(row["opp_lead"], 100.0)
|
||||
@@ -2,11 +2,11 @@
|
||||
"app": "erpnext",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Won Opportunities",
|
||||
"label": "Won Opportunities"
|
||||
"chart_name": "Territory Wise Sales",
|
||||
"label": "Territory Wise Sales"
|
||||
}
|
||||
],
|
||||
"content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use <a href=\\\"https://frappe.io/crm\\\">Frappe CRM</a> instead.\",\"col\":12}},{\"id\":\"-bzBQ_IbL9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Won Opportunities\",\"col\":12}},{\"id\":\"LdM1QgUnqU\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Lead (Last 1 Month)\",\"col\":4}},{\"id\":\"X23-SXBcYG\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"3rm7fH52M-\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Won Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"K6a2Kh5Zav\",\"type\":\"spacer\",\"data\":{\"col\":12}}]",
|
||||
"content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use <a href=\\\"http://frappe.io/crm\\\">Frappe CRM</a> instead.\",\"col\":12}},{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"69RN0XsiJK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"id\":\"t6PQ0vY-Iw\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"id\":\"VOFE0hqXRD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"0ik53fuemG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"wdROEmB_XG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"-I9HhcgUKE\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"ttpROKW9vk\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"-76QPdbBHy\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"id\":\"_YmGwzVWRr\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"Bma1PxoXk3\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"80viA0R83a\",\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"id\":\"Buo5HtKRFN\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"sLS_x4FMK2\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]",
|
||||
"creation": "2020-01-23 14:48:30.183272",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -18,6 +18,14 @@
|
||||
"is_hidden": 0,
|
||||
"label": "CRM",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Reports",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Lead",
|
||||
"hidden": 0,
|
||||
@@ -115,6 +123,14 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Maintenance",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -148,6 +164,183 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Sales Pipeline",
|
||||
"link_count": 7,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lead",
|
||||
"link_count": 0,
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Opportunity",
|
||||
"link_count": 0,
|
||||
"link_to": "Opportunity",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Customer",
|
||||
"link_count": 0,
|
||||
"link_to": "Customer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Contract",
|
||||
"link_count": 0,
|
||||
"link_to": "Contract",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Appointment",
|
||||
"link_count": 0,
|
||||
"link_to": "Appointment",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Newsletter",
|
||||
"link_count": 0,
|
||||
"link_to": "Newsletter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Communication",
|
||||
"link_count": 0,
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 2,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "CRM Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "CRM Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Campaign",
|
||||
"link_count": 5,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Campaign",
|
||||
"link_count": 0,
|
||||
"link_to": "Campaign",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Campaign",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Campaign",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Center",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Center",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Log",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -228,24 +421,11 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-03 15:05:23.983099",
|
||||
"modified": "2026-06-14 13:44:08.297053",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "New Lead (Last 1 Month)",
|
||||
"number_card_name": "New Lead (Last 1 Month)"
|
||||
},
|
||||
{
|
||||
"label": "New Opportunity (Last 1 Month)",
|
||||
"number_card_name": "New Opportunity (Last 1 Month)"
|
||||
},
|
||||
{
|
||||
"label": "Won Opportunity (Last 1 Month)",
|
||||
"number_card_name": "Won Opportunity (Last 1 Month)"
|
||||
}
|
||||
],
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
@@ -253,7 +433,552 @@
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 17.0,
|
||||
"shortcuts": [],
|
||||
"shortcuts": [
|
||||
{
|
||||
"color": "Blue",
|
||||
"format": "{} Open",
|
||||
"label": "Lead",
|
||||
"link_to": "Lead",
|
||||
"stats_filter": "{\"status\":\"Open\"}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"color": "Blue",
|
||||
"format": "{} Assigned",
|
||||
"label": "Opportunity",
|
||||
"link_to": "Opportunity",
|
||||
"stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Customer",
|
||||
"link_to": "Customer",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Sales Analytics",
|
||||
"link_to": "Sales Analytics",
|
||||
"report_ref_doctype": "Sales Order",
|
||||
"type": "Report"
|
||||
},
|
||||
{
|
||||
"label": "Dashboard",
|
||||
"link_to": "CRM",
|
||||
"type": "Dashboard"
|
||||
}
|
||||
],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "CRM",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "users-round",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Lead",
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "lightbulb",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Opportunity",
|
||||
"link_to": "Opportunity",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "customer",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer",
|
||||
"link_to": "Customer",
|
||||
"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,
|
||||
"icon": "",
|
||||
"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": "Lead Details",
|
||||
"link_to": "Lead Details",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Pipeline Analytics",
|
||||
"link_to": "Sales Pipeline Analytics",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Opportunity Summary by Sales Stage",
|
||||
"link_to": "Opportunity Summary by Sales Stage",
|
||||
"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": "Prospects Engaged But Not Converted",
|
||||
"link_to": "Prospects Engaged But Not Converted",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "First Response Time for Opportunity",
|
||||
"link_to": "First Response Time for Opportunity",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Campaign Efficiency",
|
||||
"link_to": "Campaign Efficiency",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Lead Owner Efficiency",
|
||||
"link_to": "Lead Owner Efficiency",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Maintenance",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Maintenance Schedule",
|
||||
"link_to": "Maintenance Schedule",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Maintenance Visit",
|
||||
"link_to": "Maintenance Visit",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Warranty Claim",
|
||||
"link_to": "Warranty Claim",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "funnel",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Sales Pipeline",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Lead",
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Opportunity",
|
||||
"link_to": "Opportunity",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"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": "Contract",
|
||||
"link_to": "Contract",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Appointment",
|
||||
"link_to": "Appointment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Communication",
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "sell",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Campaign",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"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": "Email Campaign",
|
||||
"link_to": "Email Campaign",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "SMS Center",
|
||||
"link_to": "SMS Center",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "SMS Log",
|
||||
"link_to": "SMS Log",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Email Group",
|
||||
"link_to": "Email Group",
|
||||
"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,
|
||||
"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": "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": "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": "Prospect",
|
||||
"link_to": "Prospect",
|
||||
"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 Stage",
|
||||
"link_to": "Sales Stage",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Lead Source",
|
||||
"link_to": "UTM Source",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "settings",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "CRM Settings",
|
||||
"link_to": "CRM Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "SMS Settings",
|
||||
"link_to": "SMS Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "CRM",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ app_email = "hello@frappe.io"
|
||||
app_license = "GNU General Public License (v3)"
|
||||
source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
app_home = "/desk"
|
||||
app_home = "/desk/home"
|
||||
|
||||
add_to_apps_screen = [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-28 10:20+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 20:08\n"
|
||||
"PO-Revision-Date: 2026-07-01 20:39\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -14218,7 +14218,7 @@ msgstr "آدرس فعلی"
|
||||
#. Label of the current_accommodation_type (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Current Address Is"
|
||||
msgstr "آدرس فعلی است"
|
||||
msgstr "آدرس فعلی"
|
||||
|
||||
#. Label of the current_amount (Currency) field in DocType 'Stock
|
||||
#. Reconciliation Item'
|
||||
@@ -17767,7 +17767,7 @@ msgstr "سود سهام پرداخت شده"
|
||||
#. Option for the 'Marital Status' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Divorced"
|
||||
msgstr "جدا شده"
|
||||
msgstr "طلاق گرفته"
|
||||
|
||||
#. Option for the 'Status' (Select) field in DocType 'Lead'
|
||||
#: erpnext/crm/doctype/lead/lead.json
|
||||
@@ -30241,7 +30241,7 @@ msgstr "متخصص بازاریابی"
|
||||
#. Option for the 'Marital Status' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Married"
|
||||
msgstr "متاهل"
|
||||
msgstr "متأهل"
|
||||
|
||||
#: erpnext/setup/setup_wizard/data/marketing_source.txt:7
|
||||
msgid "Mass Mailing"
|
||||
@@ -34688,7 +34688,7 @@ msgstr ""
|
||||
#. Option for the 'Current Address Is' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Owned"
|
||||
msgstr "مالکیت"
|
||||
msgstr "ملکی"
|
||||
|
||||
#: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29
|
||||
#: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:24
|
||||
@@ -37162,7 +37162,7 @@ msgstr "آدرس دائمی"
|
||||
#. 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Permanent Address Is"
|
||||
msgstr "آدرس دائمی است"
|
||||
msgstr "آدرس دائمی"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:73
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:77
|
||||
@@ -44208,7 +44208,7 @@ msgstr "اجاره"
|
||||
#. Option for the 'Current Address Is' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Rented"
|
||||
msgstr "اجاره شده"
|
||||
msgstr "استیجاری"
|
||||
|
||||
#. Label of the reorder_level (Float) field in DocType 'Material Request Item'
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
@@ -50940,7 +50940,7 @@ msgstr ""
|
||||
#. Option for the 'Marital Status' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Single"
|
||||
msgstr "تنها"
|
||||
msgstr "مجرد"
|
||||
|
||||
#. Option for the 'Bank Entry Type' (Select) field in DocType 'Bank Transaction
|
||||
#. Rule'
|
||||
@@ -60982,7 +60982,7 @@ msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، ب
|
||||
#. Option for the 'Marital Status' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Widowed"
|
||||
msgstr "بیوه"
|
||||
msgstr "همسر فوت شده"
|
||||
|
||||
#. Label of the width (Float) field in DocType 'Shipment Parcel'
|
||||
#. Label of the width (Float) field in DocType 'Shipment Parcel Template'
|
||||
@@ -61884,7 +61884,7 @@ msgstr "تراز صفر"
|
||||
|
||||
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353
|
||||
msgid "Zero Balance Journal: {0}"
|
||||
msgstr ""
|
||||
msgstr "دفتر تراز صفر: {0}"
|
||||
|
||||
#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78
|
||||
msgid "Zero Rated"
|
||||
@@ -62008,7 +62008,7 @@ msgstr "fieldname"
|
||||
|
||||
#: erpnext/setup/doctype/item_group/item_group.py:49
|
||||
msgid "for tax category {0}"
|
||||
msgstr ""
|
||||
msgstr "برای دسته بندی مالیاتی {0}"
|
||||
|
||||
#. Option for the 'Service Provider' (Select) field in DocType 'Currency
|
||||
#. Exchange Settings'
|
||||
@@ -62375,7 +62375,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/public/js/utils/sales_common.js:336
|
||||
msgid "{0} cannot be greater than 100"
|
||||
msgstr ""
|
||||
msgstr "{0} نمیتواند بزرگتر از ۱۰۰ باشد"
|
||||
|
||||
#: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136
|
||||
msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}"
|
||||
@@ -62444,7 +62444,7 @@ msgstr "{0} با موفقیت ارسال شد"
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:289
|
||||
msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return."
|
||||
msgstr ""
|
||||
msgstr "{0} داراییهای مرتبط با آن را ارسال کرده است. برای ایجاد بازگشت خرید، باید داراییها را لغو کنید."
|
||||
|
||||
#: erpnext/projects/doctype/project/project_dashboard.html:15
|
||||
msgid "{0} hours"
|
||||
@@ -62456,7 +62456,7 @@ msgstr "{0} در ردیف {1}"
|
||||
|
||||
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66
|
||||
msgid "{0} is a child company."
|
||||
msgstr ""
|
||||
msgstr "{0} یک شرکت فرزند است."
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465
|
||||
msgid "{0} is a child table and will be deleted automatically with its parent"
|
||||
@@ -62539,7 +62539,7 @@ msgstr "{0} در {1} فعال نیست"
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649
|
||||
msgid "{0} is not running. Cannot trigger events for this document"
|
||||
msgstr ""
|
||||
msgstr "{0} در حال اجرا نیست. نمیتوان رویدادها را برای این سند فعال کرد"
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:478
|
||||
msgid "{0} is not the default supplier for any items."
|
||||
@@ -62547,7 +62547,7 @@ msgstr "{0} تامین کننده پیشفرض هیچ موردی نیست."
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686
|
||||
msgid "{0} is on hold until {1}"
|
||||
msgstr ""
|
||||
msgstr "{0} تا زمان {1} در حالت انتظار است"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68
|
||||
msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry."
|
||||
@@ -62722,11 +62722,11 @@ msgstr ""
|
||||
#: erpnext/accounts/doctype/party_link/party_link.py:53
|
||||
#: erpnext/accounts/doctype/party_link/party_link.py:63
|
||||
msgid "{0} {1} is already linked with another {2}"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} از قبل به {2} دیگری لینک شده است"
|
||||
|
||||
#: erpnext/accounts/doctype/party_link/party_link.py:40
|
||||
msgid "{0} {1} is already linked with {2} {3}"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} از قبل به {2} {3} لینک شده است"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:711
|
||||
msgid "{0} {1} is associated with {2}, but Party Account is {3}"
|
||||
@@ -62767,7 +62767,7 @@ msgstr "{0} {1} فعال نیست"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452
|
||||
msgid "{0} {1} is not affecting bank account {2}"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} تاثیری بر حساب بانکی {2} ندارد"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:688
|
||||
msgid "{0} {1} is not associated with {2} {3}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-28 10:20+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 20:08\n"
|
||||
"PO-Revision-Date: 2026-07-03 21:32\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -5054,7 +5054,7 @@ msgstr "Fel uppstod under uppdatering process"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:368
|
||||
msgid "An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
|
||||
msgstr "Fel uppstod för vissa artiklar när Material Begäran skapades baserat på beställning nivå. Vänligen åtgärda dessa problem:"
|
||||
msgstr "Fel uppstod för vissa artiklar när Material Begäran skapades baserat på återbeställning nivå. Vänligen åtgärda dessa problem:"
|
||||
|
||||
#: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html:124
|
||||
msgid "Analysis Chart"
|
||||
@@ -6451,7 +6451,7 @@ msgstr "Automatiskt Skapad"
|
||||
#. Request'
|
||||
#: erpnext/stock/doctype/material_request/material_request.json
|
||||
msgid "Auto Created (Reorder)"
|
||||
msgstr "Skapas automatiskt (ombeställning)"
|
||||
msgstr "Skapas automatiskt (återbeställning)"
|
||||
|
||||
#. Label of the auto_created_serial_and_batch_bundle (Check) field in DocType
|
||||
#. 'Stock Ledger Entry'
|
||||
@@ -6569,7 +6569,7 @@ msgstr "Automatiskt avstämning av Parti i Bank Transaktioner"
|
||||
#. Label of the reorder_section (Section Break) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Auto re-order"
|
||||
msgstr "Automatisk Ombeställning"
|
||||
msgstr "Automatisk Återbeställning"
|
||||
|
||||
#. Label of the auto_reconcile_payments (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -8221,13 +8221,13 @@ msgstr "Parti {0} av Artikel {1} är Inaktiverad."
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Batch-Wise Balance History"
|
||||
msgstr "Saldo Historik per Parti"
|
||||
msgstr "Partibaserad Saldo Historik"
|
||||
|
||||
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164
|
||||
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183
|
||||
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86
|
||||
msgid "Batchwise Valuation"
|
||||
msgstr "Partivis Värdering"
|
||||
msgstr "Partibaserad Värdering"
|
||||
|
||||
#. Label of the section_break_3 (Section Break) field in DocType 'Stock
|
||||
#. Reconciliation Item'
|
||||
@@ -9319,7 +9319,7 @@ msgstr "Beräkna Uppskatade Ankomst Tider"
|
||||
#. Settings'
|
||||
#: erpnext/selling/doctype/selling_settings/selling_settings.json
|
||||
msgid "Calculate Product Bundle price based on child Item's rates"
|
||||
msgstr "Beräkna Artikel Paket pris baserat på priser för underordnade artiklar"
|
||||
msgstr "Beräkna Artikel Paket pris baserat på priser för paket artiklar"
|
||||
|
||||
#. Description of the 'Hidden Line (Internal Use Only)' (Check) field in
|
||||
#. DocType 'Financial Report Row'
|
||||
@@ -9814,7 +9814,7 @@ msgstr "Kan inte demontera {0} mot Lager Post {1}. Endast {2} tillgängliga för
|
||||
|
||||
#: erpnext/setup/doctype/company/company.py:233
|
||||
msgid "Cannot enable Item-wise Inventory Account, as there are existing Stock Ledger Entries for the company {0} with Warehouse-wise Inventory Account. Please cancel the stock transactions first and try again."
|
||||
msgstr "Kan inte aktivera Lager Konto per Lager, eftersom det redan finns befintliga Lager Register Poster för {0} med Lager Konto per Lager. Avbryt lager transaktioner först och försök igen."
|
||||
msgstr "Kan inte aktivera Artikelbaserad Lager Konto, eftersom det redan finns befintliga Lager Register Poster för {0} med Lagerbaserad Lager Konto. Avbryt lager transaktioner först och försök igen."
|
||||
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.py:43
|
||||
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
@@ -10213,7 +10213,7 @@ msgstr "Kategori Detaljer"
|
||||
|
||||
#: erpnext/assets/dashboard_fixtures.py:93
|
||||
msgid "Category-wise Asset Value"
|
||||
msgstr "Tillgång Värde per Kategori"
|
||||
msgstr "Kategoribaserad Tillgång Värde"
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/purchase_order.py:289
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:140
|
||||
@@ -15118,7 +15118,7 @@ msgstr "Kund eller Artikel"
|
||||
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.py:93
|
||||
msgid "Customer required for 'Customerwise Discount'"
|
||||
msgstr "Kund erfordras för \"Kund Rabatt\""
|
||||
msgstr "Kund erfordras för \"Kundbaserad Rabatt\""
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:885
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:392
|
||||
@@ -15171,7 +15171,7 @@ msgstr "Kundens Leverantör"
|
||||
#. Name of a report
|
||||
#: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.json
|
||||
msgid "Customer-wise Item Price"
|
||||
msgstr "Artikel Pris per Kund"
|
||||
msgstr "Kundbaserad Artikel Pris"
|
||||
|
||||
#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:43
|
||||
msgid "Customer/Lead Name"
|
||||
@@ -15206,7 +15206,7 @@ msgstr "Kunder inte valda."
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Customerwise Discount"
|
||||
msgstr "Rabatt per Kund"
|
||||
msgstr "Kundbaserad Rabatt"
|
||||
|
||||
#. Name of a DocType
|
||||
#. Label of the customs_tariff_number (Link) field in DocType 'Item'
|
||||
@@ -15680,7 +15680,7 @@ msgstr "Avdraget från"
|
||||
#. Deduction Certificate'
|
||||
#: erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
|
||||
msgid "Deductee Details"
|
||||
msgstr "Avdragsberättigad Detaljer"
|
||||
msgstr "Avdragstagare Detaljer"
|
||||
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/workspace_sidebar/taxes.json
|
||||
@@ -17168,7 +17168,7 @@ msgstr "Dimension Namn"
|
||||
#. Name of a report
|
||||
#: erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
|
||||
msgid "Dimension-wise Accounts Balance Report"
|
||||
msgstr "Bokföring Saldo Rapport per Dimension"
|
||||
msgstr "Dimension baserad Bokföring Saldo Rapport"
|
||||
|
||||
#. Label of the dimensions_section (Section Break) field in DocType 'GL Entry'
|
||||
#: erpnext/accounts/doctype/gl_entry/gl_entry.json
|
||||
@@ -17872,7 +17872,7 @@ msgstr "Utvidga Ej"
|
||||
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.py:129
|
||||
msgid "Do Not Use Batchwise Valuation"
|
||||
msgstr "Använd inte Parti baserad Värdering"
|
||||
msgstr "Använd inte Partibaserad Värdering"
|
||||
|
||||
#. Label of the do_not_fetch_incoming_rate_from_serial_no (Check) field in
|
||||
#. DocType 'Stock Reposting Settings'
|
||||
@@ -17908,7 +17908,7 @@ msgstr "Uppdatera inte Varianter vid Spara"
|
||||
#. Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Do not use Batch-wise Valuation"
|
||||
msgstr "Använd inte Partivis Värdering"
|
||||
msgstr "Använd inte Partibaserad Värdering"
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.js:957
|
||||
msgid "Do you really want to restore this scrapped asset?"
|
||||
@@ -18890,7 +18890,7 @@ msgstr "Aktivera Automatisk E-post"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1171
|
||||
msgid "Enable Auto Re-Order"
|
||||
msgstr "Aktivera Automatisk Ombeställning"
|
||||
msgstr "Aktivera Automatisk Återbeställning"
|
||||
|
||||
#. Label of the enable_party_matching (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -18969,7 +18969,7 @@ msgstr "Aktivera Oförenderlig Bokföring"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Enable Item-wise Inventory Account"
|
||||
msgstr "Aktivera Lager Konto per Artikel"
|
||||
msgstr "Aktivera Artikelbaserad Lager Konto"
|
||||
|
||||
#. Label of the enable_loyalty_point_program (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -19193,9 +19193,9 @@ msgid "Enabling this will do the following:\n"
|
||||
msgstr "Om du aktiverar detta kommer följande att hända:\n"
|
||||
"<ul style=\"padding-left:16px\">\n"
|
||||
"<li>Pris Kolumn i alla Artikel Paket tabeller redigerbar.</li>\n"
|
||||
"<li>Beräkna priser för alla <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">artikel paket</a> i Artikel tabell, baserat på priser för dess underordnade artiklar, som anges i Artikel Paket tabell. </li>\n"
|
||||
"<li>Beräkna priser för alla <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">Artikel Paket</a> i Artikel tabell, baserat på priser för paket artiklar, som anges i Artikel Paket tabell. </li>\n"
|
||||
"</ul>\n"
|
||||
"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra dess pris. Det kommer att återställas till det pris som baseras på dess underordnade artiklar när dokumentet sparas."
|
||||
"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra deras pris. Det kommer att återställas till pris som baseras på paket artiklar när dokument sparas."
|
||||
|
||||
#. Label of the encashment_date (Date) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -21056,7 +21056,7 @@ msgstr "Följ Kalender Månader"
|
||||
|
||||
#: erpnext/templates/emails/reorder_item.html:1
|
||||
msgid "Following Material Requests have been raised automatically based on Item's re-order level"
|
||||
msgstr "Följande Material Begäran skapades automatiskt baserat på Artikel beställning nivå"
|
||||
msgstr "Följande Material Begäran skapades automatiskt baserat på Artikel återbeställning nivå"
|
||||
|
||||
#: erpnext/selling/doctype/customer/mapper.py:173
|
||||
msgid "Following fields are mandatory to create address:"
|
||||
@@ -23792,7 +23792,7 @@ msgstr "Om artikel handlas som Noll Värdering Pris i denna post, aktivera 'Till
|
||||
#. Request Item'
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
msgid "If the reorder check is set at the Group warehouse level, the available quantity becomes the sum of the projected quantities of all its child warehouses."
|
||||
msgstr "Om ombeställning kontroll är angiven på grupp lager nivå blir tillgänglig kvantitet summa av planerad kvantitet för alla underordnade lager."
|
||||
msgstr "Om återbeställning kontroll är angiven på grupp lager nivå blir tillgänglig kvantitet summa av planerad kvantitet för alla underordnade lager."
|
||||
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.js:1286
|
||||
msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed."
|
||||
@@ -24688,7 +24688,7 @@ msgstr "Felaktig Parti Förbrukad"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:602
|
||||
msgid "Incorrect Check in (group) Warehouse for Reorder"
|
||||
msgstr "Felaktig vald (grupp) Lager för Ombeställning"
|
||||
msgstr "Felaktig vald (grupp) Lager för Återbeställning"
|
||||
|
||||
#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:148
|
||||
msgid "Incorrect Company"
|
||||
@@ -27177,7 +27177,7 @@ msgstr "Artikel Grupp inte angiven i Artikel Inställningar för Artikel {0}"
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Item Group wise Discount"
|
||||
msgstr "Rabatt per Artikel Grupp"
|
||||
msgstr "Artikel Grupp baserad Rabatt"
|
||||
|
||||
#. Label of the item_groups (Table) field in DocType 'POS Profile'
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
|
||||
@@ -27519,7 +27519,7 @@ msgstr "Artikel Referens"
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
msgid "Item Reorder"
|
||||
msgstr "Artikel Ombeställning"
|
||||
msgstr "Artikel Återbeställning"
|
||||
|
||||
#. Label of the item_row (Data) field in DocType 'Item Wise Tax Detail'
|
||||
#: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json
|
||||
@@ -27730,12 +27730,12 @@ msgstr "Var Används Artikel"
|
||||
#: erpnext/stock/report/item_wise_consumption/item_wise_consumption.json
|
||||
#: erpnext/workspace_sidebar/buying.json
|
||||
msgid "Item Wise Consumption"
|
||||
msgstr "Artikelvis Förbrukning"
|
||||
msgstr "Artikelbaserad Förbrukning"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json
|
||||
msgid "Item Wise Tax Detail"
|
||||
msgstr "Moms Detalj per Artikel"
|
||||
msgstr "Artikelbaserad Moms Detalj"
|
||||
|
||||
#. Label of the item_wise_tax_details (Table) field in DocType 'POS Invoice'
|
||||
#. Label of the item_wise_tax_details (Table) field in DocType 'Purchase
|
||||
@@ -27759,11 +27759,11 @@ msgstr "Moms Detalj per Artikel"
|
||||
#: erpnext/stock/doctype/delivery_note/delivery_note.json
|
||||
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
|
||||
msgid "Item Wise Tax Details"
|
||||
msgstr "Artikel Moms Detaljer"
|
||||
msgstr "Artikelbaserade Moms Detaljer"
|
||||
|
||||
#: erpnext/controllers/taxes_and_totals.py:562
|
||||
msgid "Item Wise Tax Details do not match with Taxes and Charges at the following rows:"
|
||||
msgstr "Artikel Moms Detaljer stämmer inte överens med Moms och Avgifter på följande rader:"
|
||||
msgstr "Artikelbaserade Moms Detaljer stämmer inte med Moms och Avgifter på följande rader:"
|
||||
|
||||
#. Label of the section_break_rrrx (Section Break) field in DocType 'Sales
|
||||
#. Forecast'
|
||||
@@ -27967,7 +27967,7 @@ msgstr "Artikel {0}: {1} Kvantitet producerad ."
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/item_wise_price_list_rate/item_wise_price_list_rate.json
|
||||
msgid "Item-wise Price List Rate"
|
||||
msgstr "Prislista Pris per Artikel"
|
||||
msgstr "Artikelbaserad Prislista Pris "
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Buying Workspace
|
||||
@@ -27976,14 +27976,14 @@ msgstr "Prislista Pris per Artikel"
|
||||
#: erpnext/buying/workspace/buying/buying.json
|
||||
#: erpnext/workspace_sidebar/buying.json
|
||||
msgid "Item-wise Purchase History"
|
||||
msgstr "Inköp Historik per Artikel"
|
||||
msgstr "Artikelbaserad Inköp Historik"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.json
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "Item-wise Purchase Register"
|
||||
msgstr "Inköp Register per Artikel"
|
||||
msgstr "Artikelbaserad Inköp Register"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Selling Workspace
|
||||
@@ -27992,19 +27992,19 @@ msgstr "Inköp Register per Artikel"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales History"
|
||||
msgstr "Försäljning Historik per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Historik"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales Register"
|
||||
msgstr "Försäljning Register per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Register"
|
||||
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "Item-wise sales Register"
|
||||
msgstr "Försäljning Register per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Register"
|
||||
|
||||
#: erpnext/stock/get_item_details.py:769
|
||||
msgid "Item/Item Code required to get Item Tax Template."
|
||||
@@ -28111,7 +28111,7 @@ msgstr "Artikel {0} saknas i Artikel Register."
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Itemwise Discount"
|
||||
msgstr "Rabatt per Artikel"
|
||||
msgstr "Artikelbaserad Rabatt"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Stock Workspace
|
||||
@@ -28120,7 +28120,7 @@ msgstr "Rabatt per Artikel"
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Itemwise Recommended Reorder Level"
|
||||
msgstr "Rekommenderad Ombeställning Nivå per Artikel"
|
||||
msgstr "Artikelbaserad Rekommenderad Återbeställning Nivå"
|
||||
|
||||
#. Option for the 'Barcode Type' (Select) field in DocType 'Item Barcode'
|
||||
#: erpnext/stock/doctype/item_barcode/item_barcode.json
|
||||
@@ -33834,7 +33834,7 @@ msgstr "Öppning Faktura Verktyg"
|
||||
#: erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py:832
|
||||
#: erpnext/accounts/doctype/sales_invoice/services/gl_composer.py:642
|
||||
msgid "Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
msgstr "Öppning Fakturan har avrundning justering på {0}.<br><br> '{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.<br><br> Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering."
|
||||
msgstr "Öppning Faktura har avrundning justering på {0}.<br><br> '{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.<br><br> Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering."
|
||||
|
||||
#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html:8
|
||||
msgid "Opening Invoices"
|
||||
@@ -40686,16 +40686,16 @@ msgstr "Projekt kommer att vara tillgänglig på hemsida till dessa Användare"
|
||||
#: erpnext/projects/workspace/projects/projects.json
|
||||
#: erpnext/workspace_sidebar/projects.json
|
||||
msgid "Project wise Stock Tracking"
|
||||
msgstr "Lager Spårning per Projekt"
|
||||
msgstr "Projektbaserad Lager Spårning"
|
||||
|
||||
#. Name of a report
|
||||
#: erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.json
|
||||
msgid "Project wise Stock Tracking "
|
||||
msgstr "Lager Spårning per Projekt"
|
||||
msgstr "Projektbaserad Lager Spårning "
|
||||
|
||||
#: erpnext/controllers/trends.py:457
|
||||
msgid "Project-wise data is not available for Quotation"
|
||||
msgstr "Data per Projekt finns inte tillgängligt för Försäljning Offert"
|
||||
msgstr "Projektbaserad data är inte tillgängligt för Försäljning Offert"
|
||||
|
||||
#. Label of the projected_on_hand (Float) field in DocType 'Material Request
|
||||
#. Item'
|
||||
@@ -41821,7 +41821,7 @@ msgstr "Kvantitet att Producera"
|
||||
|
||||
#: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:56
|
||||
msgid "Qty Wise Chart"
|
||||
msgstr "Kvantitet Diagram"
|
||||
msgstr "Kvantitetbaserad Diagram"
|
||||
|
||||
#. Label of the section_break_6 (Section Break) field in DocType 'Asset
|
||||
#. Capitalization Service Item'
|
||||
@@ -42669,7 +42669,7 @@ msgstr "Inköp Offerter är inte tillåtna för {0} på grund av Resultat Kort v
|
||||
#. Label of the auto_indent (Check) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Raise Material Request when stock reaches re-order level"
|
||||
msgstr "Skapa Material Begäran när Lager når ombeställning nivå"
|
||||
msgstr "Skapa Material Begäran när Lager når återbeställning nivå"
|
||||
|
||||
#. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim'
|
||||
#: erpnext/support/doctype/warranty_claim/warranty_claim.json
|
||||
@@ -43168,12 +43168,12 @@ msgstr "Återöppna"
|
||||
#. Label of the warehouse_reorder_level (Float) field in DocType 'Item Reorder'
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
msgid "Re-order Level"
|
||||
msgstr "Ombeställning Nivå"
|
||||
msgstr "Återbeställning Nivå"
|
||||
|
||||
#. Label of the warehouse_reorder_qty (Float) field in DocType 'Item Reorder'
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
msgid "Re-order Qty"
|
||||
msgstr "Ombeställning Kvantitet"
|
||||
msgstr "Återbeställning Kvantitet"
|
||||
|
||||
#: erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py:227
|
||||
msgid "Reached Root"
|
||||
@@ -44313,18 +44313,18 @@ msgstr "Hyrd"
|
||||
#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:64
|
||||
#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:213
|
||||
msgid "Reorder Level"
|
||||
msgstr "Ombeställning Nivå"
|
||||
msgstr "Återbeställning Nivå"
|
||||
|
||||
#. Label of the reorder_qty (Float) field in DocType 'Material Request Item'
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:220
|
||||
msgid "Reorder Qty"
|
||||
msgstr "Ombeställning Kvantitet"
|
||||
msgstr "Återbeställning Kvantitet"
|
||||
|
||||
#. Label of the reorder_levels (Table) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Reorder level based on Warehouse"
|
||||
msgstr "Ombeställning Nivå Baserad på Lager"
|
||||
msgstr "Återbeställning Nivå Baserad på Lager"
|
||||
|
||||
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry'
|
||||
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type'
|
||||
@@ -46411,7 +46411,7 @@ msgstr "Rad #{0}: Välj Underenhet Lager"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:590
|
||||
msgid "Row #{0}: Please set reorder quantity"
|
||||
msgstr "Rad # {0}: Ange Ombeställning Kvantitet"
|
||||
msgstr "Rad #{0}: Ange Återbeställning Kvantitet"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:522
|
||||
msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master"
|
||||
@@ -48044,7 +48044,7 @@ msgstr "Säljare Mål"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Sales Person-wise Transaction Summary"
|
||||
msgstr "Transaktion Översikt per Säljare"
|
||||
msgstr "Säljarebaserad Transaktion Översikt"
|
||||
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/selling/page/sales_funnel/sales_funnel.js:50
|
||||
@@ -49993,7 +49993,7 @@ msgstr "Ange Total Summa till Standard Betalning Metod"
|
||||
#. 'Territory'
|
||||
#: erpnext/setup/doctype/territory/territory.json
|
||||
msgid "Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution."
|
||||
msgstr "Ange Budget per Artikel Grupp för detta Distrikt. Man kan även inkludera säsongvariationer genom att ange Fördelning."
|
||||
msgstr "Ange Artikel Grupp baserad Budget för detta Distrikt. Inkludera även säsongvariationer genom att ange Fördelning."
|
||||
|
||||
#. Label of the set_landed_cost_based_on_purchase_invoice_rate (Check) field in
|
||||
#. DocType 'Buying Settings'
|
||||
@@ -50726,7 +50726,7 @@ msgstr "Visa Kumulativ Belopp"
|
||||
|
||||
#: erpnext/stock/report/stock_balance/stock_balance.js:143
|
||||
msgid "Show Dimension Wise Stock"
|
||||
msgstr "Visa Lager per Dimension"
|
||||
msgstr "Visa Dimensionbaserad Lager"
|
||||
|
||||
#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:29
|
||||
msgid "Show Disabled Items"
|
||||
@@ -50863,7 +50863,7 @@ msgstr "Visa Varianter"
|
||||
|
||||
#: erpnext/stock/report/stock_ageing/stock_ageing.js:64
|
||||
msgid "Show Warehouse-wise Stock"
|
||||
msgstr "Visa Lager Värde per Lager"
|
||||
msgstr "Visa Lagerbaserad Lager Värde"
|
||||
|
||||
#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:26
|
||||
msgid "Show availability of exploded items"
|
||||
@@ -55085,7 +55085,7 @@ msgstr "Distrikt Mål"
|
||||
#. Name of a report
|
||||
#: erpnext/selling/report/territory_wise_sales/territory_wise_sales.json
|
||||
msgid "Territory-wise Sales"
|
||||
msgstr "Försäljning per Distrikt"
|
||||
msgstr "Distriktbaserad Försäljning"
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -55656,7 +55656,7 @@ msgstr "{0} innehåller Enhet Pris Artiklar."
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:491
|
||||
msgid "The {0} prefix '{1}' already exists. Please change the Serial No Series, otherwise you will get a Duplicate Entry error."
|
||||
msgstr "Prefix {0} '{1}' finns redan. Ändra serienummer, annars blir det dubblett post."
|
||||
msgstr "Prefix {0} '{1}' finns redan. Ändra serie nummer, annars blir det Dubbel Post."
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:572
|
||||
msgid "The {0} {1} created successfully"
|
||||
@@ -59194,7 +59194,7 @@ msgstr "Använd <strong>Python</strong> filter för att hämta Konton"
|
||||
#. Label of the use_batchwise_valuation (Check) field in DocType 'Batch'
|
||||
#: erpnext/stock/doctype/batch/batch.json
|
||||
msgid "Use Batch-wise Valuation"
|
||||
msgstr "Använd Partivis Värdering"
|
||||
msgstr "Använd Partibaserad Värdering"
|
||||
|
||||
#. Label of the use_csv_sniffer (Check) field in DocType 'Bank Statement
|
||||
#. Import'
|
||||
@@ -60435,7 +60435,7 @@ msgstr "Verifikat {0} är övertilldelad av {1}"
|
||||
#. Name of a report
|
||||
#: erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json
|
||||
msgid "Voucher-wise Balance"
|
||||
msgstr "Saldo per Verifikat"
|
||||
msgstr "Verifikatbaserad Saldo"
|
||||
|
||||
#. Label of the vouchers (Table) field in DocType 'Repost Accounting Ledger'
|
||||
#. Label of the selected_vouchers_section (Section Break) field in DocType
|
||||
@@ -60563,7 +60563,7 @@ msgstr "Lager Typ"
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Warehouse Wise Stock Balance"
|
||||
msgstr "Lager Saldo per Lager"
|
||||
msgstr "Lagerbaserad Lager Saldo"
|
||||
|
||||
#. Label of the warehouse_and_reference (Section Break) field in DocType
|
||||
#. 'Request for Quotation Item'
|
||||
@@ -60616,7 +60616,7 @@ msgstr "Lager erfodras för Lager Artikel {0}"
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.json
|
||||
msgid "Warehouse wise Item Balance Age and Value"
|
||||
msgstr "Artikel Saldo Ålder och Värde per Lager"
|
||||
msgstr "Lagerbaserad Artikel Saldo, Ålder och Värde"
|
||||
|
||||
#: erpnext/stock/doctype/warehouse/warehouse.py:95
|
||||
msgid "Warehouse {0} can not be deleted as quantity exists for Item {1}"
|
||||
@@ -60825,7 +60825,7 @@ msgstr "Garanti Utgång (Serienummer)"
|
||||
#: erpnext/stock/doctype/serial_no/serial_no.json
|
||||
#: erpnext/support/doctype/warranty_claim/warranty_claim.json
|
||||
msgid "Warranty Expiry Date"
|
||||
msgstr "Garanti Utgångsdatum"
|
||||
msgstr "Garanti Utgång Datum"
|
||||
|
||||
#. Label of the warranty_period (Int) field in DocType 'Serial No'
|
||||
#: erpnext/stock/doctype/serial_no/serial_no.json
|
||||
@@ -61926,7 +61926,7 @@ msgstr "Du har inte utfört några avstämningar i denna sessionen ännu."
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1170
|
||||
msgid "You have to enable auto re-order in Stock Settings to maintain re-order levels."
|
||||
msgstr "Du måste aktivera automatisk ombeställning i lager inställningar för att behålla ombeställning nivåer."
|
||||
msgstr "Du måste aktivera automatisk återbeställning i Lager Inställningar för att behålla återbeställning nivåer."
|
||||
|
||||
#: erpnext/selling/page/point_of_sale/pos_controller.js:272
|
||||
msgid "You have unsaved changes. Do you want to save the invoice?"
|
||||
@@ -62020,7 +62020,7 @@ msgstr "Zip Fil"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:364
|
||||
msgid "[Important] [ERPNext] Auto Reorder Errors"
|
||||
msgstr "[Viktigt] [System] Automatisk Ombeställning Fel"
|
||||
msgstr "[Viktigt] [System] Automatisk Återbeställning Fel"
|
||||
|
||||
#: erpnext/controllers/status_updater.py:306
|
||||
msgid "`Allow Negative rates for Items`"
|
||||
|
||||
@@ -881,8 +881,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
|
||||
warehouse_list = [warehouse_list]
|
||||
|
||||
if not warehouse_list:
|
||||
# Reconcile every warehouse the item has a non-zero balance in -- including
|
||||
# negative balances left by other tests. `get_valuation_rate` averages
|
||||
# Sum(stock_value)/Sum(actual_qty) across all bins, so a leftover negative
|
||||
# balance in one warehouse can cancel the reset qty elsewhere and make the
|
||||
# average collapse to 0, which is a source of flaky BOM-cost failures.
|
||||
warehouse_list = frappe.get_all(
|
||||
"Bin", filters={"item_code": item_code, "actual_qty": [">", 0]}, pluck="warehouse"
|
||||
"Bin", filters={"item_code": item_code, "actual_qty": ["!=", 0]}, pluck="warehouse"
|
||||
)
|
||||
|
||||
if not warehouse_list:
|
||||
|
||||
@@ -2322,6 +2322,145 @@ class TestProductionPlan(ERPNextTestSuite):
|
||||
self.assertEqual(len(reserved_entries), 0)
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
|
||||
|
||||
def test_no_stock_reservation_via_purchase_receipt_when_reserve_stock_disabled(self):
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0)
|
||||
|
||||
bom_tree = {"FG For SR No Auto Reserve": {"RM For SR No Auto Reserve": {}}}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# reserve_stock is deliberately left unset (defaults to 0): this is what happens when
|
||||
# "Auto Reserve Stock" is off and nobody ticks "Reserve Stock" on the Production Plan by hand.
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=5,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
warehouse=warehouse,
|
||||
for_warehouse=warehouse,
|
||||
)
|
||||
plan.get_sub_assembly_items()
|
||||
plan.set("mr_items", [])
|
||||
for d in get_items_for_material_requests(plan.as_dict()):
|
||||
plan.append("mr_items", d)
|
||||
plan.save()
|
||||
|
||||
self.assertEqual(plan.reserve_stock, 0)
|
||||
plan.submit()
|
||||
|
||||
plan.submit_material_request = 1
|
||||
plan.make_material_request()
|
||||
|
||||
material_requests = frappe.get_all(
|
||||
"Material Request", filters={"production_plan": plan.name}, pluck="name"
|
||||
)
|
||||
self.assertGreater(len(material_requests), 0)
|
||||
|
||||
for mr_name in list(set(material_requests)):
|
||||
po = make_purchase_order(mr_name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.submit()
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.submit()
|
||||
|
||||
sre = StockReservation(plan)
|
||||
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
|
||||
self.assertEqual(len(reserved_entries), 0)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
|
||||
|
||||
def test_stock_reservation_ignores_production_plans_with_reserve_stock_off_on_shared_purchase_order(self):
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
bom_reserve = create_nested_bom({"FG SR Mixed Reserve": {"RM SR Mixed Reserve": {}}}, prefix="")
|
||||
bom_skip = create_nested_bom({"FG SR Mixed Skip": {"RM SR Mixed Skip": {}}}, prefix="")
|
||||
|
||||
def make_submitted_plan(item_code, reserve_stock):
|
||||
plan = create_production_plan(
|
||||
item_code=item_code,
|
||||
planned_qty=5,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
warehouse=warehouse,
|
||||
for_warehouse=warehouse,
|
||||
reserve_stock=reserve_stock,
|
||||
)
|
||||
plan.get_sub_assembly_items()
|
||||
plan.set("mr_items", [])
|
||||
for d in get_items_for_material_requests(plan.as_dict()):
|
||||
plan.append("mr_items", d)
|
||||
plan.save()
|
||||
plan.submit()
|
||||
plan.submit_material_request = 1
|
||||
plan.make_material_request()
|
||||
return plan
|
||||
|
||||
plan_reserve = make_submitted_plan(bom_reserve.item, reserve_stock=1)
|
||||
plan_skip = make_submitted_plan(bom_skip.item, reserve_stock=0)
|
||||
|
||||
self.assertEqual(plan_reserve.reserve_stock, 1)
|
||||
self.assertEqual(plan_skip.reserve_stock, 0)
|
||||
|
||||
mr_reserve = frappe.get_all(
|
||||
"Material Request", filters={"production_plan": plan_reserve.name}, pluck="name"
|
||||
)[0]
|
||||
mr_skip = frappe.get_all(
|
||||
"Material Request", filters={"production_plan": plan_skip.name}, pluck="name"
|
||||
)[0]
|
||||
|
||||
# One Purchase Order pulling rows from both Material Requests, so the Purchase Receipt made
|
||||
# from it has both a reservable and a non-reservable Production Plan reference in `doc.items`.
|
||||
po = frappe.new_doc("Purchase Order")
|
||||
po.supplier = "_Test Supplier"
|
||||
po.company = plan_reserve.company
|
||||
po.schedule_date = nowdate()
|
||||
|
||||
for mr_name in (mr_reserve, mr_skip):
|
||||
mr = frappe.get_doc("Material Request", mr_name)
|
||||
for item in mr.items:
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"qty": item.qty,
|
||||
"rate": 100,
|
||||
"schedule_date": nowdate(),
|
||||
"warehouse": warehouse,
|
||||
"material_request": mr.name,
|
||||
"material_request_item": item.name,
|
||||
},
|
||||
)
|
||||
|
||||
po.submit()
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.submit()
|
||||
|
||||
reserved_for_plan_reserve = StockReservation(plan_reserve).get_reserved_entries(
|
||||
"Production Plan", plan_reserve.name
|
||||
)
|
||||
reserved_for_plan_skip = StockReservation(plan_skip).get_reserved_entries(
|
||||
"Production Plan", plan_skip.name
|
||||
)
|
||||
|
||||
self.assertGreater(len(reserved_for_plan_reserve), 0)
|
||||
self.assertEqual(len(reserved_for_plan_skip), 0)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
|
||||
|
||||
def test_stock_reservation_restored_on_work_order_cancel(self):
|
||||
# Spec #5 (cancellation path): when a Work Order created from a Production Plan is cancelled,
|
||||
# the reservation that was transferred PP -> WO must flow back to the still-open Production
|
||||
|
||||
@@ -132,7 +132,9 @@ class StatusService:
|
||||
|
||||
status = (
|
||||
"In Process"
|
||||
if flt(self.doc.material_transferred_for_manufacturing) > 0 or self.doc.skip_transfer
|
||||
if flt(self.doc.material_transferred_for_manufacturing) > 0
|
||||
or self.doc.skip_transfer
|
||||
or self._has_transferred_material()
|
||||
else "Not Started"
|
||||
)
|
||||
precision = frappe.get_precision("Work Order", "produced_qty")
|
||||
@@ -141,6 +143,26 @@ class StatusService:
|
||||
status = "Completed"
|
||||
return status
|
||||
|
||||
def _has_transferred_material(self):
|
||||
"""True if any raw material was transferred against this work order via a pick list
|
||||
(these leave material_transferred_for_manufacturing at 0 via the min-fraction rule)."""
|
||||
ste = frappe.qb.DocType("Stock Entry")
|
||||
ste_child = frappe.qb.DocType("Stock Entry Detail")
|
||||
qty = (
|
||||
frappe.qb.from_(ste)
|
||||
.inner_join(ste_child)
|
||||
.on(ste_child.parent == ste.name)
|
||||
.select(Sum(ste_child.transfer_qty))
|
||||
.where(
|
||||
(ste.work_order == self.doc.name)
|
||||
& (ste.docstatus == 1)
|
||||
& (ste.purpose == "Material Transfer for Manufacture")
|
||||
& (ste.is_return == 0)
|
||||
& (ste.pick_list.isnotnull())
|
||||
)
|
||||
).run()[0][0]
|
||||
return flt(qty) > 0
|
||||
|
||||
def _is_partial_skip_transfer(self):
|
||||
return bool(
|
||||
self.doc.skip_transfer
|
||||
|
||||
@@ -1528,6 +1528,38 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0)
|
||||
|
||||
def test_status_in_process_when_only_one_required_item_transferred(self):
|
||||
"""Stock Entry created from a Pick List that picked only one of the required items:
|
||||
min-fraction keeps material_transferred_for_manufacturing at 0, but the work order must
|
||||
still move to In Process because material is already in WIP."""
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import create_pick_list
|
||||
from erpnext.stock.doctype.pick_list.mapper import create_stock_entry
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
planned_start_date=now(), qty=2, source_warehouse="Stores - _TC"
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0
|
||||
)
|
||||
|
||||
pick_list = create_pick_list(work_order.name, for_qty=work_order.qty)
|
||||
# pick only _Test Item; the other required item is left out of this pick list
|
||||
pick_list.pick_manually = 1
|
||||
pick_list.locations = [loc for loc in pick_list.locations if loc.item_code == "_Test Item"]
|
||||
pick_list.save()
|
||||
pick_list.submit()
|
||||
|
||||
stock_entry = frappe.get_doc(create_stock_entry(pick_list.as_dict()))
|
||||
self.assertEqual(stock_entry.fg_completed_qty, 0.0)
|
||||
stock_entry.submit()
|
||||
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 0.0)
|
||||
self.assertEqual(work_order.status, "In Process")
|
||||
|
||||
def test_backflushed_batch_raw_materials_based_on_transferred(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.manufacturing.report.bom_explorer.bom_explorer import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBOMExplorer(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
# the tests look up `_Test FG Item`'s BOM, which comes from the BOM fixtures;
|
||||
# load them so the file also passes when run in isolation
|
||||
self.load_test_records("BOM")
|
||||
|
||||
def run_report(self, bom):
|
||||
filters = frappe._dict({"bom": bom})
|
||||
return execute(filters)[1]
|
||||
|
||||
def top_level_rows_by_item(self, data):
|
||||
# key only the direct (indent 0) components, so an item that also appears in a
|
||||
# deeper sub-assembly can't overwrite the top-level row we assert against
|
||||
return {row["item_code"]: row for row in data if row["indent"] == 0}
|
||||
|
||||
def test_default_bom_lists_components_at_top_level(self):
|
||||
bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1})
|
||||
self.assertIsNotNone(bom, "Default active BOM for _Test FG Item not found")
|
||||
|
||||
data = self.run_report(bom)
|
||||
rows_by_item = self.top_level_rows_by_item(data)
|
||||
|
||||
self.assertIn("_Test Item", rows_by_item)
|
||||
self.assertIn("_Test Item Home Desktop 100", rows_by_item)
|
||||
|
||||
for item_code in ("_Test Item", "_Test Item Home Desktop 100"):
|
||||
row = rows_by_item[item_code]
|
||||
self.assertEqual(row["indent"], 0)
|
||||
self.assertEqual(row["bom_level"], 0)
|
||||
|
||||
def test_qty_matches_bom_item_qty(self):
|
||||
bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1})
|
||||
data = self.run_report(bom)
|
||||
rows_by_item = self.top_level_rows_by_item(data)
|
||||
|
||||
for bom_item in frappe.get_all(
|
||||
"BOM Item", filters={"parent": bom}, fields=["item_code", "qty", "uom"]
|
||||
):
|
||||
row = rows_by_item[bom_item.item_code]
|
||||
self.assertEqual(row["qty"], bom_item.qty)
|
||||
self.assertEqual(row["uom"], bom_item.uom)
|
||||
|
||||
def test_nested_bom_shows_deeper_level(self):
|
||||
# Sub-assembly: "sub" is itself a BOM containing "leaf".
|
||||
parent_bom = create_nested_bom(
|
||||
{"parent": {"sub": {"leaf": {}}, "flat": {}}},
|
||||
prefix="_Test explorer ",
|
||||
)
|
||||
|
||||
data = self.run_report(parent_bom.name)
|
||||
rows_by_item = {row["item_code"]: row for row in data}
|
||||
|
||||
sub_item = "_Test explorer sub"
|
||||
leaf_item = "_Test explorer leaf"
|
||||
flat_item = "_Test explorer flat"
|
||||
|
||||
self.assertIn(sub_item, rows_by_item)
|
||||
self.assertIn(flat_item, rows_by_item)
|
||||
self.assertIn(leaf_item, rows_by_item)
|
||||
|
||||
# Direct components of the parent sit at level 0.
|
||||
self.assertEqual(rows_by_item[flat_item]["indent"], 0)
|
||||
self.assertEqual(rows_by_item[sub_item]["indent"], 0)
|
||||
|
||||
# The sub-assembly row carries its own BOM reference.
|
||||
self.assertTrue(rows_by_item[sub_item]["bom"])
|
||||
|
||||
# The leaf belongs to the sub-assembly, so it is exploded one level deeper.
|
||||
self.assertEqual(rows_by_item[leaf_item]["indent"], 1)
|
||||
self.assertEqual(rows_by_item[leaf_item]["bom_level"], 1)
|
||||
@@ -9,77 +9,93 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
OPERATION = "_Test BOM Ops Time Operation"
|
||||
WORKSTATION = "_Test BOM Ops Time Workstation"
|
||||
OTHER_OPERATION = "_Test BOM Ops Time Operation 2"
|
||||
OTHER_WORKSTATION = "_Test BOM Ops Time Workstation 2"
|
||||
TIME_IN_MINS = 45
|
||||
|
||||
|
||||
class TestBOMOperationsTime(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
ensure_workstation_and_operation()
|
||||
ensure_workstation_and_operation(WORKSTATION, OPERATION)
|
||||
self.rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
self.fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.bom = build_bom_with_operation(self.fg_item, self.rm_item)
|
||||
self.bom = build_bom_with_operation(self.fg_item, self.rm_item, OPERATION, WORKSTATION)
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"bom_id": [self.bom.name]})
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
def run_report(self, **filters):
|
||||
return execute(frappe._dict(filters))[1]
|
||||
|
||||
def bom_names(self, rows):
|
||||
return {row.name for row in rows}
|
||||
|
||||
def build_other_bom(self):
|
||||
"""A submitted BOM for a different item, built on a different workstation."""
|
||||
ensure_workstation_and_operation(OTHER_WORKSTATION, OTHER_OPERATION)
|
||||
other_fg = make_item(properties={"is_stock_item": 1}).name
|
||||
return build_bom_with_operation(other_fg, self.rm_item, OTHER_OPERATION, OTHER_WORKSTATION)
|
||||
|
||||
def test_operation_row_appears_with_expected_values(self):
|
||||
rows = self.run_report()
|
||||
rows = self.run_report(bom_id=[self.bom.name])
|
||||
|
||||
bom_rows = [row for row in rows if row.name == self.bom.name]
|
||||
self.assertEqual(len(bom_rows), 1)
|
||||
|
||||
row = bom_rows[0]
|
||||
self.assertEqual(len(rows), 1)
|
||||
row = rows[0]
|
||||
self.assertEqual(row.name, self.bom.name)
|
||||
self.assertEqual(row.item, self.fg_item)
|
||||
self.assertEqual(row.operation, OPERATION)
|
||||
self.assertEqual(row.workstation, WORKSTATION)
|
||||
self.assertEqual(row.time_in_mins, TIME_IN_MINS)
|
||||
|
||||
def test_item_code_filter_scopes_to_bom(self):
|
||||
rows = self.run_report(item_code=self.fg_item)
|
||||
def test_item_code_filter_includes_matching_and_excludes_other(self):
|
||||
other_bom = self.build_other_bom()
|
||||
|
||||
self.assertTrue(rows)
|
||||
self.assertTrue(all(row.item == self.fg_item for row in rows))
|
||||
self.assertIn(self.bom.name, {row.name for row in rows})
|
||||
# no bom_id here, so the item_code filter alone must scope the result
|
||||
names = self.bom_names(self.run_report(item_code=self.fg_item))
|
||||
self.assertIn(self.bom.name, names)
|
||||
self.assertNotIn(other_bom.name, names)
|
||||
|
||||
def test_workstation_filter(self):
|
||||
matching = self.run_report(workstation=WORKSTATION)
|
||||
self.assertIn(self.bom.name, {row.name for row in matching})
|
||||
# reverse direction: filtering the other item drops our BOM
|
||||
other_names = self.bom_names(self.run_report(item_code=other_bom.item))
|
||||
self.assertIn(other_bom.name, other_names)
|
||||
self.assertNotIn(self.bom.name, other_names)
|
||||
|
||||
other_workstation = ensure_other_workstation()
|
||||
non_matching = self.run_report(workstation=other_workstation)
|
||||
self.assertNotIn(self.bom.name, {row.name for row in non_matching})
|
||||
def test_workstation_filter_includes_matching_and_excludes_other(self):
|
||||
other_bom = self.build_other_bom()
|
||||
|
||||
# no bom_id here, so the workstation filter alone must scope the result
|
||||
names = self.bom_names(self.run_report(workstation=WORKSTATION))
|
||||
self.assertIn(self.bom.name, names)
|
||||
self.assertNotIn(other_bom.name, names)
|
||||
|
||||
# reverse direction: filtering the other workstation drops our BOM
|
||||
other_names = self.bom_names(self.run_report(workstation=OTHER_WORKSTATION))
|
||||
self.assertIn(other_bom.name, other_names)
|
||||
self.assertNotIn(self.bom.name, other_names)
|
||||
|
||||
def test_draft_bom_excluded(self):
|
||||
draft_bom = build_bom_with_operation(
|
||||
make_item(properties={"is_stock_item": 1}).name, self.rm_item, do_not_submit=True
|
||||
make_item(properties={"is_stock_item": 1}).name,
|
||||
self.rm_item,
|
||||
OPERATION,
|
||||
WORKSTATION,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
rows = execute(frappe._dict({"bom_id": [draft_bom.name]}))[1]
|
||||
rows = self.run_report(bom_id=[draft_bom.name])
|
||||
self.assertEqual(rows, [])
|
||||
|
||||
|
||||
def ensure_workstation_and_operation():
|
||||
if not frappe.db.exists("Workstation", WORKSTATION):
|
||||
frappe.get_doc({"doctype": "Workstation", "workstation_name": WORKSTATION}).insert(
|
||||
def ensure_workstation_and_operation(workstation, operation):
|
||||
if not frappe.db.exists("Workstation", workstation):
|
||||
frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Operation", OPERATION):
|
||||
frappe.get_doc({"doctype": "Operation", "name": OPERATION, "workstation": WORKSTATION}).insert(
|
||||
if not frappe.db.exists("Operation", operation):
|
||||
frappe.get_doc({"doctype": "Operation", "name": operation, "workstation": workstation}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
|
||||
def ensure_other_workstation():
|
||||
name = "_Test BOM Ops Time Workstation 2"
|
||||
if not frappe.db.exists("Workstation", name):
|
||||
frappe.get_doc({"doctype": "Workstation", "workstation_name": name}).insert(ignore_permissions=True)
|
||||
return name
|
||||
|
||||
|
||||
def build_bom_with_operation(fg_item, rm_item, do_not_submit=False):
|
||||
def build_bom_with_operation(fg_item, rm_item, operation, workstation, do_not_submit=False):
|
||||
bom = make_bom(
|
||||
item=fg_item,
|
||||
raw_materials=[rm_item],
|
||||
@@ -89,8 +105,8 @@ def build_bom_with_operation(fg_item, rm_item, do_not_submit=False):
|
||||
bom.append(
|
||||
"operations",
|
||||
{
|
||||
"operation": OPERATION,
|
||||
"workstation": WORKSTATION,
|
||||
"operation": operation,
|
||||
"workstation": workstation,
|
||||
"time_in_mins": TIME_IN_MINS,
|
||||
"hour_rate": 100,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.bom_variance_report.bom_variance_report import execute
|
||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBOMVarianceReport(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.production_item = "_Test FG Item"
|
||||
self.warehouse = "_Test Warehouse - _TC"
|
||||
self.bom_no = frappe.db.get_value(
|
||||
"BOM", {"item": self.production_item, "is_active": 1, "is_default": 1}
|
||||
)
|
||||
self.raw_materials = self.get_bom_raw_materials()
|
||||
|
||||
# allow over-production so a Work Order can produce more than planned; ERPNextTestSuite
|
||||
# rolls this back at tearDown, so no manual restore is needed
|
||||
frappe.db.set_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order", 100)
|
||||
|
||||
def get_bom_raw_materials(self):
|
||||
return {
|
||||
row.item_code: row.qty
|
||||
for row in frappe.get_all(
|
||||
"BOM Item", filters={"parent": self.bom_no}, fields=["item_code", "qty"]
|
||||
)
|
||||
}
|
||||
|
||||
def create_over_produced_work_order(self, ordered_qty=2, produced_qty=3):
|
||||
work_order = make_wo_order_test_record(
|
||||
item=self.production_item,
|
||||
qty=ordered_qty,
|
||||
source_warehouse=self.warehouse,
|
||||
skip_transfer=1,
|
||||
)
|
||||
|
||||
for item_code in self.raw_materials:
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item_code, target=self.warehouse, qty=100, basic_rate=100
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", produced_qty))
|
||||
stock_entry.submit()
|
||||
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.produced_qty, produced_qty)
|
||||
return work_order
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"bom_no": self.bom_no, **extra})
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_over_produced_work_order_appears_with_planned_and_actual(self):
|
||||
work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3)
|
||||
|
||||
data = self.run_report(work_order=work_order.name)
|
||||
|
||||
summary_rows = [row for row in data if row.get("work_order") == work_order.name]
|
||||
self.assertEqual(len(summary_rows), 1)
|
||||
|
||||
summary = summary_rows[0]
|
||||
self.assertEqual(summary.get("production_item"), self.production_item)
|
||||
self.assertEqual(summary.get("bom_no"), self.bom_no)
|
||||
self.assertEqual(summary.get("qty"), 2)
|
||||
self.assertEqual(summary.get("produced_qty"), 3)
|
||||
|
||||
raw_material_rows = {
|
||||
row.get("raw_material_code"): row for row in data if row.get("raw_material_code")
|
||||
}
|
||||
for item_code, per_unit_qty in self.raw_materials.items():
|
||||
self.assertIn(item_code, raw_material_rows)
|
||||
# planned/required qty scales with the ordered qty on the work order
|
||||
self.assertEqual(raw_material_rows[item_code].get("required_qty"), per_unit_qty * 2)
|
||||
|
||||
def test_bom_no_filter_returns_over_produced_orders(self):
|
||||
work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3)
|
||||
|
||||
data = self.run_report()
|
||||
|
||||
matched = [row for row in data if row.get("work_order") == work_order.name]
|
||||
self.assertEqual(len(matched), 1)
|
||||
self.assertEqual(matched[0].get("bom_no"), self.bom_no)
|
||||
|
||||
def test_unstarted_work_order_is_excluded(self):
|
||||
work_order = make_wo_order_test_record(
|
||||
item=self.production_item,
|
||||
qty=2,
|
||||
source_warehouse=self.warehouse,
|
||||
skip_transfer=1,
|
||||
)
|
||||
|
||||
data = self.run_report(work_order=work_order.name)
|
||||
|
||||
matched = [row for row in data if row.get("work_order") == work_order.name]
|
||||
self.assertEqual(matched, [])
|
||||
|
||||
def test_work_order_produced_exactly_on_plan_is_excluded(self):
|
||||
# the canonical no-variance case: produced qty equals the planned qty, so the
|
||||
# report (which lists only over-produced orders) must not include it
|
||||
work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=2)
|
||||
|
||||
data = self.run_report(work_order=work_order.name)
|
||||
|
||||
matched = [row for row in data if row.get("work_order") == work_order.name]
|
||||
self.assertEqual(matched, [])
|
||||
@@ -120,6 +120,12 @@ def get_columns(filters):
|
||||
"options": "Workstation",
|
||||
"width": "100",
|
||||
},
|
||||
{
|
||||
"label": _("Hour Rate"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "hour_rate",
|
||||
"width": "120",
|
||||
},
|
||||
{
|
||||
"label": _("Operating Cost"),
|
||||
"fieldtype": "Currency",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils.data import add_to_date, now
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.mapper import make_corrective_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.cost_of_poor_quality_report.cost_of_poor_quality_report import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCostOfPoorQualityReport(ERPNextTestSuite):
|
||||
"""A Job Card appears in this report only when it is submitted (docstatus == 1) and flagged
|
||||
as a corrective job card (is_corrective_job_card == 1). Such a card is created against a
|
||||
corrective Operation (is_corrective_operation == 1); without any corrective operation the
|
||||
report returns no rows at all."""
|
||||
|
||||
def setUp(self):
|
||||
self.load_test_records("BOM")
|
||||
|
||||
def create_corrective_job_card(self, hour_rate=100):
|
||||
"""Produce a submitted corrective Job Card and return (corrective_jc, operation, workstation)."""
|
||||
work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": work_order.name})
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
corrective_operation = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_operation.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.hour_rate = hour_rate
|
||||
corrective_job_card.insert()
|
||||
corrective_job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=2),
|
||||
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
corrective_job_card.submit()
|
||||
|
||||
return corrective_job_card, corrective_operation.name, corrective_job_card.workstation
|
||||
|
||||
def run_report(self, **filters):
|
||||
return execute(frappe._dict(filters))[1]
|
||||
|
||||
def test_corrective_job_card_is_listed_with_expected_fields(self):
|
||||
corrective_jc, operation, workstation = self.create_corrective_job_card(hour_rate=100)
|
||||
|
||||
rows = self.run_report(company="_Test Company")
|
||||
row = next((r for r in rows if r["name"] == corrective_jc.name), None)
|
||||
|
||||
self.assertIsNotNone(row, "Submitted corrective job card must appear in the report")
|
||||
self.assertEqual(row["work_order"], corrective_jc.work_order)
|
||||
self.assertEqual(row["operation"], operation)
|
||||
self.assertEqual(row["workstation"], workstation)
|
||||
self.assertEqual(row["item_code"], corrective_jc.production_item)
|
||||
self.assertEqual(row["hour_rate"], 100)
|
||||
self.assertEqual(row["total_time_in_mins"], corrective_jc.total_time_in_mins)
|
||||
# operating_cost = hour_rate * total_time_in_mins / 60 (SQL float -> compare approximately)
|
||||
self.assertAlmostEqual(row["operating_cost"], 100 * corrective_jc.total_time_in_mins / 60.0, places=6)
|
||||
|
||||
def test_non_corrective_job_card_is_excluded(self):
|
||||
corrective_jc, _operation, _workstation = self.create_corrective_job_card()
|
||||
|
||||
# The regular (non-corrective) job card the corrective one was raised against must not appear.
|
||||
regular_jc = corrective_jc.for_job_card
|
||||
rows = self.run_report(company="_Test Company")
|
||||
self.assertNotIn(regular_jc, {r["name"] for r in rows})
|
||||
|
||||
def test_operation_filter_scopes_rows(self):
|
||||
corrective_jc, operation, _workstation = self.create_corrective_job_card()
|
||||
|
||||
matching = self.run_report(company="_Test Company", operation=operation)
|
||||
self.assertIn(corrective_jc.name, {r["name"] for r in matching})
|
||||
|
||||
other_operation = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
filtered = self.run_report(company="_Test Company", operation=other_operation.name)
|
||||
self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered})
|
||||
|
||||
def test_workstation_filter_scopes_rows(self):
|
||||
corrective_jc, _operation, workstation = self.create_corrective_job_card()
|
||||
|
||||
matching = self.run_report(company="_Test Company", workstation=workstation)
|
||||
self.assertIn(corrective_jc.name, {r["name"] for r in matching})
|
||||
|
||||
filtered = self.run_report(company="_Test Company", workstation="__non_existent_ws__")
|
||||
self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered})
|
||||
|
||||
def test_work_order_and_name_filters_scope_rows(self):
|
||||
corrective_jc, _operation, _workstation = self.create_corrective_job_card()
|
||||
|
||||
by_work_order = self.run_report(company="_Test Company", work_order=corrective_jc.work_order)
|
||||
self.assertIn(corrective_jc.name, {r["name"] for r in by_work_order})
|
||||
|
||||
by_name = self.run_report(company="_Test Company", name=corrective_jc.name)
|
||||
self.assertEqual({r["name"] for r in by_name}, {corrective_jc.name})
|
||||
|
||||
def test_date_filter_scopes_rows(self):
|
||||
corrective_jc, _operation, _workstation = self.create_corrective_job_card()
|
||||
|
||||
# Time logs sit ~2 hours from now; a window covering today includes the card.
|
||||
within = self.run_report(
|
||||
company="_Test Company",
|
||||
work_order=corrective_jc.work_order,
|
||||
from_date=add_to_date(now(), days=-1),
|
||||
to_date=add_to_date(now(), days=1),
|
||||
)
|
||||
self.assertIn(corrective_jc.name, {r["name"] for r in within})
|
||||
|
||||
# A future-only window excludes it, proving the Job Card Time Log join filters by time.
|
||||
outside = self.run_report(
|
||||
company="_Test Company",
|
||||
work_order=corrective_jc.work_order,
|
||||
from_date=add_to_date(now(), days=5),
|
||||
to_date=add_to_date(now(), days=6),
|
||||
)
|
||||
self.assertNotIn(corrective_jc.name, {r["name"] for r in outside})
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.manufacturing.report.exponential_smoothing_forecasting.exponential_smoothing_forecasting import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
FROM_DATE = "2026-06-01"
|
||||
TO_DATE = "2026-08-31"
|
||||
SMOOTHING_CONSTANT = 0.5
|
||||
|
||||
|
||||
class TestExponentialSmoothingForecasting(ERPNextTestSuite):
|
||||
"""Drive real submitted Sales Orders and assert the report buckets the ordered
|
||||
quantities into the correct historical periods and produces a forecast."""
|
||||
|
||||
def setUp(self):
|
||||
# The forecast query has no lower date bound, so it would pick up any committed
|
||||
# Sales Order for the item. A uniquely-named item keeps the buckets scoped to
|
||||
# just this test's orders.
|
||||
self.item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
def test_monthly_qty_forecast_from_sales_orders(self):
|
||||
# Historical demand: distinct calendar months strictly before FROM_DATE.
|
||||
# Monthly period keys are derived from the period's last day (e.g. "mar_2026").
|
||||
history = {"mar_2026": 7, "apr_2026": 4, "may_2026": 9}
|
||||
self.create_sales_orders(
|
||||
{
|
||||
"2026-03-15": history["mar_2026"],
|
||||
"2026-04-15": history["apr_2026"],
|
||||
"2026-05-15": history["may_2026"],
|
||||
}
|
||||
)
|
||||
|
||||
columns, row = self.run_report()
|
||||
fields = {col["fieldname"] for col in columns}
|
||||
|
||||
# For Monthly periodicity only future periods are exposed as columns, each as a
|
||||
# forecast_ field. Historical demand lives in the row data (keyed by month) but is
|
||||
# not surfaced as its own column.
|
||||
self.assertIn("forecast_jun_2026", fields, "expected future forecast column")
|
||||
self.assertNotIn("jun_2026", fields, "future period must not expose raw demand column")
|
||||
self.assertNotIn("mar_2026", fields, "historical month is not a Monthly report column")
|
||||
|
||||
# Historical buckets must exactly reflect the ordered quantities.
|
||||
for key, qty in history.items():
|
||||
self.assertEqual(flt(row.get(key)), flt(qty), f"bucket {key} mismatch")
|
||||
|
||||
# The forecast seeds at the average of the non-zero historical months and then
|
||||
# smooths through them in order: F = F + a*(actual - F). Asserting the exact
|
||||
# analytical value pins the smoothing formula (Jun 2026 works out to ~7.2083).
|
||||
expected_avg = sum(history.values()) / len(history)
|
||||
self.assertAlmostEqual(flt(row.get("avg")), expected_avg, places=6)
|
||||
|
||||
forecast = expected_avg
|
||||
for month in ("mar_2026", "apr_2026", "may_2026"):
|
||||
forecast = forecast + SMOOTHING_CONSTANT * (history[month] - forecast)
|
||||
self.assertAlmostEqual(flt(row.get("forecast_jun_2026")), forecast, places=6)
|
||||
|
||||
def test_ignores_documents_outside_range_and_other_docstatus(self):
|
||||
self.create_sales_orders({"2026-05-10": 6})
|
||||
# A draft SO and a future-dated SO must not contribute to historical demand.
|
||||
make_sales_order(item_code=self.item, qty=100, transaction_date="2026-05-20", do_not_submit=True)
|
||||
make_sales_order(item_code=self.item, qty=100, transaction_date=FROM_DATE)
|
||||
|
||||
_columns, row = self.run_report()
|
||||
self.assertEqual(flt(row.get("may_2026")), 6.0)
|
||||
|
||||
def create_sales_orders(self, date_to_qty):
|
||||
for transaction_date, qty in date_to_qty.items():
|
||||
make_sales_order(item_code=self.item, qty=qty, transaction_date=transaction_date)
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Monthly",
|
||||
"from_date": FROM_DATE,
|
||||
"to_date": TO_DATE,
|
||||
"smoothing_constant": SMOOTHING_CONSTANT,
|
||||
"item_code": self.item,
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
|
||||
columns, data = execute(filters)[:2]
|
||||
item_row = next(
|
||||
(r for r in data if r.get("item_code") == self.item),
|
||||
None,
|
||||
)
|
||||
self.assertIsNotNone(item_row, f"{self.item} row missing from report output")
|
||||
return columns, item_row
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.job_card_summary.job_card_summary import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestJobCardSummary(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
# `_Test FG Item 2` has a default active BOM with operations, so submitting a
|
||||
# Work Order for it auto-creates Job Cards (one per operation).
|
||||
self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
self.job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": self.work_order.name},
|
||||
fields=["name", "operation", "workstation", "production_item", "status"],
|
||||
)
|
||||
|
||||
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 rows_for_work_order(self, rows):
|
||||
return [row for row in rows if row.get("work_order") == self.work_order.name]
|
||||
|
||||
def test_job_cards_are_listed(self):
|
||||
self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards")
|
||||
|
||||
rows = self.rows_for_work_order(self.run_report())
|
||||
self.assertEqual(len(rows), len(self.job_cards))
|
||||
|
||||
reported_names = {row.get("name") for row in rows}
|
||||
self.assertEqual(reported_names, {jc.name for jc in self.job_cards})
|
||||
|
||||
# Fresh (unsubmitted) job cards are reported as Open, and each row carries the
|
||||
# operation / workstation / production item pulled from the Job Card.
|
||||
for jc in self.job_cards:
|
||||
row = next(row for row in rows if row.get("name") == jc.name)
|
||||
self.assertEqual(row.get("status"), "Open")
|
||||
self.assertEqual(row.get("operation"), jc.operation)
|
||||
self.assertEqual(row.get("workstation"), jc.workstation)
|
||||
self.assertEqual(row.get("production_item"), jc.production_item)
|
||||
|
||||
def test_operation_filter_scopes_rows(self):
|
||||
self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards")
|
||||
operation = self.job_cards[0].operation
|
||||
matching = {jc.name for jc in self.job_cards if jc.operation == operation}
|
||||
|
||||
rows = self.rows_for_work_order(self.run_report(operation=operation))
|
||||
self.assertEqual({row.get("name") for row in rows}, matching)
|
||||
|
||||
def test_status_filter(self):
|
||||
self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards")
|
||||
|
||||
# The status filter matches the Job Card's *stored* status, so derive the
|
||||
# expected set from that rather than assuming fresh cards are literally "Open".
|
||||
stored_status = self.job_cards[0].status
|
||||
expected = {jc.name for jc in self.job_cards if jc.status == stored_status}
|
||||
|
||||
rows = self.rows_for_work_order(self.run_report(status=stored_status))
|
||||
self.assertEqual({row.get("name") for row in rows}, expected)
|
||||
# any non-completed card is displayed as "Open" regardless of its stored status
|
||||
for row in rows:
|
||||
self.assertEqual(row.get("status"), "Open")
|
||||
|
||||
# None of the freshly created job cards are Completed yet.
|
||||
completed_rows = self.rows_for_work_order(self.run_report(status="Completed"))
|
||||
self.assertEqual(completed_rows, [])
|
||||
|
||||
def test_date_filter_excludes_out_of_range(self):
|
||||
# Job Card posting_date defaults to today; a past-only window should exclude them.
|
||||
rows = self.rows_for_work_order(
|
||||
self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5))
|
||||
)
|
||||
self.assertEqual(rows, [])
|
||||
@@ -50,11 +50,11 @@ def get_data(filters: Filters) -> Data:
|
||||
.groupby(se.work_order)
|
||||
)
|
||||
|
||||
if "item" in filters:
|
||||
query.where(wo.production_item == filters.item)
|
||||
if filters.get("item"):
|
||||
query = query.where(wo.production_item == filters.item)
|
||||
|
||||
if "work_order" in filters:
|
||||
query.where(wo.name == filters.work_order)
|
||||
if filters.get("work_order"):
|
||||
query = query.where(wo.name == filters.work_order)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class TestProcessLossReport(ERPNextTestSuite):
|
||||
self.assertEqual(wo_order.process_loss_qty, 1)
|
||||
self.assertEqual(wo_order.produced_qty, 4)
|
||||
|
||||
data = self.run_report(work_order=wo_order.name)
|
||||
data = self.run_report()
|
||||
row = self.find_row(data, wo_order.name)
|
||||
|
||||
self.assertIsNotNone(row, "Work order with process loss should appear in the report")
|
||||
@@ -93,26 +93,22 @@ class TestProcessLossReport(ERPNextTestSuite):
|
||||
self.assertEqual(wo_order.process_loss_qty, 0)
|
||||
self.assertEqual(wo_order.produced_qty, 5)
|
||||
|
||||
data = self.run_report(work_order=wo_order.name)
|
||||
data = self.run_report()
|
||||
self.assertIsNone(
|
||||
self.find_row(data, wo_order.name),
|
||||
"Work order that produced the full planned qty should not appear (no loss)",
|
||||
)
|
||||
|
||||
def test_item_and_work_order_filters_are_ineffective(self):
|
||||
"""BUG: the `item` and `work_order` filters in process_loss_report.get_data
|
||||
call `query.where(...)` without reassigning the result. frappe's query
|
||||
builder is immutable, so `.where()` returns a new query and these extra
|
||||
conditions are silently dropped. A non-matching item filter therefore fails
|
||||
to exclude the row. This test documents the current (buggy) behaviour; if the
|
||||
report is fixed to reassign the query, update the assertion below to
|
||||
`assertIsNone`.
|
||||
"""
|
||||
def test_item_filter_scopes_rows(self):
|
||||
wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4)
|
||||
|
||||
# A non-matching item filter should exclude the row, but currently does not.
|
||||
data = self.run_report(item="_Test FG Item 2")
|
||||
self.assertIsNotNone(
|
||||
self.find_row(data, wo_order.name),
|
||||
"Filter bug regressed/fixed: `item` filter now takes effect - update this test",
|
||||
)
|
||||
# a matching production item includes the row, a non-matching one excludes it
|
||||
self.assertIsNotNone(self.find_row(self.run_report(item="_Test FG Item"), wo_order.name))
|
||||
self.assertIsNone(self.find_row(self.run_report(item="_Test FG Item 2"), wo_order.name))
|
||||
|
||||
def test_work_order_filter_scopes_rows(self):
|
||||
wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4)
|
||||
|
||||
# the matching work order is included, a different work order name is excluded
|
||||
self.assertIsNotNone(self.find_row(self.run_report(work_order=wo_order.name), wo_order.name))
|
||||
self.assertIsNone(self.find_row(self.run_report(work_order=f"{wo_order.name}-XX"), wo_order.name))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import getdate, today
|
||||
from frappe.utils import get_datetime, getdate, today
|
||||
|
||||
from erpnext.stock.report.stock_analytics.stock_analytics import (
|
||||
get_period,
|
||||
@@ -31,7 +31,9 @@ def get_columns(period_columns):
|
||||
|
||||
def get_work_orders(filters):
|
||||
from_date = filters.get("from_date")
|
||||
to_date = filters.get("to_date")
|
||||
# `creation` and `actual_end_date` are datetime columns, so a bare date upper
|
||||
# bound would coerce to midnight and drop records created later on the last day.
|
||||
to_date = get_datetime(filters.get("to_date")).replace(hour=23, minute=59, second=59)
|
||||
|
||||
WorkOrder = frappe.qb.DocType("Work Order")
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_first_day, get_last_day, today
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProductionAnalytics(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
from erpnext.manufacturing.report.production_analytics.production_analytics import execute
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": get_first_day(today()),
|
||||
"to_date": get_last_day(today()),
|
||||
"range": "Monthly",
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
columns, data, _msg, _chart = execute(filters)
|
||||
return columns, data
|
||||
|
||||
def get_period_count(self, columns, data, status, period_label):
|
||||
"""Return the count for a status row under the period column resolved by label."""
|
||||
period_fieldname = next(col["fieldname"] for col in columns if col.get("label") == period_label)
|
||||
# the report stores the translated status label, so translate before matching
|
||||
row = next(row for row in data if row["status"] == _(status))
|
||||
return row[period_fieldname]
|
||||
|
||||
def test_submitted_work_order_increments_status_count(self):
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
|
||||
# pin the reporting window once so both runs use the same period even if the
|
||||
# test happens to straddle a month boundary
|
||||
from_date, to_date = get_first_day(today()), get_last_day(today())
|
||||
|
||||
# The current month is the period a newly created Work Order falls into (bucketed by creation date).
|
||||
cols_before, data_before = self.run_report(from_date=from_date, to_date=to_date)
|
||||
period_label = cols_before[-1]["label"]
|
||||
before = self.get_period_count(cols_before, data_before, "Not Started", period_label)
|
||||
|
||||
wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company")
|
||||
self.assertEqual(wo.docstatus, 1)
|
||||
# A freshly submitted Work Order with no material transfer has status "Not Started".
|
||||
self.assertEqual(wo.status, "Not Started")
|
||||
|
||||
cols_after, data_after = self.run_report(from_date=from_date, to_date=to_date)
|
||||
after = self.get_period_count(cols_after, data_after, "Not Started", period_label)
|
||||
|
||||
self.assertEqual(after, before + 1)
|
||||
|
||||
def test_report_shape(self):
|
||||
columns, data = self.run_report()
|
||||
|
||||
# First column is the Status column, followed by one column per period.
|
||||
self.assertEqual(columns[0]["fieldname"], "status")
|
||||
self.assertGreaterEqual(len(columns), 2)
|
||||
|
||||
# One row per known Work Order status.
|
||||
statuses = {row["status"] for row in data}
|
||||
for status in ("Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"):
|
||||
self.assertIn(_(status), statuses)
|
||||
@@ -42,7 +42,9 @@ def get_production_plan_item_details(filters, data, order_details):
|
||||
|
||||
order_qty = row.planned_qty
|
||||
total_produced_qty = 0.0
|
||||
pending_qty = 0.0
|
||||
# default to the full planned qty so a plan without any work order still
|
||||
# reports everything as pending rather than a misleading zero
|
||||
pending_qty = flt(order_qty)
|
||||
for work_order in work_orders:
|
||||
produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
|
||||
pending_qty = flt(order_qty) - produced_qty
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import create_production_plan
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_se_from_wo
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.production_plan_summary.production_plan_summary import execute
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProductionPlanSummary(ERPNextTestSuite):
|
||||
def run_report(self, production_plan):
|
||||
filters = frappe._dict({"production_plan": production_plan})
|
||||
return execute(filters)[1]
|
||||
|
||||
def make_plan(self, planned_qty=2):
|
||||
return create_production_plan(
|
||||
item_code="_Test FG Item",
|
||||
planned_qty=planned_qty,
|
||||
skip_getting_mr_items=1,
|
||||
)
|
||||
|
||||
def make_submitted_work_order(self, plan, qty):
|
||||
wo = make_wo_order_test_record(
|
||||
item_code="_Test FG Item",
|
||||
qty=qty,
|
||||
company=plan.company,
|
||||
wip_warehouse="Work In Progress - _TC",
|
||||
fg_warehouse="Finished Goods - _TC",
|
||||
skip_transfer=1,
|
||||
use_multi_level_bom=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
wo.production_plan = plan.name
|
||||
wo.production_plan_item = plan.po_items[0].name
|
||||
wo.submit()
|
||||
return wo
|
||||
|
||||
def stock_required_materials(self, wo):
|
||||
# make sure every raw material is available in its source warehouse before manufacturing,
|
||||
# otherwise a clean database raises NegativeStockError
|
||||
for item in wo.required_items:
|
||||
make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
to_warehouse=item.source_warehouse or "_Test Warehouse - _TC",
|
||||
qty=item.required_qty + 10,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
def get_work_order_row(self, data, item_code):
|
||||
for row in data:
|
||||
if row.get("item_code") == item_code and row.get("document_type") == "Work Order":
|
||||
return row
|
||||
return None
|
||||
|
||||
def get_summary_row(self, data, item_code):
|
||||
for row in data:
|
||||
if row.get("item_code") == item_code and not row.get("document_type"):
|
||||
return row
|
||||
return None
|
||||
|
||||
def test_summary_without_work_order(self):
|
||||
"""A submitted plan with no work order still yields a summary row for the planned item."""
|
||||
plan = self.make_plan(planned_qty=2)
|
||||
|
||||
data = self.run_report(plan.name)
|
||||
summary = self.get_summary_row(data, "_Test FG Item")
|
||||
|
||||
self.assertIsNotNone(summary)
|
||||
self.assertEqual(summary.get("qty"), 2)
|
||||
self.assertEqual(summary.get("produced_qty"), 0)
|
||||
# nothing produced yet, so the whole planned qty is pending
|
||||
self.assertEqual(summary.get("pending_qty"), 2)
|
||||
self.assertIsNone(self.get_work_order_row(data, "_Test FG Item"))
|
||||
|
||||
def test_summary_with_pending_work_order(self):
|
||||
"""An unproduced work order shows full planned qty as pending."""
|
||||
plan = self.make_plan(planned_qty=2)
|
||||
wo = self.make_submitted_work_order(plan, qty=2)
|
||||
|
||||
data = self.run_report(plan.name)
|
||||
wo_row = self.get_work_order_row(data, "_Test FG Item")
|
||||
|
||||
self.assertIsNotNone(wo_row)
|
||||
self.assertEqual(wo_row.get("document_name"), wo.name)
|
||||
self.assertEqual(wo_row.get("qty"), 2)
|
||||
self.assertEqual(wo_row.get("produced_qty"), 0)
|
||||
self.assertEqual(wo_row.get("pending_qty"), 2)
|
||||
|
||||
summary = self.get_summary_row(data, "_Test FG Item")
|
||||
self.assertEqual(summary.get("qty"), 2)
|
||||
self.assertEqual(summary.get("produced_qty"), 0)
|
||||
|
||||
def test_summary_reflects_produced_qty(self):
|
||||
"""Producing part of the work order updates produced and pending quantities."""
|
||||
plan = self.make_plan(planned_qty=2)
|
||||
wo = self.make_submitted_work_order(plan, qty=2)
|
||||
self.stock_required_materials(wo)
|
||||
|
||||
se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
|
||||
se.submit()
|
||||
|
||||
data = self.run_report(plan.name)
|
||||
wo_row = self.get_work_order_row(data, "_Test FG Item")
|
||||
|
||||
self.assertEqual(wo_row.get("document_name"), wo.name)
|
||||
self.assertEqual(wo_row.get("produced_qty"), 1)
|
||||
self.assertEqual(wo_row.get("pending_qty"), 1)
|
||||
|
||||
summary = self.get_summary_row(data, "_Test FG Item")
|
||||
self.assertEqual(summary.get("qty"), 2)
|
||||
self.assertEqual(summary.get("produced_qty"), 1)
|
||||
self.assertEqual(summary.get("pending_qty"), 1)
|
||||
|
||||
def test_summary_scoped_to_its_own_plan(self):
|
||||
"""Each plan's report only reports its own work order documents."""
|
||||
plan_a = self.make_plan(planned_qty=2)
|
||||
wo_a = self.make_submitted_work_order(plan_a, qty=2)
|
||||
|
||||
plan_b = self.make_plan(planned_qty=3)
|
||||
wo_b = self.make_submitted_work_order(plan_b, qty=3)
|
||||
|
||||
data_a = self.run_report(plan_a.name)
|
||||
document_names = {row.get("document_name") for row in data_a if row.get("document_name")}
|
||||
|
||||
self.assertIn(wo_a.name, document_names)
|
||||
self.assertNotIn(wo_b.name, document_names)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.manufacturing.report.quality_inspection_summary.quality_inspection_summary import execute
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
make_minimal_job_card,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestQualityInspectionSummary(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
create_item("_Test Item")
|
||||
self.job_card = make_minimal_job_card(production_item="_Test Item")
|
||||
self.qi = create_quality_inspection(
|
||||
item_code="_Test Item",
|
||||
reference_type="Job Card",
|
||||
reference_name=self.job_card,
|
||||
status="Accepted",
|
||||
)
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def _rows_for_qi(self, data):
|
||||
return [row for row in data if row.get("name") == self.qi.name]
|
||||
|
||||
def test_appears_in_date_range(self):
|
||||
data = self.run_report(from_date=add_days(nowdate(), -1), to_date=add_days(nowdate(), 1))
|
||||
rows = self._rows_for_qi(data)
|
||||
self.assertEqual(len(rows), 1)
|
||||
|
||||
row = rows[0]
|
||||
self.assertEqual(row["status"], "Accepted")
|
||||
self.assertEqual(row["item_code"], "_Test Item")
|
||||
self.assertEqual(row["reference_type"], "Job Card")
|
||||
self.assertEqual(row["reference_name"], self.job_card)
|
||||
|
||||
def test_excluded_outside_date_range(self):
|
||||
data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5))
|
||||
self.assertEqual(self._rows_for_qi(data), [])
|
||||
|
||||
def test_status_filter_includes_matching(self):
|
||||
data = self.run_report(
|
||||
from_date=add_days(nowdate(), -1),
|
||||
to_date=add_days(nowdate(), 1),
|
||||
status=["Accepted"],
|
||||
)
|
||||
self.assertEqual(len(self._rows_for_qi(data)), 1)
|
||||
|
||||
def test_status_filter_excludes_non_matching(self):
|
||||
data = self.run_report(
|
||||
from_date=add_days(nowdate(), -1),
|
||||
to_date=add_days(nowdate(), 1),
|
||||
status=["Rejected"],
|
||||
)
|
||||
self.assertEqual(self._rows_for_qi(data), [])
|
||||
|
||||
def test_item_code_filter_includes_matching(self):
|
||||
data = self.run_report(
|
||||
from_date=add_days(nowdate(), -1),
|
||||
to_date=add_days(nowdate(), 1),
|
||||
item_code=["_Test Item"],
|
||||
)
|
||||
self.assertEqual(len(self._rows_for_qi(data)), 1)
|
||||
|
||||
def test_item_code_filter_excludes_other_item(self):
|
||||
other_item = frappe.generate_hash(length=10)
|
||||
data = self.run_report(
|
||||
from_date=add_days(nowdate(), -1),
|
||||
to_date=add_days(nowdate(), 1),
|
||||
item_code=[other_item],
|
||||
)
|
||||
self.assertEqual(self._rows_for_qi(data), [])
|
||||
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.work_order_consumed_materials.work_order_consumed_materials import execute
|
||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestWorkOrderConsumedMaterials(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": add_days(nowdate(), -1),
|
||||
"to_date": add_days(nowdate(), 1),
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def make_manufactured_work_order(self, qty=2):
|
||||
"""Create a submitted WO, stock its raw materials, transfer and fully manufacture it."""
|
||||
wo = make_wo_order_test_record(production_item="_Test FG Item", qty=qty, company="_Test Company")
|
||||
|
||||
for item in wo.required_items:
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target=wo.wip_warehouse,
|
||||
qty=item.required_qty,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", qty))
|
||||
transfer.insert()
|
||||
transfer.submit()
|
||||
|
||||
manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", qty))
|
||||
manufacture.insert()
|
||||
manufacture.submit()
|
||||
|
||||
wo.reload()
|
||||
return wo
|
||||
|
||||
def get_wo_rows(self, data, work_order):
|
||||
"""The report blanks parent fields after the first raw-material row, so match by raw
|
||||
material's parent work order instead of the (blanked) `name` column."""
|
||||
return [row for row in data if row.get("parent") == work_order]
|
||||
|
||||
def test_consumed_materials_reported_after_manufacture(self):
|
||||
wo = self.make_manufactured_work_order(qty=2)
|
||||
|
||||
# fully producing the WO consumes exactly the required quantity of each raw material
|
||||
self.assertEqual(wo.produced_qty, 2)
|
||||
|
||||
data = self.run_report()
|
||||
rows = self.get_wo_rows(data, wo.name)
|
||||
|
||||
self.assertEqual(len(rows), len(wo.required_items))
|
||||
|
||||
# pair rows to required items by sorting rather than a dict keyed on item code, so
|
||||
# a BOM with two lines for the same component wouldn't silently collapse to one row
|
||||
rows_sorted = sorted(rows, key=lambda r: (r["raw_material_item_code"], r["required_qty"]))
|
||||
items_sorted = sorted(wo.required_items, key=lambda i: (i.item_code, i.required_qty))
|
||||
for row, item in zip(rows_sorted, items_sorted, strict=True):
|
||||
self.assertEqual(row["raw_material_item_code"], item.item_code)
|
||||
self.assertEqual(row["required_qty"], item.required_qty)
|
||||
self.assertEqual(row["transferred_qty"], item.required_qty)
|
||||
self.assertEqual(row["consumed_qty"], item.required_qty)
|
||||
# no over-consumption in a clean full manufacture
|
||||
self.assertEqual(row["extra_consumed_qty"], 0.0)
|
||||
self.assertEqual(row["returned_qty"], 0.0)
|
||||
|
||||
# parent columns are populated on the first row only
|
||||
first = rows[0]
|
||||
self.assertEqual(first["status"], wo.status)
|
||||
self.assertEqual(first["production_item"], "_Test FG Item")
|
||||
self.assertEqual(first["qty"], 2)
|
||||
self.assertEqual(first["produced_qty"], 2)
|
||||
|
||||
def test_work_order_filter_scopes_output(self):
|
||||
wo = self.make_manufactured_work_order(qty=1)
|
||||
|
||||
data = self.run_report(name=wo.name)
|
||||
|
||||
parents = {row.get("parent") for row in data}
|
||||
self.assertEqual(parents, {wo.name})
|
||||
self.assertTrue(data)
|
||||
|
||||
def test_draft_work_order_is_excluded(self):
|
||||
# report only lists WOs in status In Process / Completed / Stopped
|
||||
draft = make_wo_order_test_record(
|
||||
production_item="_Test FG Item", qty=1, company="_Test Company", do_not_submit=True
|
||||
)
|
||||
|
||||
data = self.run_report()
|
||||
self.assertNotIn(draft.name, {row.get("parent") for row in data})
|
||||
|
||||
def test_date_range_filter_excludes_work_order(self):
|
||||
wo = self.make_manufactured_work_order(qty=1)
|
||||
|
||||
# positive anchor: the WO shows up within the default (current) window
|
||||
self.assertIn(wo.name, {row.get("parent") for row in self.run_report()})
|
||||
|
||||
# a window that ends before the WO was created must not include it
|
||||
data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5))
|
||||
self.assertNotIn(wo.name, {row.get("parent") for row in data})
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.work_order_summary.work_order_summary import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestWorkOrderSummary(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": add_days(today(), -1),
|
||||
"to_date": today(),
|
||||
}
|
||||
)
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_work_order_appears_with_expected_fields(self):
|
||||
wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company")
|
||||
|
||||
rows = {row["name"]: row for row in self.run_report()}
|
||||
self.assertIn(wo.name, rows)
|
||||
|
||||
row = rows[wo.name]
|
||||
self.assertEqual(row["production_item"], "_Test FG Item")
|
||||
self.assertEqual(row["qty"], 10)
|
||||
self.assertEqual(row["produced_qty"], 0)
|
||||
self.assertEqual(row["status"], "Not Started")
|
||||
|
||||
def test_status_filter_excludes_other_statuses(self):
|
||||
wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company")
|
||||
self.assertEqual(wo.status, "Not Started")
|
||||
|
||||
# A "Completed" filter must not return a "Not Started" work order.
|
||||
names = {row["name"] for row in self.run_report(status="Completed")}
|
||||
self.assertNotIn(wo.name, names)
|
||||
|
||||
# The matching status still returns it.
|
||||
names = {row["name"] for row in self.run_report(status="Not Started")}
|
||||
self.assertIn(wo.name, names)
|
||||
|
||||
def test_date_range_excludes_work_order_outside_window(self):
|
||||
wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company")
|
||||
|
||||
# A window entirely in the past cannot contain a WO created today.
|
||||
names = {
|
||||
row["name"]
|
||||
for row in self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5))
|
||||
}
|
||||
self.assertNotIn(wo.name, names)
|
||||
|
||||
# A window that includes today does contain it.
|
||||
names = {row["name"] for row in self.run_report()}
|
||||
self.assertIn(wo.name, names)
|
||||
@@ -432,9 +432,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-05 11:00:26.131777",
|
||||
"modified": "2026-06-14 13:44:07.420267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"module_onboarding": "Manufacturing Onboarding",
|
||||
"name": "Manufacturing",
|
||||
"number_cards": [
|
||||
{
|
||||
@@ -458,6 +459,465 @@
|
||||
"roles": [],
|
||||
"sequence_id": 8.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Manufacturing",
|
||||
"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": "Manufacturing",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "list-tree",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM",
|
||||
"link_to": "BOM",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "factory",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Work Order",
|
||||
"link_to": "Work Order",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "person-standing",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Job Card",
|
||||
"link_to": "Job Card",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "stock",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Stock Entry",
|
||||
"link_to": "Stock Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Material Planning",
|
||||
"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 Lead Time",
|
||||
"link_to": "Item Lead Time",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Production Plan",
|
||||
"link_to": "Production Plan",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Forecasting",
|
||||
"link_to": "Exponential Smoothing Forecasting",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Master Production Schedule",
|
||||
"link_to": "Master Production Schedule",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Forecast",
|
||||
"link_to": "Sales Forecast",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Production Planning Report",
|
||||
"link_to": "Production Planning Report",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "tool",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Tools",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM Creator",
|
||||
"link_to": "BOM Creator",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM Update Tool",
|
||||
"link_to": "BOM Update Tool",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM Comparison Tool",
|
||||
"link_to": "bom-comparison-tool",
|
||||
"link_type": "Page",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Downtime Entry",
|
||||
"link_to": "Downtime Entry",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "notepad-text",
|
||||
"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": "Production Planning Report",
|
||||
"link_to": "Production Planning Report",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Work Order Summary",
|
||||
"link_to": "Work Order Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Inspection Summary",
|
||||
"link_to": "Quality Inspection Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Downtime Analysis",
|
||||
"link_to": "Downtime Analysis",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Job Card Summary",
|
||||
"link_to": "Job Card Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM Search",
|
||||
"link_to": "BOM Search",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Production Analytics",
|
||||
"link_to": "Production Analytics",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "BOM Operations Time",
|
||||
"link_to": "BOM Operations Time",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Work Order Consumed Materials",
|
||||
"link_to": "Work Order Consumed Materials",
|
||||
"link_type": "Report",
|
||||
"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": "Item",
|
||||
"link_to": "Item",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Warehouse",
|
||||
"link_to": "Warehouse",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Operation",
|
||||
"link_to": "Operation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Workstation",
|
||||
"link_to": "Workstation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Workstation Type",
|
||||
"link_to": "Workstation Type",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Plant Floor",
|
||||
"link_to": "Plant Floor",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Routing",
|
||||
"link_to": "Routing",
|
||||
"link_type": "DocType",
|
||||
"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": "Manufacturing Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Manufacturing",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ def complete_onboarding_steps_if_record_exists(steps):
|
||||
if (
|
||||
step.action == "Create Entry"
|
||||
and step.reference_document
|
||||
and frappe.db.exists("DocType", step.reference_document)
|
||||
and frappe.get_all(step.reference_document, limit=1)
|
||||
):
|
||||
frappe.db.set_value("Onboarding Step", step.name, "is_complete", 1, update_modified=False)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.projects.report.project_summary.project_summary import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProjectSummary(ERPNextTestSuite):
|
||||
"""Lists projects with their total / completed / overdue task counts."""
|
||||
|
||||
def make_project(self):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Project",
|
||||
"project_name": f"_Test PS {frappe.generate_hash(length=6)}",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def make_task(self, project, status="Open"):
|
||||
task = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Task",
|
||||
"subject": f"Task {frappe.generate_hash(length=6)}",
|
||||
"project": project.name,
|
||||
}
|
||||
).insert()
|
||||
if status != "Open":
|
||||
# set the status directly; the report counts tasks by their stored status
|
||||
frappe.db.set_value("Task", task.name, "status", status)
|
||||
return task
|
||||
|
||||
def run_report(self, project):
|
||||
return execute(frappe._dict({"name": project.name}))
|
||||
|
||||
def project_row(self, project):
|
||||
_columns, data, *_rest = self.run_report(project)
|
||||
return next((r for r in data if r["name"] == project.name), None)
|
||||
|
||||
def test_task_counts(self):
|
||||
project = self.make_project()
|
||||
self.make_task(project, "Completed")
|
||||
self.make_task(project, "Completed")
|
||||
self.make_task(project, "Open")
|
||||
self.make_task(project, "Overdue")
|
||||
|
||||
row = self.project_row(project)
|
||||
self.assertIsNotNone(row, "Project missing from report")
|
||||
self.assertEqual(row["total_tasks"], 4)
|
||||
self.assertEqual(row["completed_tasks"], 2)
|
||||
self.assertEqual(row["overdue_tasks"], 1)
|
||||
|
||||
def test_report_summary_totals(self):
|
||||
project = self.make_project()
|
||||
self.make_task(project, "Completed")
|
||||
self.make_task(project, "Open")
|
||||
|
||||
_columns, _data, _message, _chart, report_summary = self.run_report(project)
|
||||
summary = {s["label"]: s["value"] for s in report_summary}
|
||||
self.assertEqual(summary[_("Total Tasks")], 2)
|
||||
self.assertEqual(summary[_("Completed Tasks")], 1)
|
||||
self.assertEqual(summary[_("Overdue Tasks")], 0)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet
|
||||
from erpnext.projects.report.timesheet_billing_summary.timesheet_billing_summary import execute
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTimesheetBillingSummary(ERPNextTestSuite):
|
||||
"""Lists submitted Timesheet Detail rows with working/billing hours and amount,
|
||||
optionally grouped by date/project/employee."""
|
||||
|
||||
def setUp(self):
|
||||
self.employee = make_employee("timesheet_billing@example.com", company="_Test Company")
|
||||
self.project = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Project",
|
||||
"project_name": f"_Test TBS {frappe.generate_hash(length=6)}",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def make_ts(self, is_billable=1):
|
||||
return make_timesheet(
|
||||
self.employee, simulate=True, is_billable=is_billable, project=self.project.name
|
||||
)
|
||||
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"company": "_Test Company", "employee": self.employee})
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_billable_timesheet_row(self):
|
||||
ts = self.make_ts(is_billable=1)
|
||||
detail = ts.time_logs[0]
|
||||
|
||||
rows = [r for r in self.run_report() if r.get("timesheet") == ts.name]
|
||||
self.assertTrue(rows, "Timesheet missing from report")
|
||||
row = rows[0]
|
||||
self.assertEqual(row["hours"], 2)
|
||||
self.assertEqual(row["billing_hours"], detail.billing_hours)
|
||||
self.assertEqual(row["billing_amount"], detail.billing_amount)
|
||||
self.assertEqual(row["project"], self.project.name)
|
||||
|
||||
def test_group_by_project_sums_hours(self):
|
||||
self.make_ts(is_billable=1)
|
||||
|
||||
data = self.run_report(group_by="project")
|
||||
group_rows = [r for r in data if r.get("is_group") and r.get("project") == self.project.name]
|
||||
self.assertTrue(group_rows, "Grouped project row missing")
|
||||
self.assertEqual(group_rows[0]["hours"], 2)
|
||||
|
||||
def test_draft_excluded_unless_requested(self):
|
||||
ts = make_timesheet(
|
||||
self.employee, simulate=True, is_billable=1, project=self.project.name, do_not_submit=True
|
||||
)
|
||||
|
||||
# submitted-only by default: the draft timesheet is absent
|
||||
self.assertNotIn(ts.name, {r.get("timesheet") for r in self.run_report()})
|
||||
# ... but included when draft timesheets are requested
|
||||
self.assertIn(ts.name, {r.get("timesheet") for r in self.run_report(include_draft_timesheets=1)})
|
||||
@@ -18,6 +18,14 @@
|
||||
"is_hidden": 0,
|
||||
"label": "Projects",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Projects",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -37,6 +45,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Project",
|
||||
"link_count": 0,
|
||||
"link_to": "Project",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Task",
|
||||
"link_count": 0,
|
||||
"link_to": "Task",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -59,6 +89,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Project Template",
|
||||
"link_count": 0,
|
||||
"link_to": "Project Template",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -70,6 +111,28 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Project Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Project Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Project",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Project Update",
|
||||
"link_count": 0,
|
||||
"link_to": "Project Update",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Project",
|
||||
"hidden": 0,
|
||||
@@ -89,6 +152,14 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Time Tracking",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -100,6 +171,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Timesheet",
|
||||
"link_count": 0,
|
||||
"link_to": "Timesheet",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Activity Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -122,6 +215,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Activity Type",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity Cost",
|
||||
"link_count": 0,
|
||||
"link_to": "Activity Cost",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -130,6 +234,25 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Reports",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Timesheet",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Daily Timesheet Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Daily Timesheet Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Timesheet",
|
||||
"hidden": 0,
|
||||
@@ -152,6 +275,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Project",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Project wise Stock Tracking",
|
||||
"link_count": 0,
|
||||
"link_to": "Project wise Stock Tracking",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Project",
|
||||
"hidden": 0,
|
||||
@@ -163,6 +297,28 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Project",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Timesheet Billing Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Timesheet Billing Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Task",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Delayed Tasks Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Delayed Tasks Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Task",
|
||||
"hidden": 0,
|
||||
@@ -182,6 +338,24 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 1,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Projects Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "Projects Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -193,9 +367,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-02 17:26:44.644507",
|
||||
"modified": "2026-07-01 13:20:50.651608",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"module_onboarding": "Projects Onboarding",
|
||||
"name": "Projects",
|
||||
"number_cards": [
|
||||
{
|
||||
@@ -219,6 +394,250 @@
|
||||
"roles": [],
|
||||
"sequence_id": 11.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Projects",
|
||||
"link_type": "Workspace",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
"link_to": "Project",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "projects",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project",
|
||||
"link_to": "Project",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "list-todo",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Task",
|
||||
"link_to": "Task",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "calendar-clock",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Timesheet",
|
||||
"link_to": "Timesheet",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"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,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Activity Type",
|
||||
"link_to": "Activity Type",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Activity Cost",
|
||||
"link_to": "Activity Cost",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project Template",
|
||||
"link_to": "Project Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project Type",
|
||||
"link_to": "Project Type",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project Update",
|
||||
"link_to": "Project Update",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"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,
|
||||
"default_workspace": 0,
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project Summary",
|
||||
"link_to": "Project Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Daily Timesheet Summary",
|
||||
"link_to": "Daily Timesheet Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Timesheet Billing Summary",
|
||||
"link_to": "Timesheet Billing Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Project wise Stock Tracking",
|
||||
"link_to": "Project wise Stock Tracking",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Delayed Tasks Summary",
|
||||
"link_to": "Delayed Tasks Summary",
|
||||
"link_type": "Report",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "settings",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Settings",
|
||||
"link_to": "Projects Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Projects",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-02 17:32:47.522875",
|
||||
"modified": "2026-06-14 13:44:07.920643",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Quality Management",
|
||||
"name": "Quality",
|
||||
@@ -174,6 +174,161 @@
|
||||
"roles": [],
|
||||
"sequence_id": 9.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Quality",
|
||||
"link_type": "Workspace",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "inspection-panel",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Inspection",
|
||||
"link_to": "Quality Inspection",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "goal",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Goal",
|
||||
"link_to": "Quality Goal",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "review",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Review",
|
||||
"link_to": "Quality Review",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "square-activity",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Action",
|
||||
"link_to": "Quality Action",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "grid-2x2-check",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Non Conformance",
|
||||
"link_to": "Non Conformance",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "thumbs-up",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Feedback",
|
||||
"link_to": "Quality Feedback",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "users",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Meeting",
|
||||
"link_to": "Quality Meeting",
|
||||
"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,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Procedure",
|
||||
"link_to": "Quality Procedure",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Feedback Template",
|
||||
"link_to": "Quality Feedback Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Quality Inspection Template",
|
||||
"link_to": "Quality Inspection Template",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Quality",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ erpnext.company.setup_queries = function (frm) {
|
||||
],
|
||||
[
|
||||
"stock_delivered_but_not_billed",
|
||||
{ root_type: "Liability", account_type: "Stock Delivered But Not Billed" },
|
||||
{ root_type: "Asset", account_type: "Stock Delivered But Not Billed" },
|
||||
],
|
||||
[
|
||||
"service_received_but_not_billed",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"default_purchase_price_variance_account",
|
||||
"default_manufacturing_variance_account",
|
||||
"stock_received_but_not_billed",
|
||||
"enable_stock_delivered_but_not_billed",
|
||||
"stock_delivered_but_not_billed",
|
||||
"disable_sdbnb_in_sr",
|
||||
"default_provisional_account",
|
||||
@@ -353,33 +354,48 @@
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "round_off_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Round Off Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "round_off_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Round Off Cost Center",
|
||||
"no_copy": 1,
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "write_off_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Write Off Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "exchange_gain_loss_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Exchange Gain / Loss Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "unrealized_exchange_gain_loss_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Unrealized Exchange Gain/Loss Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -526,15 +542,19 @@
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "accumulated_depreciation_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Accumulated Depreciation Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "depreciation_expense_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Depreciation Expense Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
@@ -549,29 +569,39 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "disposal_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Gain/Loss Account on Asset Disposal",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "depreciation_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Asset Depreciation Cost Center",
|
||||
"no_copy": 1,
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "capital_work_in_progress_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Capital Work In Progress Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "asset_received_but_not_billed",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Asset Received But Not Billed",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -703,15 +733,21 @@
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "unrealized_profit_loss_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Unrealized Profit / Loss Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "default_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Default Payment Discount Account",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -753,8 +789,10 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_received_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Default Advance Received Account",
|
||||
"mandatory_depends_on": "book_advance_payments_as_liability",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -763,8 +801,10 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_paid_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Default Advance Paid Account",
|
||||
"mandatory_depends_on": "book_advance_payments_as_liability",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -844,9 +884,12 @@
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "round_off_for_opening",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Round Off for Opening",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -1006,18 +1049,28 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_stock_delivered_but_not_billed",
|
||||
"fieldname": "disable_sdbnb_in_sr",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Stock Delivered But Not Billed in Sales Return",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "enable_stock_delivered_but_not_billed",
|
||||
"fieldname": "stock_delivered_but_not_billed",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Stock Delivered But Not Billed",
|
||||
"mandatory_depends_on": "enable_stock_delivered_but_not_billed",
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the value of goods delivered before invoicing will be recorded in the Stock Delivered But Not Billed account.",
|
||||
"fieldname": "enable_stock_delivered_but_not_billed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Stock Delivered But Not Billed"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1026,7 +1079,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-01 11:48:07.853494",
|
||||
"modified": "2026-07-02 07:21:21.794533",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
||||
@@ -84,6 +84,7 @@ class Company(NestedSet):
|
||||
default_operating_cost_account: DF.Link | None
|
||||
default_payable_account: DF.Link | None
|
||||
default_provisional_account: DF.Link | None
|
||||
default_purchase_price_variance_account: DF.Link | None
|
||||
default_receivable_account: DF.Link | None
|
||||
default_sales_contact: DF.Link | None
|
||||
default_scrap_warehouse: DF.Link | None
|
||||
@@ -99,6 +100,7 @@ class Company(NestedSet):
|
||||
enable_item_wise_inventory_account: DF.Check
|
||||
enable_perpetual_inventory: DF.Check
|
||||
enable_provisional_accounting_for_non_stock_items: DF.Check
|
||||
enable_stock_delivered_but_not_billed: DF.Check
|
||||
exception_budget_approver_role: DF.Link | None
|
||||
exchange_gain_loss_account: DF.Link | None
|
||||
existing_company: DF.Link | None
|
||||
@@ -185,6 +187,64 @@ class Company(NestedSet):
|
||||
self.validate_inventory_account_settings()
|
||||
self.cant_change_valuation_method()
|
||||
self.validate_pending_reposts(old_doc)
|
||||
self.validate_sdbnb_configuration()
|
||||
|
||||
def validate_outstanding_sdbnb_transactions(self, account):
|
||||
GLEntry = frappe.qb.DocType("GL Entry")
|
||||
DeliveryNote = frappe.qb.DocType("Delivery Note")
|
||||
|
||||
delivery_notes = (
|
||||
frappe.qb.from_(GLEntry)
|
||||
.join(DeliveryNote)
|
||||
.on((GLEntry.voucher_type == "Delivery Note") & (GLEntry.voucher_no == DeliveryNote.name))
|
||||
.select(DeliveryNote.name)
|
||||
.where(
|
||||
(GLEntry.is_cancelled == 0)
|
||||
& (GLEntry.company == self.name)
|
||||
& (GLEntry.account == account)
|
||||
& (DeliveryNote.per_billed < 100)
|
||||
& (DeliveryNote.docstatus == 1)
|
||||
& (DeliveryNote.status.isin(["To Bill", "Partially Billed"]))
|
||||
)
|
||||
.distinct()
|
||||
.run(pluck=True)
|
||||
)
|
||||
|
||||
if delivery_notes:
|
||||
dn_links = ", ".join(get_link_to_form("Delivery Note", dn) for dn in delivery_notes[:10])
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"Stock Delivered But Not Billed Account cannot be changed or disabled since account {0} contains outstanding Delivery Notes: {1}"
|
||||
).format(
|
||||
bold(account),
|
||||
dn_links,
|
||||
)
|
||||
)
|
||||
|
||||
def validate_sdbnb_configuration(self):
|
||||
if self.get("__islocal"):
|
||||
return
|
||||
|
||||
if self.enable_stock_delivered_but_not_billed and not self.stock_delivered_but_not_billed:
|
||||
frappe.throw(_("Please select Stock Delivered But Not Billed Account"))
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
if not (doc_before_save and doc_before_save.stock_delivered_but_not_billed):
|
||||
return
|
||||
|
||||
account_changed = (
|
||||
self.stock_delivered_but_not_billed != doc_before_save.stock_delivered_but_not_billed
|
||||
)
|
||||
|
||||
feature_disabled = (
|
||||
doc_before_save.enable_stock_delivered_but_not_billed
|
||||
and not self.enable_stock_delivered_but_not_billed
|
||||
)
|
||||
|
||||
if account_changed or feature_disabled:
|
||||
self.validate_outstanding_sdbnb_transactions(doc_before_save.stock_delivered_but_not_billed)
|
||||
|
||||
def cant_change_valuation_method(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
frappe.listview_settings["Company"] = {
|
||||
onload() {
|
||||
frappe.breadcrumbs.add("Accounts");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,11 @@ from frappe.utils import random_string
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
|
||||
get_charts_for_country,
|
||||
)
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.setup.doctype.company.company import get_default_company_address
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -234,6 +238,44 @@ class TestCompany(ERPNextTestSuite):
|
||||
after = get_all_transactions_annual_history(company).get(key, 0)
|
||||
self.assertEqual(after - before, 2)
|
||||
|
||||
def test_sdbnb_validation_requires_account_when_enabled(self):
|
||||
company = get_test_company()
|
||||
|
||||
company.enable_stock_delivered_but_not_billed = 1
|
||||
company.stock_delivered_but_not_billed = None
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
company.save()
|
||||
|
||||
def test_disable_sdbnb_with_outstanding_delivery_note_fails(self):
|
||||
company = get_test_company()
|
||||
|
||||
item_code = create_stock_item_with_inventory()
|
||||
create_outstanding_delivery_note(item_code)
|
||||
|
||||
company.enable_stock_delivered_but_not_billed = 0
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
company.save()
|
||||
|
||||
def test_cannot_change_sdbnb_account_with_outstanding_delivery_note(self):
|
||||
company = get_test_company()
|
||||
|
||||
item_code = create_stock_item_with_inventory()
|
||||
create_outstanding_delivery_note(item_code)
|
||||
|
||||
new_account = create_account(
|
||||
account_name="Stock Delivered But Not Billed - New",
|
||||
account_type="Stock Delivered But Not Billed",
|
||||
parent_account="Stock Assets - _TSDBNB",
|
||||
company=company.name,
|
||||
)
|
||||
|
||||
company.stock_delivered_but_not_billed = new_account
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
company.save()
|
||||
|
||||
def test_demo_data(self):
|
||||
from erpnext.setup.demo import clear_demo_data, setup_demo_data
|
||||
|
||||
@@ -297,3 +339,49 @@ def create_test_lead_in_company(company):
|
||||
lead.company = company
|
||||
lead.save()
|
||||
return lead.name
|
||||
|
||||
|
||||
def get_test_company():
|
||||
if frappe.db.exists("Company", "_Test SDBNB Company"):
|
||||
return frappe.get_doc("Company", "_Test SDBNB Company")
|
||||
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "_Test SDBNB Company",
|
||||
"abbr": "_TSDBNB",
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"enable_perpetual_inventory": 1,
|
||||
"enable_stock_delivered_but_not_billed": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_stock_item_with_inventory():
|
||||
item_code = make_item(
|
||||
"SDBNB Test Item",
|
||||
properties={"is_stock_item": 1},
|
||||
).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="Stores - _TSDBNB",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
company="_Test SDBNB Company",
|
||||
)
|
||||
|
||||
return item_code
|
||||
|
||||
|
||||
def create_outstanding_delivery_note(item_code):
|
||||
return create_delivery_note(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=150,
|
||||
company="_Test SDBNB Company",
|
||||
warehouse="Stores - _TSDBNB",
|
||||
cost_center="Main - _TSDBNB",
|
||||
expense_account="Stock Delivered But Not Billed - _TSDBNB",
|
||||
)
|
||||
|
||||
@@ -223,5 +223,17 @@
|
||||
"doctype": "Company",
|
||||
"chart_of_accounts": "Standard",
|
||||
"create_chart_of_accounts_based_on": "Standard Template"
|
||||
},
|
||||
{
|
||||
"abbr": "_TSDBNB",
|
||||
"company_name": "_Test SDBNB Company",
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"doctype": "Company",
|
||||
"domain": "Manufacturing",
|
||||
"chart_of_accounts": "Standard",
|
||||
"default_holiday_list": "_Test Holiday List",
|
||||
"enable_perpetual_inventory": 1,
|
||||
"enable_stock_delivered_but_not_billed": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -69,7 +69,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-09 13:05:08.007297",
|
||||
"modified": "2026-06-14 13:43:50.429297",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "ERPNext Settings",
|
||||
@@ -128,6 +128,236 @@
|
||||
"type": "DocType"
|
||||
}
|
||||
],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "earth",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Global Defaults",
|
||||
"link_to": "Global Defaults",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "washing-machine",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "System Settings",
|
||||
"link_to": "System Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "accounting",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Settings",
|
||||
"link_to": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "computer",
|
||||
"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": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "sell",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Selling Settings",
|
||||
"link_to": "Selling Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "buying",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Buying Settings",
|
||||
"link_to": "Buying Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "stock",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Stock Settings",
|
||||
"link_to": "Stock Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "building-2",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Manufacturing Settings",
|
||||
"link_to": "Manufacturing Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "projects",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Projects Settings",
|
||||
"link_to": "Projects Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "crm",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "CRM Settings",
|
||||
"link_to": "CRM Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "support",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Support Settings",
|
||||
"link_to": "Support Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Other Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Subscription Settings",
|
||||
"link_to": "Subscription Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Item Variant Settings",
|
||||
"link_to": "Item Variant Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Delivery Settings",
|
||||
"link_to": "Delivery Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Currency Exchange Settings",
|
||||
"link_to": "Currency Exchange Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Appointment Booking Settings",
|
||||
"link_to": "Appointment Booking Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Stock Reposting Settings",
|
||||
"link_to": "Stock Reposting Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "ERPNext Settings",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[{\"id\":\"aCk49ShVRs\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"WU4F-HUcIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
|
||||
"content": "[{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
|
||||
"creation": "2020-01-23 13:46:38.833076",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -13,6 +13,14 @@
|
||||
"is_hidden": 0,
|
||||
"label": "Home",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Accounting",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -32,6 +40,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Chart of Accounts",
|
||||
"link_count": 0,
|
||||
"link_to": "Account",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Company",
|
||||
"link_count": 0,
|
||||
"link_to": "Company",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -54,6 +84,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Customer",
|
||||
"link_count": 0,
|
||||
"link_to": "Customer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Supplier",
|
||||
"link_count": 0,
|
||||
"link_to": "Supplier",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -73,6 +125,14 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Stock",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -84,6 +144,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Item",
|
||||
"link_count": 0,
|
||||
"link_to": "Item",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Warehouse",
|
||||
"link_count": 0,
|
||||
"link_to": "Warehouse",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -106,6 +188,17 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Brand",
|
||||
"link_count": 0,
|
||||
"link_to": "Brand",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -117,6 +210,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Unit of Measure (UOM)",
|
||||
"link_count": 0,
|
||||
"link_to": "UOM",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Stock Reconciliation",
|
||||
"link_count": 0,
|
||||
"link_to": "Stock Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -136,6 +251,25 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "CRM",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lead",
|
||||
"link_count": 0,
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -158,6 +292,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Customer Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Customer Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Territory",
|
||||
"link_count": 0,
|
||||
"link_to": "Territory",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -177,6 +333,14 @@
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Data Import and Settings",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -188,6 +352,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Import Data",
|
||||
"link_count": 0,
|
||||
"link_to": "Data Import",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Opening Invoice Creation Tool",
|
||||
"link_count": 0,
|
||||
"link_to": "Opening Invoice Creation Tool",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -210,6 +396,28 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Chart of Accounts Importer",
|
||||
"link_count": 0,
|
||||
"link_to": "Chart of Accounts Importer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Letter Head",
|
||||
"link_count": 0,
|
||||
"link_to": "Letter Head",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -221,6 +429,17 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Account",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Account",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -233,7 +452,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-02 14:12:28.407612",
|
||||
"modified": "2026-07-01 14:22:16.927245",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Home",
|
||||
@@ -267,6 +486,74 @@
|
||||
"type": "DocType"
|
||||
}
|
||||
],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Item",
|
||||
"link_to": "Item",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Home",
|
||||
"link_type": "Workspace",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer",
|
||||
"link_to": "Customer",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Supplier",
|
||||
"link_to": "Supplier",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Home",
|
||||
"type": "Workspace"
|
||||
}
|
||||
|
||||
204
erpnext/setup/workspace/organization/organization.json
Normal file
204
erpnext/setup/workspace/organization/organization.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"allowed_users": [
|
||||
{
|
||||
"user": "Administrator"
|
||||
},
|
||||
{
|
||||
"user": "Guest"
|
||||
},
|
||||
{
|
||||
"user": "accounts@test.com"
|
||||
},
|
||||
{
|
||||
"user": "ankush@erpnext.com"
|
||||
},
|
||||
{
|
||||
"user": "faris@erpnext.com"
|
||||
},
|
||||
{
|
||||
"user": "mention_test_user@example.com"
|
||||
},
|
||||
{
|
||||
"user": "project@frappe.io"
|
||||
},
|
||||
{
|
||||
"user": "rushabh@erpnext.com"
|
||||
},
|
||||
{
|
||||
"user": "saqib@erpnext.com"
|
||||
},
|
||||
{
|
||||
"user": "soham@frappe.io"
|
||||
},
|
||||
{
|
||||
"user": "sohamengineer123@gmail.com"
|
||||
},
|
||||
{
|
||||
"user": "sohamkulkarns9@gmail.com"
|
||||
},
|
||||
{
|
||||
"user": "sydel@frappe.io"
|
||||
},
|
||||
{
|
||||
"user": "test'5@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test1@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test2@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test3@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test4@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test@example.com"
|
||||
},
|
||||
{
|
||||
"user": "test@portal.com"
|
||||
},
|
||||
{
|
||||
"user": "testpassword@example.com"
|
||||
},
|
||||
{
|
||||
"user": "testperm@example.com"
|
||||
},
|
||||
{
|
||||
"user": "web@web.com"
|
||||
}
|
||||
],
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"creation": "2026-06-11 11:51:21.789012",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "organization",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Organization",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-06-16 00:45:57.595188",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"module_onboarding": "Organization Onboarding",
|
||||
"name": "Organization",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"roles": [],
|
||||
"sequence_id": 46.0,
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 1,
|
||||
"icon": "organization",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Company",
|
||||
"link_to": "Company",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "book-text",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Letter Head",
|
||||
"link_to": "Letter Head",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "file-user",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Department",
|
||||
"link_to": "Department",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "book-user",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Branch",
|
||||
"link_to": "Branch",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "users",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "User",
|
||||
"link_to": "User",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "user-round-check",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Role Permissions",
|
||||
"link_to": "permission-manager",
|
||||
"link_type": "Page",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "mail",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Email Account",
|
||||
"link_to": "Email Account",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"standard": 1,
|
||||
"title": "Organization",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -426,6 +426,7 @@ class DeliveryNote(SellingController):
|
||||
"stock_delivered_but_not_billed",
|
||||
"disable_sdbnb_in_sr",
|
||||
"default_expense_account",
|
||||
"enable_stock_delivered_but_not_billed",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -433,7 +434,7 @@ class DeliveryNote(SellingController):
|
||||
sdbnb_account = company_values.stock_delivered_but_not_billed
|
||||
disable_sdbnb_in_sr = company_values.disable_sdbnb_in_sr
|
||||
default_expense_account = company_values.default_expense_account
|
||||
|
||||
is_enabled_sdbnb = company_values.enable_stock_delivered_but_not_billed
|
||||
for item in self.items:
|
||||
if item.get("against_sales_invoice"):
|
||||
if sdbnb_account and item.expense_account == sdbnb_account:
|
||||
@@ -447,14 +448,16 @@ class DeliveryNote(SellingController):
|
||||
# Only stock items
|
||||
if is_stock_item and not item.get("is_fixed_asset") and not item.get("is_subcontracted"):
|
||||
# Sales Return handling
|
||||
if self.is_return and disable_sdbnb_in_sr:
|
||||
if self.is_return and disable_sdbnb_in_sr and sdbnb_account and is_enabled_sdbnb:
|
||||
if default_expense_account and (
|
||||
not item.expense_account or item.expense_account == sdbnb_account
|
||||
):
|
||||
item.expense_account = default_expense_account
|
||||
|
||||
elif sdbnb_account:
|
||||
elif sdbnb_account and is_enabled_sdbnb:
|
||||
item.expense_account = sdbnb_account
|
||||
elif sdbnb_account and item.expense_account == sdbnb_account:
|
||||
item.expense_account = default_expense_account
|
||||
if not item.expense_account and default_expense_account:
|
||||
item.expense_account = default_expense_account
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestDeliveryNote(ERPNextTestSuite):
|
||||
self.load_test_records("Stock Entry")
|
||||
|
||||
def get_perpetual_defaults(self):
|
||||
company = frappe.get_doc("Company", "_Test Company with perpetual inventory")
|
||||
company = frappe.get_doc("Company", "_Test SDBNB Company")
|
||||
self.perpetual_company = company.name
|
||||
self.perpetual_account = company.stock_delivered_but_not_billed
|
||||
self.perpetual_cost_center = company.cost_center
|
||||
|
||||
@@ -239,6 +239,7 @@ class Item(Document):
|
||||
self.validate_item_defaults()
|
||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||
self.cant_change()
|
||||
self.validate_serialized_change_with_bundle()
|
||||
self.validate_standard_cost_change()
|
||||
self.validate_item_tax_net_rate_range()
|
||||
|
||||
@@ -1130,6 +1131,25 @@ class Item(Document):
|
||||
|
||||
frappe.throw(msg, title=_("Linked with submitted documents"))
|
||||
|
||||
def validate_serialized_change_with_bundle(self):
|
||||
"""Block turning a serialized item non-serialized while any Serial and Batch Bundle still exists
|
||||
for it. Such bundles carry the item's serial numbers; the user must delete or cancel them first."""
|
||||
if self.is_new() or self.has_serial_no or not self._doc_before_save:
|
||||
return
|
||||
|
||||
# Only relevant when the item was serialized before and is now being unset.
|
||||
if not self._doc_before_save.has_serial_no:
|
||||
return
|
||||
|
||||
# Draft (docstatus 0) or submitted (docstatus 1) bundles block the change; cancelled ones don't.
|
||||
if frappe.db.count("Serial and Batch Bundle", {"item_code": self.name, "docstatus": ("<", 2)}):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first."
|
||||
).format(frappe.bold(self.name)),
|
||||
title=_("Serial and Batch Bundle Exists"),
|
||||
)
|
||||
|
||||
def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None:
|
||||
linked_doctypes = [
|
||||
"Delivery Note Item",
|
||||
|
||||
@@ -1120,6 +1120,47 @@ class TestItem(ERPNextTestSuite):
|
||||
sabb_qty = frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "total_qty")
|
||||
self.assertEqual(abs(sabb_qty), properties["opening_stock"])
|
||||
|
||||
def test_cannot_unset_serialized_while_bundle_exists(self):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
make_serial_batch_bundle,
|
||||
)
|
||||
|
||||
item = make_item(
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TSN-UNSET-.####"}
|
||||
).name
|
||||
|
||||
serial_no = f"{item}-SN-01"
|
||||
frappe.get_doc(
|
||||
{"doctype": "Serial No", "serial_no": serial_no, "item_code": item, "company": "_Test Company"}
|
||||
).insert()
|
||||
|
||||
# A draft (unsubmitted) Serial and Batch Bundle for the item must block the change.
|
||||
bundle = make_serial_batch_bundle(
|
||||
{
|
||||
"item_code": item,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"company": "_Test Company",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"voucher_type": "Stock Entry",
|
||||
"serial_nos": [serial_no],
|
||||
"type_of_transaction": "Inward",
|
||||
"do_not_submit": True,
|
||||
"ignore_sabb_validation": True,
|
||||
}
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Item", item)
|
||||
doc.has_serial_no = 0
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
# Once the bundle is removed, the item can be made non-serialized.
|
||||
frappe.delete_doc("Serial and Batch Bundle", bundle.name, force=True)
|
||||
doc = frappe.get_doc("Item", item)
|
||||
doc.has_serial_no = 0
|
||||
doc.save()
|
||||
self.assertEqual(frappe.db.get_value("Item", item, "has_serial_no"), 0)
|
||||
|
||||
|
||||
def set_item_variant_settings(fields):
|
||||
doc = frappe.get_doc("Item Variant Settings")
|
||||
|
||||
@@ -63,6 +63,13 @@ class PurchaseReceiptStockReservation:
|
||||
return
|
||||
|
||||
production_plan_references = self.get_production_plan_references()
|
||||
if not production_plan_references:
|
||||
return
|
||||
|
||||
reservable_plans = self.get_reservable_production_plans(production_plan_references)
|
||||
if not reservable_plans:
|
||||
return
|
||||
|
||||
production_plan_items = []
|
||||
doc.reload()
|
||||
|
||||
@@ -70,6 +77,9 @@ class PurchaseReceiptStockReservation:
|
||||
for row in doc.items:
|
||||
if row.material_request_item and row.material_request_item in production_plan_references:
|
||||
_ref = production_plan_references[row.material_request_item]
|
||||
if _ref.production_plan not in reservable_plans:
|
||||
continue
|
||||
|
||||
docnames.append(_ref.production_plan)
|
||||
row.update(
|
||||
{
|
||||
@@ -95,6 +105,25 @@ class PurchaseReceiptStockReservation:
|
||||
docnames, from_doctype="Production Plan", to_doctype="Work Order"
|
||||
)
|
||||
|
||||
def get_reservable_production_plans(self, production_plan_references: frappe._dict) -> set:
|
||||
"""Production Plans that opted into stock reservation (``reserve_stock``).
|
||||
|
||||
A Production Plan only gets this flag set if "Auto Reserve Stock" was enabled in
|
||||
Stock Settings when it was created, or the user ticked "Reserve Stock" manually.
|
||||
Without this check, a Purchase Receipt would auto-reserve stock for every
|
||||
Production Plan whenever "Enable Stock Reservation" is on, ignoring both of those.
|
||||
"""
|
||||
plan_names = {ref.production_plan for ref in production_plan_references.values()}
|
||||
return {
|
||||
p.name
|
||||
for p in frappe.get_all(
|
||||
"Production Plan",
|
||||
filters={"name": ["in", list(plan_names)]},
|
||||
fields=["name", "reserve_stock"],
|
||||
)
|
||||
if p.reserve_stock
|
||||
}
|
||||
|
||||
def get_production_plan_references(self) -> frappe._dict:
|
||||
production_plan_references = frappe._dict()
|
||||
material_request_items = []
|
||||
|
||||
@@ -425,10 +425,10 @@ def repost(doc):
|
||||
if isinstance(message, dict):
|
||||
message = message.get("message")
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
|
||||
status = "In Progress"
|
||||
# Recoverable errors (deadlock, lock/query timeout, job timeout) re-queue as In Progress.
|
||||
# Classify by type: the old traceback string-match only knew MariaDB's "Deadlock found" and
|
||||
# missed Postgres deadlocks ("deadlock detected"), failing them permanently.
|
||||
status = "In Progress" if isinstance(e, RecoverableErrors) else "Failed"
|
||||
|
||||
if traceback:
|
||||
message += "<br><br>" + "<b>Traceback:</b> <br>" + traceback
|
||||
@@ -447,7 +447,8 @@ def repost(doc):
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
# status == "Failed" already implies e is not recoverable, so no need to re-check here.
|
||||
if outgoing_email_account:
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
@@ -510,7 +511,7 @@ def repost_gl_entries(doc):
|
||||
transactions = directly_dependent_transactions + list(repost_affected_transaction)
|
||||
|
||||
# handle stock delivered but not billed ledger entries
|
||||
if frappe.get_cached_value("Company", doc.company, "stock_delivered_but_not_billed"):
|
||||
if frappe.get_cached_value("Company", doc.company, "enable_stock_delivered_but_not_billed"):
|
||||
_update_post_delivery_billed_vouchers(transactions)
|
||||
|
||||
enable_separate_reposting_for_gl = frappe.db.get_single_value(
|
||||
|
||||
@@ -220,6 +220,39 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin):
|
||||
sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))),
|
||||
)
|
||||
|
||||
def test_recoverable_error_requeues_instead_of_failing(self):
|
||||
# A recoverable DB error (e.g. Postgres deadlock -> QueryDeadlockError) must re-queue the
|
||||
# repost as "In Progress"; a non-recoverable error still fails. Regression: the old check
|
||||
# string-matched MariaDB's "Deadlock found" and missed Postgres deadlocks ("deadlock detected").
|
||||
from unittest.mock import patch
|
||||
|
||||
from frappe.exceptions import QueryDeadlockError
|
||||
|
||||
from erpnext.stock.doctype.repost_item_valuation import repost_item_valuation as riv
|
||||
|
||||
orig_max_writes = frappe.db.MAX_WRITES_PER_TRANSACTION
|
||||
self.addCleanup(setattr, frappe.db, "MAX_WRITES_PER_TRANSACTION", orig_max_writes)
|
||||
|
||||
def status_after(error):
|
||||
doc = frappe.new_doc("Repost Item Valuation")
|
||||
doc.name = "test-recoverable-riv"
|
||||
doc.set_status = doc.log_error = doc.db_set = MagicMock()
|
||||
captured = {}
|
||||
with (
|
||||
patch.object(frappe, "in_test", False),
|
||||
patch.object(frappe.db, "exists", return_value=True),
|
||||
patch.object(frappe.db, "commit"),
|
||||
patch.object(frappe.db, "rollback"),
|
||||
patch.object(frappe.db, "set_value", side_effect=lambda *a, **k: captured.update(a[2])),
|
||||
patch.object(riv, "repost_sl_entries", side_effect=error),
|
||||
patch.object(frappe, "get_cached_value", return_value=None),
|
||||
):
|
||||
riv.repost(doc)
|
||||
return captured.get("status")
|
||||
|
||||
self.assertEqual(status_after(QueryDeadlockError("deadlock detected")), "In Progress")
|
||||
self.assertEqual(status_after(ValueError("boom")), "Failed")
|
||||
|
||||
def test_gl_repost_progress(self):
|
||||
from erpnext.accounts import utils
|
||||
|
||||
|
||||
@@ -455,6 +455,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap
|
||||
[
|
||||
"stock_delivered_but_not_billed",
|
||||
"disable_sdbnb_in_sr",
|
||||
"enable_stock_delivered_but_not_billed",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -464,6 +465,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap
|
||||
and ctx.is_stock_item
|
||||
and company_values
|
||||
and company_values.stock_delivered_but_not_billed
|
||||
and company_values.enable_stock_delivered_but_not_billed
|
||||
and not ctx.get("is_fixed_asset")
|
||||
and not ctx.get("is_subcontracted")
|
||||
):
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
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.stock.report.cogs_by_item_group.cogs_by_item_group import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company with perpetual inventory"
|
||||
|
||||
|
||||
class TestCogsByItemGroup(ERPNextTestSuite):
|
||||
def run_report(self, **extra) -> list:
|
||||
filters = frappe._dict(
|
||||
company="_Test Company with perpetual inventory",
|
||||
company=COMPANY,
|
||||
from_date="2026-01-01",
|
||||
to_date="2026-12-31",
|
||||
)
|
||||
@@ -20,16 +23,21 @@ class TestCogsByItemGroup(ERPNextTestSuite):
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_cogs_for_item_group(self):
|
||||
# Reuse the bootstrap item `_Test Item` (item group `_Test Item Group`).
|
||||
# It has zero stock in `Stores - TCP1`, so this receipt starts from a clean balance.
|
||||
item = "_Test Item"
|
||||
# A dedicated item group with a single item keeps `agg_value` scoped to this
|
||||
# test's COGS. The report sums COGS up the whole item-group tree keyed on the
|
||||
# company's default expense account, so a shared group would accumulate COGS
|
||||
# booked by any other test/fixture for the same company within the date range.
|
||||
# The group name is unique per run so items created by earlier runs (which
|
||||
# reuse a fixed group name) can't inflate the total either.
|
||||
item_group = make_item_group(f"_Test COGS Item Group {frappe.generate_hash(length=6)}")
|
||||
item = make_item(properties={"is_stock_item": 1, "item_group": item_group}).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item,
|
||||
to_warehouse="Stores - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
company=COMPANY,
|
||||
posting_date="2026-06-01",
|
||||
)
|
||||
|
||||
@@ -40,7 +48,7 @@ class TestCogsByItemGroup(ERPNextTestSuite):
|
||||
qty=4,
|
||||
rate=150,
|
||||
warehouse="Stores - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
company=COMPANY,
|
||||
update_stock=1,
|
||||
cost_center="Main - TCP1",
|
||||
parent_cost_center="Main - TCP1",
|
||||
@@ -51,7 +59,20 @@ class TestCogsByItemGroup(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
data = self.run_report()
|
||||
rows = [row for row in data if "_Test Item Group" in row.get("item_group")]
|
||||
self.assertTrue(rows, "No row found for _Test Item Group")
|
||||
rows = [row for row in data if item_group in row.get("item_group")]
|
||||
self.assertTrue(rows, "No row found for the dedicated item group")
|
||||
# 4 units delivered at 100 valuation rate -> 400 COGS.
|
||||
self.assertEqual(rows[0].get("cogs_debit"), 400)
|
||||
|
||||
|
||||
def make_item_group(name: str) -> str:
|
||||
if not frappe.db.exists("Item Group", name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": name,
|
||||
"parent_item_group": "All Item Groups",
|
||||
"is_group": 0,
|
||||
}
|
||||
).insert()
|
||||
return name
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
|
||||
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.stock.report.incorrect_balance_qty_after_transaction.incorrect_balance_qty_after_transaction import (
|
||||
execute,
|
||||
@@ -28,6 +29,30 @@ class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite):
|
||||
flagged = [row for row in data if row.get("item_code") == item]
|
||||
self.assertEqual(flagged, [])
|
||||
|
||||
def test_inconsistent_balance_qty_is_flagged(self):
|
||||
# a unique item keeps this SLE the only ledger entry for the item/warehouse
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
entry = make_stock_entry(
|
||||
item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01"
|
||||
)
|
||||
|
||||
# Corrupt the running balance so it no longer matches the cumulative actual_qty --
|
||||
# exactly the inconsistency this report exists to detect. set_value bypasses the
|
||||
# ledger recompute that would otherwise keep the two in sync.
|
||||
sle_name = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": entry.name, "item_code": item, "warehouse": WAREHOUSE},
|
||||
"name",
|
||||
)
|
||||
frappe.db.set_value("Stock Ledger Entry", sle_name, "qty_after_transaction", 5)
|
||||
|
||||
flagged = [row for row in self.run_report(item_code=item) if row.get("name") == sle_name]
|
||||
self.assertEqual(len(flagged), 1, "The tampered stock ledger entry should be flagged")
|
||||
row = flagged[0]
|
||||
self.assertEqual(row["expected_balance_qty"], 10) # cumulative actual_qty
|
||||
self.assertEqual(row["qty_after_transaction"], 5) # tampered balance
|
||||
self.assertEqual(row["differnce"], 5)
|
||||
|
||||
def test_sequence_of_movements_not_flagged(self):
|
||||
item = "_Test Item 2"
|
||||
make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=20, rate=50, posting_date="2026-06-01")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.item_wise_consumption.item_wise_consumption import execute
|
||||
@@ -22,7 +23,9 @@ class TestItemWiseConsumption(ERPNextTestSuite):
|
||||
return execute(filters)[1]
|
||||
|
||||
def test_consumed_vs_delivered_split(self):
|
||||
item = "_Test Item"
|
||||
# a uniquely-named item guarantees no residual stock/consumption from other
|
||||
# tests leaks in -- the report aggregates an item across all warehouses.
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
# purchase receipt gives the supplier mapping and stocks the item
|
||||
make_purchase_receipt(
|
||||
item_code=item,
|
||||
@@ -41,5 +44,7 @@ class TestItemWiseConsumption(ERPNextTestSuite):
|
||||
self.assertEqual(row[5], 400) # consumed amount
|
||||
self.assertEqual(row[6], 3) # delivered qty
|
||||
self.assertEqual(row[7], 300) # delivered amount
|
||||
self.assertEqual(row[8], 7) # total qty
|
||||
self.assertEqual(row[8], 7) # total qty = consumed + delivered
|
||||
self.assertEqual(row[9], 700) # total amount = consumed + delivered amount
|
||||
self.assertEqual(row[9], row[5] + row[7]) # total aggregates the two amounts
|
||||
self.assertIn("_Test Supplier", row[10])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user