Merge pull request #54437 from frappe/version-16-hotfix

This commit is contained in:
diptanilsaha
2026-04-22 05:49:31 +05:30
committed by GitHub
121 changed files with 3697 additions and 1790 deletions

View File

@@ -203,7 +203,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2025-08-02 06:26:44.657146",
"modified": "2026-04-14 18:14:42.202065",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -256,6 +256,14 @@
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -321,72 +321,6 @@ class TestAccount(ERPNextTestSuite):
self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.tests.utils import make_test_objects
accounts = [
# [account_name, parent_account, is_group]
["_Test Bank", "Bank Accounts", 0, "Bank", None],
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account CST", "Direct Expenses", 0, "Tax", None],
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
for company, abbr in [
["_Test Company", "_TC"],
["_Test Company 1", "_TC1"],
["_Test Company with perpetual inventory", "TCP1"],
]:
test_objects = make_test_objects(
"Account",
[
{
"doctype": "Account",
"account_name": account_name,
"parent_account": parent_account + " - " + abbr,
"company": company,
"is_group": is_group,
"account_type": account_type,
"account_currency": currency,
}
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects
def get_inventory_account(company, warehouse=None):
account = None
if warehouse:

View File

@@ -82,7 +82,7 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self):
def on_update(self):
if frappe.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {},
refresh: function (frm) {
frm.set_query("document_type", "repost_allowed_types", function (doc, cdt, cdn) {
return {
filters: {
name: ["in", frappe.boot.sysdefaults.repost_allowed_doctypes],
},
};
});
},
enable_immutable_ledger: function (frm) {
if (!frm.doc.enable_immutable_ledger) {
return;

View File

@@ -62,6 +62,8 @@
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"repost_section",
"repost_allowed_types",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
@@ -702,6 +704,17 @@
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
},
{
"fieldname": "repost_section",
"fieldtype": "Section Break",
"label": "Repost"
},
{
"fieldname": "repost_allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"grid_page_length": 50,
@@ -711,7 +724,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-30 07:32:58.182018",
"modified": "2026-04-13 15:30:28.729627",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -10,6 +10,9 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import sync_auto_reconcile_config
SELLING_DOCTYPES = [
@@ -44,6 +47,8 @@ class AccountsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -86,6 +91,7 @@ class AccountsSettings(Document):
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -140,6 +146,7 @@ class AccountsSettings(Document):
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
self.update_property_for_accounting_dimension()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -186,6 +193,17 @@ class AccountsSettings(Document):
title=_("Auto Tax Settings Error"),
)
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.repost_allowed_types]
if not doctypes:
return
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
@@ -225,3 +243,12 @@ def create_property_setter_for_hiding_field(doctype, field_name, hide):
"Check",
validate_fields_for_doctype=False,
)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -126,7 +126,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2025-01-22 10:46:42.904001",
"modified": "2026-04-14 18:15:27.367298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -173,11 +173,20 @@
"role": "Employee",
"select": 1,
"share": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -47,3 +47,12 @@ frappe.ui.form.on("Item Tax Template", {
});
},
});
frappe.ui.form.on("Item Tax Template Detail", {
not_applicable: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.not_applicable) {
frappe.model.set_value(cdt, cdn, "tax_rate", 0);
}
},
});

View File

@@ -27,8 +27,15 @@ class ItemTaxTemplate(Document):
# end: auto-generated types
def validate(self):
self.set_zero_rate_for_not_applicable_tax()
self.validate_tax_accounts()
def set_zero_rate_for_not_applicable_tax(self):
"""Ensure tax_rate is 0 for any row marked as not applicable."""
for row in self.get("taxes"):
if row.not_applicable:
row.tax_rate = 0
def autoname(self):
if self.company and self.title:
abbr = frappe.get_cached_value("Company", self.company, "abbr")

View File

@@ -6,7 +6,8 @@
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
"tax_rate",
"not_applicable"
],
"fields": [
{
@@ -21,20 +22,30 @@
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
"label": "Tax Rate",
"read_only_depends_on": "eval:doc.not_applicable"
},
{
"default": "0",
"description": "Check if this tax is not applicable to items (distinct from 0% rate)",
"fieldname": "not_applicable",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Not Applicable"
}
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:55.735360",
"modified": "2025-12-26 17:19:18.791891",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,6 +14,7 @@ class ItemTaxTemplateDetail(Document):
if TYPE_CHECKING:
from frappe.types import DF
not_applicable: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -648,7 +648,7 @@ $.extend(erpnext.journal_entry, {
reqd: 1,
default: frm.doc.posting_date,
},
{ fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark") },
{ fieldtype: "Small Text", fieldname: "remark", label: __("Remark") },
{
fieldtype: "Select",
fieldname: "naming_series",
@@ -665,8 +665,14 @@ $.extend(erpnext.journal_entry, {
var values = dialog.get_values();
frm.set_value("posting_date", values.posting_date);
frm.set_value("user_remark", values.user_remark);
frm.set_value("naming_series", values.naming_series);
if (values.remark) {
frm.set_value("custom_remark", 1);
frm.set_value("remark", values.remark);
} else {
frm.set_value("custom_remark", 0);
frm.set_value("remark", "");
}
// clear table is used because there might've been an error while adding child
// and cleanup didn't happen

View File

@@ -78,6 +78,7 @@
"from_template",
"title",
"column_break3",
"custom_remark",
"remark",
"mode_of_payment",
"party_not_required"
@@ -202,6 +203,7 @@
{
"fieldname": "user_remark",
"fieldtype": "Small Text",
"hidden": 1,
"label": "User Remark",
"no_copy": 1,
"oldfieldname": "user_remark",
@@ -315,7 +317,7 @@
"no_copy": 1,
"oldfieldname": "remark",
"oldfieldtype": "Small Text",
"read_only": 1
"read_only_depends_on": "eval: !doc.custom_remark"
},
{
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
@@ -651,6 +653,17 @@
"fieldname": "tax_withholding_tab",
"fieldtype": "Tab Break",
"label": "Tax Withholding"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"default": "0",
"fieldname": "custom_remark",
"fieldtype": "Check",
"label": "Custom Remark"
}
],
"icon": "fa fa-file-text",
@@ -665,7 +678,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-03-09 17:15:26.569327",
"modified": "2026-04-08 14:19:30.870894",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -61,6 +61,7 @@ class JournalEntry(AccountsController):
cheque_no: DF.Data | None
clearance_date: DF.Date | None
company: DF.Link
custom_remark: DF.Check
difference: DF.Currency
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -1026,8 +1027,8 @@ class JournalEntry(AccountsController):
if self.flags.skip_remarks_creation:
return
if self.user_remark:
r.append(_("Note: {0}").format(self.user_remark))
if self.get("custom_remark"):
return
if self.cheque_no:
if self.cheque_date:
@@ -1548,7 +1549,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
frappe.qb.from_(JournalEntry)
.join(JournalEntryAccount)
.on(JournalEntryAccount.parent == JournalEntry.name)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.user_remark)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
.where(JournalEntryAccount.account == filters.get("account"))
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
.where(JournalEntry.docstatus == 1)

View File

@@ -1,5 +1,5 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
get_indicator: function (doc) {
if (doc.docstatus === 1) {
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];

View File

@@ -413,9 +413,9 @@ class TestJournalEntry(ERPNextTestSuite):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
# Configure Repost Accounting Ledger for JVs
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]:
settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True})
settings = frappe.get_doc("Accounts Settings")
if "Journal Entry" not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": "Journal Entry"})
settings.save()
# Create JV with defaut cost center - _Test Cost Center
@@ -523,7 +523,7 @@ class TestJournalEntry(ERPNextTestSuite):
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.extend(
"accounts",
[
@@ -592,6 +592,14 @@ class TestJournalEntry(ERPNextTestSuite):
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_custom_remark(self):
# When custom_remark is enabled, remark should not be auto-overwritten on save
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.custom_remark = 1
jv.remark = "My custom remark text"
jv.insert()
self.assertEqual(jv.remark, "My custom remark text")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
@@ -620,7 +628,7 @@ def make_journal_entry(
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or nowdate()
jv.company = company or "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.multi_currency = 1
jv.set(
"accounts",

View File

@@ -48,7 +48,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-08-16 19:22:42.942264",
"modified": "2026-04-14 18:16:47.795986",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
@@ -68,12 +68,21 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"translated_doctype": 1
}
}

View File

@@ -2105,6 +2105,37 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertEqual(ref.voucher_no, so.name)
self.assertIsNotNone(ref.payment_term)
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -811,6 +811,7 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -857,7 +858,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-11-12 18:11:11.818015",
"modified": "2026-04-20 16:16:12.322024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -662,7 +662,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.is_recursive:
transaction_qty = sum(
[
row.qty
flt(row.qty)
for row in doc.items
if not row.is_free_item
and row.item_code == args.item_code

View File

@@ -2256,9 +2256,9 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
def test_repost_accounting_entries(self):
# update repost settings
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
settings = frappe.get_doc("Accounts Settings")
if "Purchase Invoice" not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": "Purchase Invoice"})
settings.save()
pi = make_purchase_invoice(

View File

@@ -219,7 +219,6 @@ def get_allowed_types_from_settings(child_doc: bool = False):
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types",
filters={"allowed": True},
fields=["document_type"],
distinct=True,
)
@@ -274,14 +273,13 @@ def validate_docs_for_voucher_types(doc_voucher_types):
if disallowed_types := voucher_types.difference(allowed_types):
message = "are" if len(disallowed_types) > 1 else "is"
frappe.throw(
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
_(
"{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}."
).format(
frappe.bold(comma_and(list(disallowed_types))),
message,
frappe.bold(
frappe.utils.get_link_to_form(
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
)
),
frappe.bold("Allowed Doctype"),
frappe.utils.get_link_to_form("Accounts Settings"),
)
)
@@ -289,8 +287,6 @@ def validate_docs_for_voucher_types(doc_voucher_types):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
filters = {"allowed": True}
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})

View File

@@ -203,6 +203,11 @@ class TestRepostAccountingLedger(ERPNextTestSuite):
def test_06_repost_purchase_receipt(self):
from erpnext.accounts.doctype.account.test_account import create_account
if not frappe.db.set_value("Company", "_Test Company", "service_expense_account"):
frappe.db.set_value(
"Company", "_Test Company", "service_expense_account", "Marketing Expenses - _TC"
)
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
@@ -275,7 +280,8 @@ def update_repost_settings():
"Journal Entry",
"Purchase Receipt",
]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
settings = frappe.get_doc("Accounts Settings")
for _type in allowed_types:
if _type not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": _type})
settings.save()

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
// refresh(frm) {
// },
// });

View File

@@ -1,53 +0,0 @@
{
"actions": [],
"creation": "2023-11-07 09:57:20.619939",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"allowed_types"
],
"fields": [
{
"fieldname": "allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:21.312607",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "System Manager",
"select": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,45 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
class RepostAccountingLedgerSettings(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
allowed_types: DF.Table[RepostAllowedTypes]
# end: auto-generated types
def validate(self):
self.update_property_for_accounting_dimension()
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
if not doctypes:
return
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -1,11 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from erpnext.tests.utils import ERPNextTestSuite
class TestRepostAccountingLedgerSettings(ERPNextTestSuite):
pass

View File

@@ -6,9 +6,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_sfzb",
"allowed"
"document_type"
],
"fields": [
{
@@ -17,29 +15,20 @@
"in_list_view": 1,
"label": "Doctype",
"options": "DocType"
},
{
"default": "0",
"fieldname": "allowed",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allowed"
},
{
"fieldname": "column_break_sfzb",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:32.415806",
"modified": "2026-04-14 16:53:16.806714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Allowed Types",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -14,7 +14,6 @@ class RepostAllowedTypes(Document):
if TYPE_CHECKING:
from frappe.types import DF
allowed: DF.Check
document_type: DF.Link | None
parent: DF.Data
parentfield: DF.Data

View File

@@ -25,6 +25,10 @@ frappe.ui.form.on("Shipping Rule", {
},
calculate_based_on: function (frm) {
frm.trigger("toggle_reqd");
if (frm.doc.calculate_based_on === "Fixed") {
frm.clear_table("conditions");
frm.refresh_field("conditions");
}
},
toggle_reqd: function (frm) {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");

View File

@@ -58,6 +58,11 @@ class ShippingRule(Document):
self.validate_overlapping_shipping_rule_conditions()
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:
self.set("conditions", [])
return
zero_to_values = []
for d in self.get("conditions"):

View File

@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -117,3 +117,49 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -196,6 +196,7 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -362,6 +363,7 @@ class ReceivablePayableReport:
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
@@ -390,6 +392,7 @@ class ReceivablePayableReport:
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
@@ -931,6 +934,7 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -998,6 +1002,9 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1237,6 +1244,7 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1413,6 +1421,7 @@ class InitSQLProceduresForAR:
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
@@ -1432,6 +1441,7 @@ class InitSQLProceduresForAR:
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
@@ -1447,7 +1457,7 @@ class InitSQLProceduresForAR:
begin
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
then
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1489,7 +1499,7 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""

View File

@@ -774,22 +774,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
jane = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
).insert()
self.customer = jane.name
si2 = self.create_sales_invoice(do_not_submit=True)
si2.posting_date = add_days(today(), -1)
si2.customer = self.customer2.name
si2.currency = "USD"
si2.conversion_rate = 80
si2.debit_to = self.debtors_usd
@@ -997,22 +993,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(expected_data, report_output)
def test_future_payments_on_foreign_currency(self):
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
jane = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
).insert()
self.customer = jane.name
si = self.create_sales_invoice(do_not_submit=True)
si.posting_date = add_days(today(), -1)
si.customer = self.customer2.name
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
@@ -1204,3 +1196,52 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -774,7 +774,28 @@ class GrossProfitGenerator:
# IMP NOTE
# stock_ledger_entries should already be filtered by item_code and warehouse and
# sorted by posting_date desc, posting_time desc
if item_code in self.non_stock_items and (row.project or row.cost_center):
if (
row.delivered_by_supplier
and row.so_detail
and (
po_details := frappe.get_all(
"Purchase Order Item",
filters={"sales_order_item": row.so_detail, "docstatus": 1},
pluck="name",
)
)
):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty * table.base_net_rate))
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
)
return flt(query.run()[0][0])
elif item_code in self.non_stock_items and (row.project or row.cost_center):
# Issue 6089-Get last purchasing rate for non-stock item
item_rate = self.get_last_purchase_rate(item_code, row)
return flt(row.qty) * item_rate
@@ -804,26 +825,6 @@ class GrossProfitGenerator:
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif (
row.delivered_by_supplier
and row.so_detail
and (
po_details := frappe.get_all(
"Purchase Order Item",
filters={"sales_order_item": row.so_detail, "docstatus": 1},
pluck="name",
)
)
):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.stock_qty * table.base_net_rate))
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
)
return flt(query.run()[0][0])
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:

View File

@@ -499,7 +499,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -5,6 +5,7 @@ import frappe
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.tests.utils import ERPNextTestSuite
@@ -26,6 +27,52 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")

View File

@@ -4,6 +4,7 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
@@ -72,6 +73,43 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
self.assertEqual(len(report[1]), 1)
result = frappe._dict(report[1][0])
self.assertEqual(result.voucher_no, si.name)
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -68,8 +68,10 @@ def get_tax_withholding_data(filters):
}
data.append(row)
# Sort by section code and transaction date
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
# Sort by section code, transaction date, then withholding_name for deterministic ordering
data.sort(
key=lambda x: (x["section_code"] or "", x["transaction_date"] or "", x["withholding_name"] or "")
)
return data

View File

@@ -5,10 +5,12 @@ import frappe
from frappe.utils import add_to_date, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_purchase_invoice,
create_records,
create_sales_invoice,
create_tax_withholding_category,
make_journal_entry_with_tax_withholding,
)
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
@@ -20,43 +22,45 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_records()
def test_tax_withholding_for_customers(self):
create_tax_category(cumulative_threshold=300)
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(customer="Test TCS Customer", rate=1000)
si.submit()
create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
[jv.name, "TCS", 0.075, 1000.75, 0.75, 1000.75],
["", "TCS", 0.075, 0, 0.75, 0],
[si.name, "TCS", 0.075, 1000.0, 0.75, 1000.75],
]
self.check_expected_values(result, expected_values)
def test_single_account_for_multiple_categories(self):
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.tax_withholding_category = "TDS - 1"
create_tax_category("TDS - 1", rate=10, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 1")
inv_1 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_1.submit()
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_2.tax_withholding_category = "TDS - 2"
create_tax_category("TDS - 2", rate=20, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 2")
inv_2 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_2.submit()
result = execute(
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
)[1]
expected_values = [
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
[inv_1.name, "TDS - 1", 10, 5000, 500, 4500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
@@ -81,20 +85,21 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
tds_doc.save()
inv_1 = make_purchase_invoice(
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", tds_doc.name)
inv_1 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=add_to_date(fiscal_year[1], days=1),
set_posting_time=True,
)
inv_1.set_posting_time = 1
inv_1.apply_tds = 1
inv_1.tax_withholding_category = tds_doc.name
inv_1.save()
inv_1.submit()
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
inv_2.set_posting_time = 1
inv_2.apply_tds = 1
inv_2.tax_withholding_category = tds_doc.name
inv_2.save()
inv_2 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=from_date,
set_posting_time=True,
)
inv_2.submit()
result = execute(
@@ -113,6 +118,7 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
self.assertEqual(len(result), len(expected_values))
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
@@ -127,21 +133,6 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def create_tax_accounts():
account_names = ["TCS", "TDS"]
for account in account_names:
frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"account_name": account,
"parent_account": "Duties and Taxes - _TC",
"report_type": "Balance Sheet",
"root_type": "Liability",
}
).insert(ignore_if_duplicate=True)
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
@@ -157,55 +148,34 @@ def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulat
)
def create_tcs_payment_entry():
def create_tcs_payment_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Payment Entry that generates a Tax Withholding Entry (Over Withheld)."""
payment_entry = create_payment_entry(
payment_type="Receive",
party_type="Customer",
party="_Test Customer",
party=party,
paid_from="Debtors - _TC",
paid_to="Cash - _TC",
paid_amount=2550,
)
payment_entry.append(
"taxes",
{
"account_head": "TCS - _TC",
"charge_type": "Actual",
"tax_amount": 0.53,
"add_deduct_tax": "Add",
"description": "Test",
"cost_center": "Main - _TC",
},
paid_amount=amount,
)
payment_entry.apply_tds = 1
payment_entry.tax_withholding_category = category
payment_entry.save()
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
def create_tcs_journal_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Credit Note Journal Entry that generates a Tax Withholding Entry."""
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party=party,
voucher_type="Credit Note",
amount=amount,
save=False,
)
jv.insert()
return jv.submit()
jv.apply_tds = 1
jv.tax_withholding_category = category
jv.save()
jv.submit()
return jv

View File

@@ -2513,6 +2513,7 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2539,6 +2540,7 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2556,6 +2558,7 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -24,6 +24,7 @@
"allow_zero_qty_in_supplier_quotation",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation",
"allow_negative_rates_for_items",
"column_break_12",
"maintain_same_rate",
"allow_multiple_items",
@@ -279,6 +280,12 @@
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
},
{
"default": "0",
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
}
],
"grid_page_length": 50,
@@ -288,7 +295,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:19.432589",
"modified": "2026-04-15 16:07:35.484787",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -18,6 +18,7 @@ class BuyingSettings(Document):
from frappe.types import DF
allow_multiple_items: DF.Check
allow_negative_rates_for_items: DF.Check
allow_zero_qty_in_purchase_order: DF.Check
allow_zero_qty_in_request_for_quotation: DF.Check
allow_zero_qty_in_supplier_quotation: DF.Check

View File

@@ -477,6 +477,11 @@ def create_supplier_quotation(doc):
if isinstance(doc, str):
doc = json.loads(doc)
if frappe.session.user not in frappe.get_all(
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
try:
sq_doc = frappe.get_doc(
{

View File

@@ -264,6 +264,13 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
for data in supplier_data:
rfq.append("suppliers", data)
frappe.new_doc(
"Portal User",
user="Administrator",
parent=data.get("supplier"),
parentfield="portal_users",
parenttype="Supplier",
).insert()
rfq.append(
"items",

View File

@@ -68,6 +68,7 @@ from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
ItemDetailsCtx,
_get_item_tax_template,
get_conversion_factor,
@@ -1767,6 +1768,7 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1851,6 +1853,7 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -3684,8 +3687,11 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
tax_map = json.loads(child_item.get("item_tax_rate"))
for tax_type in tax_map:
tax_rate = flt(tax_map[tax_type])
for tax_type, tax_rate in tax_map.items():
if tax_rate == NOT_APPLICABLE_TAX:
continue
tax_rate = flt(tax_rate)
taxes = parent_doc.get("taxes") or []
# add new row for tax head only if missing
found = any(tax.account_head == tax_type for tax in taxes)

View File

@@ -18,7 +18,11 @@ from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor, get_item_defaults
from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
get_conversion_factor,
get_item_defaults,
)
from erpnext.stock.utils import get_incoming_rate
@@ -523,6 +527,9 @@ class BuyingController(SubcontractingController):
if account not in tax_accounts:
continue
if rate == NOT_APPLICABLE_TAX:
continue
net_rate = item.base_net_amount
if item.sales_incoming_rate:
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -272,6 +272,12 @@ class StatusUpdater(Document):
continue
items_to_validate = []
selling_negative_rate_allowed = frappe.get_single_value(
"Selling Settings", "allow_negative_rates_for_items"
)
buying_negative_rate_allowed = frappe.get_single_value(
"Buying Settings", "allow_negative_rates_for_items"
)
# get unique transactions to update
for d in self.get_all_children():
@@ -281,7 +287,12 @@ class StatusUpdater(Document):
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if not frappe.get_single_value("Selling Settings", "allow_negative_rates_for_items"):
if (
not selling_negative_rate_allowed and self.doctype in ["Sales Invoice", "Delivery Note"]
) or (
not buying_negative_rate_allowed
and self.doctype in ["Purchase Invoice", "Purchase Receipt"]
):
if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0:
frappe.throw(
_(
@@ -289,7 +300,11 @@ class StatusUpdater(Document):
).format(
frappe.bold(d.item_code),
frappe.bold(_("`Allow Negative rates for Items`")),
get_link_to_form("Selling Settings", "Selling Settings"),
get_link_to_form(
"Selling Settings"
if self.doctype in ["Sales Invoice", "Delivery Note"]
else "Buying Settings"
),
),
)

View File

@@ -142,14 +142,19 @@ class StockController(AccountsController):
]:
for item in self.get("items"):
if (
(item.get("valuation_rate") == 0 or item.get("incoming_rate") == 0)
(
item.get("valuation_rate") == 0
or (item.get("incoming_rate") == 0 and self.get("update_stock", 1))
)
and item.get("allow_zero_valuation_rate") == 0
and frappe.get_cached_value("Item", item.item_code, "is_stock_item")
):
frappe.toast(
_(
"Row #{0}: Item {1} has zero rate but 'Allow Zero Valuation Rate' is not enabled."
).format(item.idx, frappe.bold(item.item_code)),
_("Row #{0}: Item {1} has zero rate but '{2}' is not enabled.").format(
item.idx,
frappe.bold(item.item_code),
item.meta.get_label("allow_zero_valuation_rate"),
),
indicator="orange",
)

View File

@@ -20,7 +20,12 @@ from erpnext.controllers.accounts_controller import (
validate_taxes_and_charges,
)
from erpnext.deprecation_dumpster import deprecated
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
ItemDetailsCtx,
_get_item_tax_template,
get_item_tax_map,
)
from erpnext.utilities.regional import temporary_flag
@@ -358,6 +363,9 @@ class calculate_taxes_and_totals:
if cint(tax.included_in_print_rate):
tax_rate = self._get_tax_rate(tax, item_tax_map)
if tax_rate == NOT_APPLICABLE_TAX:
return current_tax_fraction, inclusive_tax_amount_per_qty
if tax.charge_type == "On Net Total":
current_tax_fraction = tax_rate / 100.0
@@ -382,9 +390,12 @@ class calculate_taxes_and_totals:
def _get_tax_rate(self, tax, item_tax_map):
if tax.account_head in item_tax_map:
return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax))
else:
return tax.rate
rate = item_tax_map[tax.account_head]
if rate == NOT_APPLICABLE_TAX:
return NOT_APPLICABLE_TAX
return flt(rate, self.doc.precision("rate", tax))
return tax.rate
def calculate_net_total(self):
self.doc.total_qty = (
@@ -594,6 +605,9 @@ class calculate_taxes_and_totals:
current_tax_amount = 0.0
current_net_amount = 0.0
if tax_rate == NOT_APPLICABLE_TAX:
return current_net_amount, current_tax_amount
if tax.charge_type == "Actual":
current_net_amount = item.net_amount
# distribute the tax amount proportionally to each item row
@@ -784,18 +798,17 @@ class calculate_taxes_and_totals:
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = 0
self.doc.base_rounded_total = 0
self.doc.rounding_adjustment = 0
return
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
else:
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
# rounding adjustment should always be the difference vetween grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
# rounding adjustment should always be the difference between grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
@@ -1290,7 +1303,8 @@ def get_itemised_tax(doc, with_tax_account=False):
)
tax_info.tax_amount += flt(row.amount, precision)
tax_info.taxable_amount += flt(row.taxable_amount, precision)
conversion_rate = doc.conversion_rate or 1
tax_info.taxable_amount += flt(row.taxable_amount / conversion_rate, precision)
if with_tax_account:
tax_info.tax_account = tax.account_head

View File

@@ -301,3 +301,238 @@ class TestTaxesAndTotals(ERPNextTestSuite):
tax = doc.taxes[0]
detail = doc.item_wise_tax_details[0]
self.assertEqual(detail.amount, tax.base_tax_amount_after_discount_amount)
@change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_not_applicable_tax_in_item_tax_template(self):
"""Test that items with 'not applicable' tax don't contribute to net amount of that tax."""
template_7pct = frappe.get_doc(
{
"doctype": "Item Tax Template",
"title": "_Test VAT 7% Template",
"company": "_Test Company",
"taxes": [
{
"tax_type": "_Test Account VAT - _TC",
"tax_rate": 7,
},
{
"tax_type": "_Test Account Service Tax - _TC",
"tax_rate": 0,
"not_applicable": 1,
},
],
}
).insert(ignore_if_duplicate=True)
template_19pct = frappe.get_doc(
{
"doctype": "Item Tax Template",
"title": "_Test VAT 19% Template",
"company": "_Test Company",
"taxes": [
{
"tax_type": "_Test Account VAT - _TC",
"tax_rate": 0,
},
{
"tax_type": "_Test Account Service Tax - _TC",
"tax_rate": 19,
},
],
}
).insert(ignore_if_duplicate=True)
self.doc.items[0].item_tax_template = template_7pct.name
self.doc.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 100,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
"item_tax_template": template_19pct.name,
},
)
self.doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT 7%",
"rate": 7,
},
)
self.doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT 19%",
"rate": 19,
},
)
self.doc.save()
# VAT 7%: Both items contribute (Item 2 has 0% rate, not "not applicable")
self.assertEqual(self.doc.taxes[0].net_amount, 200.0)
# Service Tax 19%: Only Item 2 contributes (Item 1 has not_applicable)
self.assertEqual(self.doc.taxes[1].net_amount, 100.0)
expected_values = [
{
"item_row": self.doc.items[0].name,
"tax_row": self.doc.taxes[0].name,
"rate": 7.0,
"amount": 7.0,
"taxable_amount": 100.0,
},
{
"item_row": self.doc.items[1].name,
"tax_row": self.doc.taxes[0].name,
"rate": 0.0,
"amount": 0.0,
"taxable_amount": 100.0,
},
{
"item_row": self.doc.items[1].name,
"tax_row": self.doc.taxes[1].name,
"rate": 19.0,
"amount": 19.0,
"taxable_amount": 100.0,
},
]
actual_values = [
{
"item_row": row.item_row,
"tax_row": row.tax_row,
"rate": row.rate,
"amount": row.amount,
"taxable_amount": row.taxable_amount,
}
for row in self.doc.item_wise_tax_details
]
self.assertEqual(actual_values, expected_values)
def test_not_applicable_tax_in_item_tax_template_with_different_items(self):
"""Test that items with 'not applicable' tax don't contribute to net amount of that tax."""
template_7pct = frappe.get_doc(
{
"doctype": "Item Tax Template",
"title": "_Test VAT 7% Template",
"company": "_Test Company",
"taxes": [
{
"tax_type": "_Test Account VAT - _TC",
"tax_rate": 7,
},
{
"tax_type": "_Test Account Service Tax - _TC",
"tax_rate": 0,
"not_applicable": 1,
},
],
}
).insert(ignore_if_duplicate=True)
template_19pct = frappe.get_doc(
{
"doctype": "Item Tax Template",
"title": "_Test VAT 19% Template",
"company": "_Test Company",
"taxes": [
{
"tax_type": "_Test Account VAT - _TC",
"tax_rate": 0,
"not_applicable": 1,
},
{
"tax_type": "_Test Account Service Tax - _TC",
"tax_rate": 19,
},
],
}
).insert(ignore_if_duplicate=True)
self.doc.items[0].item_tax_template = template_7pct.name
self.doc.append(
"items",
{
"item_code": "_Test Item 2",
"qty": 1,
"rate": 100,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
"item_tax_template": template_19pct.name,
},
)
self.doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT 7%",
"rate": 0,
},
)
self.doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT 19%",
"rate": 0,
},
)
self.doc.save()
# VAT 7%: Only Item 1 contributes (Item 2 has not_applicable)
self.assertEqual(self.doc.taxes[0].net_amount, 100.0)
# Service Tax 19%: Only Item 2 contributes (Item 1 has not_applicable)
self.assertEqual(self.doc.taxes[1].net_amount, 100.0)
expected_values = [
{
"item_row": self.doc.items[0].name,
"tax_row": self.doc.taxes[0].name,
"rate": 7.0,
"amount": 7.0,
"taxable_amount": 100.0,
},
{
"item_row": self.doc.items[1].name,
"tax_row": self.doc.taxes[1].name,
"rate": 19.0,
"amount": 19.0,
"taxable_amount": 100.0,
},
]
actual_values = [
{
"item_row": row.item_row,
"tax_row": row.tax_row,
"rate": row.rate,
"amount": row.amount,
"taxable_amount": row.taxable_amount,
}
for row in self.doc.item_wise_tax_details
]
self.assertEqual(actual_values, expected_values)

View File

@@ -0,0 +1,37 @@
import frappe
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
class TestTaxesAndTotals(ERPNextTestSuite):
def test_disabling_rounded_total_resets_base_fields(self):
"""Disabling rounded total should also clear base rounded values."""
so = make_sales_order(do_not_save=True)
so.items[0].qty = 1
so.items[0].rate = 1000.25
so.items[0].price_list_rate = 1000.25
so.items[0].discount_percentage = 0
so.items[0].discount_amount = 0
so.set("taxes", [])
so.disable_rounded_total = 0
calculate_taxes_and_totals(so)
self.assertEqual(so.grand_total, 1000.25)
self.assertEqual(so.rounded_total, 1000.0)
self.assertEqual(so.rounding_adjustment, -0.25)
self.assertEqual(so.base_grand_total, 1000.25)
self.assertEqual(so.base_rounded_total, 1000.0)
self.assertEqual(so.base_rounding_adjustment, -0.25)
# User toggles disable_rounded_total after values are already set.
so.disable_rounded_total = 1
calculate_taxes_and_totals(so)
self.assertEqual(so.rounded_total, 0)
self.assertEqual(so.rounding_adjustment, 0)
self.assertEqual(so.base_rounded_total, 0)
self.assertEqual(so.base_rounding_adjustment, 0)

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import DateTimeLikeObject, getdate, today
from erpnext.accounts.utils import get_fiscal_year
def get_columns(filters, trans):
@@ -45,6 +47,10 @@ def get_columns(filters, trans):
def validate_filters(filters):
if not filters.get("fiscal_year"):
filters["fiscal_year"] = get_fiscal_year(today())[0]
if not filters.get("company"):
filters["company"] = frappe.defaults.get_user_default("Company")
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
if not filters.get(f.lower().replace(" ", "_")):
frappe.throw(_("{0} is mandatory").format(_(f)))

View File

@@ -692,3 +692,10 @@ fields_for_group_similar_items = ["qty", "amount"]
# ------------
# List of apps whose translatable strings should be excluded from this app's translations.
ignore_translatable_strings_from = ["frappe"]
repost_allowed_doctypes = [
"Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Payment Entry",
"Purchase Receipt",
]

File diff suppressed because it is too large Load Diff

View File

@@ -7,31 +7,18 @@
"engine": "InnoDB",
"field_order": [
"production_item_tab",
"final_product_section",
"company",
"item",
"column_break_ztxc",
"quantity",
"cb0",
"is_active",
"is_default",
"allow_alternative_item",
"set_rate_of_sub_assembly_item_based_on_bom",
"is_phantom_bom",
"cost_allocation_section",
"uom",
"cost_allocation__process_loss_section",
"cost_allocation_per",
"column_break_srby",
"cost_allocation",
"process_loss_section",
"column_break_tgkb",
"process_loss_percentage",
"column_break_ssj2",
"process_loss_qty",
"currency_detail",
"rm_cost_as_per",
"buying_price_list",
"price_list_currency",
"plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"operations_section_section",
"with_operations",
"track_semi_finished_goods",
@@ -46,8 +33,27 @@
"operations",
"materials_section",
"items",
"secondary_items_tab",
"section_break_hygk",
"secondary_items",
"bom_conf_tab",
"bom_configuration_section",
"column_break_zbzp",
"is_active",
"is_default",
"set_rate_of_sub_assembly_item_based_on_bom",
"cb0",
"is_phantom_bom",
"allow_alternative_item",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"default_warehouse_section",
"default_source_warehouse",
"column_break_inep",
"default_target_warehouse",
"consume_components_section",
"backflush_based_on",
"costing",
"operating_cost",
"raw_material_cost",
@@ -59,23 +65,21 @@
"column_break_26",
"total_cost",
"base_total_cost",
"quality_inspection_tab",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"more_info_tab",
"currency_detail",
"rm_cost_as_per",
"buying_price_list",
"price_list_currency",
"plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"production_item_info_section",
"item_name",
"uom",
"image",
"column_break_27",
"description",
"has_variants",
"default_warehouse_section",
"default_source_warehouse",
"column_break_inep",
"default_target_warehouse",
"section_break_ouuf",
"project",
"section_break0",
@@ -99,17 +103,18 @@
],
"fields": [
{
"description": "Item to be manufactured or repacked",
"description": "The final item that will be produced using this BOM.",
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item",
"label": "Item to Manufacture",
"oldfieldname": "item",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
"search_index": 1,
"show_description_on_click": 1
},
{
"fetch_from": "item.item_name",
@@ -130,23 +135,26 @@
"read_only": 1
},
{
"depends_on": "item",
"fetch_from": "item.stock_uom",
"fieldname": "uom",
"fieldtype": "Link",
"label": "Item UOM",
"label": "Unit Of Measure",
"options": "UOM",
"read_only": 1
},
{
"default": "1",
"description": "Quantity of item obtained after manufacturing / repacking from given quantities of raw materials",
"depends_on": "item",
"description": "How many units of the final product this BOM makes.",
"fieldname": "quantity",
"fieldtype": "Float",
"label": "Quantity",
"label": "Quantity (Output Qty)",
"non_negative": 1,
"oldfieldname": "quantity",
"oldfieldtype": "Currency",
"reqd": 1
"reqd": 1,
"show_description_on_click": 1
},
{
"fieldname": "cb0",
@@ -288,14 +296,13 @@
{
"fieldname": "materials_section",
"fieldtype": "Section Break",
"label": "Raw Materials",
"oldfieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"label": "Components",
"oldfieldname": "bom_materials",
"oldfieldtype": "Table",
"options": "BOM Item",
@@ -415,6 +422,7 @@
"depends_on": "eval:!doc.is_phantom_bom",
"fieldname": "website_section",
"fieldtype": "Tab Break",
"hidden": 1,
"label": "Website"
},
{
@@ -528,11 +536,6 @@
"fieldtype": "Section Break",
"label": "Operations"
},
{
"fieldname": "process_loss_section",
"fieldtype": "Section Break",
"label": "Process Loss"
},
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
@@ -546,10 +549,6 @@
"non_negative": 1,
"read_only": 1
},
{
"fieldname": "column_break_ssj2",
"fieldtype": "Column Break"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
@@ -668,11 +667,6 @@
"fieldname": "section_break_ouuf",
"fieldtype": "Section Break"
},
{
"fieldname": "quality_inspection_tab",
"fieldtype": "Tab Break",
"label": "Quality Inspection"
},
{
"fieldname": "secondary_items",
"fieldtype": "Table",
@@ -697,20 +691,6 @@
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "secondary_items_tab",
"fieldtype": "Tab Break",
"label": "Secondary Items"
},
{
"fieldname": "cost_allocation_section",
"fieldtype": "Section Break",
"label": "Cost Allocation"
},
{
"fieldname": "column_break_srby",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_allocation",
"fieldtype": "Currency",
@@ -725,6 +705,55 @@
"fieldtype": "Percent",
"label": "% Cost Allocation",
"non_negative": 1
},
{
"collapsible": 1,
"fieldname": "bom_configuration_section",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zbzp",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ztxc",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "cost_allocation__process_loss_section",
"fieldtype": "Section Break",
"label": "Cost Allocation / Process Loss"
},
{
"fieldname": "column_break_tgkb",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hygk",
"fieldtype": "Section Break"
},
{
"fieldname": "final_product_section",
"fieldtype": "Section Break"
},
{
"fieldname": "bom_conf_tab",
"fieldtype": "Tab Break",
"label": "BOM Configuration"
},
{
"fieldname": "consume_components_section",
"fieldtype": "Section Break",
"label": "Consume Components"
},
{
"description": "Controls how raw materials are consumed during the \u2018Manufacture\u2019 stock entry.",
"fieldname": "backflush_based_on",
"fieldtype": "Select",
"label": "Based On",
"options": "\nBOM\nMaterial Transferred for Manufacture",
"show_description_on_click": 1
}
],
"icon": "fa fa-sitemap",
@@ -732,7 +761,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2026-02-26 14:13:34.040181",
"modified": "2026-04-17 15:22:33.598938",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -117,6 +117,7 @@ class BOM(WebsiteGenerator):
allow_alternative_item: DF.Check
amended_from: DF.Link | None
backflush_based_on: DF.Literal["", "BOM", "Material Transferred for Manufacture"]
base_operating_cost: DF.Currency
base_raw_material_cost: DF.Currency
base_secondary_items_cost: DF.Currency
@@ -1982,3 +1983,16 @@ def get_secondary_items_from_sub_assemblies(bom_no, company, qty, secondary_item
get_secondary_items_from_sub_assemblies(row.bom_no, company, qty, secondary_items)
return secondary_items
def get_backflush_based_on(bom_no):
backflush_based_on = None
if bom_no:
backflush_based_on = frappe.get_cached_value("BOM", bom_no, "backflush_based_on")
if not backflush_based_on:
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
return backflush_based_on

View File

@@ -64,6 +64,13 @@ frappe.ui.form.on("BOM Creator", {
options: "Item",
reqd: 1,
},
{
label: __("Is Phantom BOM"),
fieldtype: "Check",
fieldname: "is_phantom",
default: 0,
change: toggle_filter,
},
{ fieldtype: "Column Break" },
{
label: __("Quantity"),
@@ -72,7 +79,7 @@ frappe.ui.form.on("BOM Creator", {
reqd: 1,
default: 1.0,
},
{ fieldtype: "Section Break" },
{ fieldtype: "Section Break", depends_on: "eval:!doc.is_phantom" },
{
label: __("Currency"),
fieldtype: "Link",
@@ -89,7 +96,7 @@ frappe.ui.form.on("BOM Creator", {
reqd: 1,
default: 1.0,
},
{ fieldtype: "Section Break" },
{ fieldtype: "Section Break", depends_on: "eval:!doc.is_phantom" },
{
label: __("Routing"),
fieldtype: "Link",
@@ -99,14 +106,39 @@ frappe.ui.form.on("BOM Creator", {
],
primary_action_label: __("Create"),
primary_action: (values) => {
values.doctype = frm.doc.doctype;
frappe.db.insert(values).then((doc) => {
frappe.set_route("Form", doc.doctype, doc.name);
frappe.db.get_value("Item", values.item_code, "is_stock_item").then((r) => {
if (r.message) {
if (r.message.is_stock_item && values.is_phantom) {
frappe.throw(
__("Phantom BOM cannot be created for stock item {0}.", [values.item_code])
);
} else if (!r.message.is_stock_item && !values.is_phantom) {
frappe.throw(
__("Non-phantom BOM cannot be created for non-stock item {0}.", [
values.item_code,
])
);
} else {
values.doctype = frm.doc.doctype;
frappe.db.insert(values).then((doc) => {
frappe.set_route("Form", doc.doctype, doc.name);
});
}
}
});
},
});
dialog.fields_dict.item_code.get_query = "erpnext.controllers.queries.item_query";
function toggle_filter() {
dialog.fields_dict.item_code.get_query = {
query: "erpnext.controllers.queries.item_query",
filters: {
is_stock_item: !dialog.fields_dict.is_phantom.value,
},
};
}
toggle_filter();
dialog.show();
},

View File

@@ -13,6 +13,7 @@
"details_tab",
"section_break_ylsl",
"item_code",
"is_phantom",
"item_name",
"item_group",
"column_break_ikj7",
@@ -282,6 +283,7 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.is_phantom",
"fieldname": "section_break_xvld",
"fieldtype": "Section Break",
"label": "Operations Routing"
@@ -291,6 +293,13 @@
"fieldtype": "Link",
"label": "Routing",
"options": "Routing"
},
{
"default": "0",
"fieldname": "is_phantom",
"fieldtype": "Check",
"label": "Is Phantom Item",
"read_only": 1
}
],
"hide_toolbar": 1,
@@ -302,7 +311,7 @@
"link_fieldname": "bom_creator"
}
],
"modified": "2024-11-25 16:41:03.047835",
"modified": "2026-04-16 20:43:30.820781",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator",
@@ -336,9 +345,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -52,6 +52,7 @@ class BOMCreator(Document):
currency: DF.Link
default_warehouse: DF.Link | None
error_log: DF.Text | None
is_phantom: DF.Check
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
@@ -77,6 +78,7 @@ class BOMCreator(Document):
self.set_rate_for_items()
def validate(self):
self.validate_finished_good()
self.validate_items()
self.validate_duplicate_item()
@@ -102,6 +104,15 @@ class BOMCreator(Document):
else:
item_map[key] = row.idx
def validate_finished_good(self):
is_stock_item = frappe.get_cached_value("Item", self.item_code, "is_stock_item")
if is_stock_item and self.is_phantom:
frappe.throw(_("Phantom BOM cannot be created for stock item {0}.").format(self.item_code))
elif not is_stock_item and not self.is_phantom:
frappe.throw(
_("Non-phantom BOM cannot be created for non-stock item {0}.").format(self.item_code)
)
def validate_items(self):
for row in self.items:
if row.is_expandable and row.item_code == self.item_code:
@@ -334,10 +345,12 @@ class BOMCreator(Document):
}
)
if row.item_code == self.item_code and (self.routing or self.has_operations()):
bom.routing = self.routing
bom.with_operations = 1
bom.transfer_material_against = "Work Order"
if row.item_code == self.item_code:
bom.is_phantom_bom = self.is_phantom
if not self.is_phantom and (self.routing or self.has_operations()):
bom.routing = self.routing
bom.with_operations = 1
bom.transfer_material_against = "Work Order"
for field in BOM_FIELDS:
if self.get(field):

View File

@@ -2879,6 +2879,9 @@ def make_bom(**args):
}
)
if args.backflush_based_on:
bom.backflush_based_on = args.backflush_based_on
if args.operating_cost_per_bom_quantity:
bom.fg_based_operating_cost = 1
bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity

View File

@@ -4240,6 +4240,66 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertEqual(wo_order.operations[0].time_in_mins, 72)
self.assertEqual(wo_order.operations[1].time_in_mins, 240)
def test_backflush_based_on_in_bom(self):
raw_material_1 = make_item(item_code="BOM RM 1", properties={"is_stock_item": 1}).name
raw_material_2 = make_item(item_code="BOM RM 2", properties={"is_stock_item": 1}).name
fg_item = make_item(item_code="BOM FG 1", properties={"is_stock_item": 1}).name
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
self.assertEqual(backflush_based_on, "BOM")
for item_code in [raw_material_1, raw_material_2]:
test_stock_entry.make_stock_entry(
item_code=item_code, target="Stores - _TC", qty=1, basic_rate=100
)
bom = make_bom(
item=fg_item,
quantity=1,
raw_materials=[raw_material_1],
backflush_based_on="Material Transferred for Manufacture",
)
wo_order = make_wo_order_test_record(item=fg_item, qty=1, source_warehouse="Stores - _TC")
self.assertEqual(bom.name, wo_order.bom_no)
backflush_based_on = frappe.db.get_value("BOM", wo_order.bom_no, "backflush_based_on")
self.assertEqual(backflush_based_on, "Material Transferred for Manufacture")
material_transfer_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)
)
material_transfer_entry.save()
# Add second raw material in the material transfer entry which is not in the BOM to simulate backflush based on material transfer scenario
material_transfer_entry.append(
"items",
{
"item_code": raw_material_2,
"item_name": raw_material_2,
"item_group": frappe.get_value("Item", raw_material_2, "item_group"),
"uom": frappe.get_value("Item", raw_material_2, "stock_uom"),
"conversion_factor": 1,
"s_warehouse": "Stores - _TC",
"t_warehouse": material_transfer_entry.items[0].t_warehouse,
"qty": 1,
},
)
material_transfer_entry.submit()
manufacture_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1))
manufacture_entry.save()
self.assertEqual(len(manufacture_entry.items), 3)
for row in manufacture_entry.items:
if row.s_warehouse:
self.assertIn(row.item_code, [raw_material_1, raw_material_2])
def get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry")

View File

@@ -241,6 +241,16 @@ frappe.ui.form.on("Work Order", {
frm.trigger("allow_alternative_item");
frm.trigger("hide_reserve_stock_button");
frm.trigger("toggle_items_editable");
frm.trigger("set_fg_warehouse_mandatory");
frm.trigger("toggle_hide_fields");
},
toggle_hide_fields(frm) {
frm.toggle_display("operations", frm.doc?.operations && frm.doc.operations.length > 0);
},
skip_transfer(frm) {
frm.trigger("set_fg_warehouse_mandatory");
},
toggle_items_editable(frm) {
@@ -277,6 +287,11 @@ frappe.ui.form.on("Work Order", {
return has_reserved_stock;
},
set_fg_warehouse_mandatory(frm) {
let mandatory = frm.doc.skip_transfer === 1 || frm.doc.track_semi_finished_goods === 1 ? false : true;
frm.toggle_reqd("fg_warehouse", mandatory);
},
add_custom_button_to_return_components: function (frm) {
if (frm.doc.docstatus === 1 && ["Closed", "Completed"].includes(frm.doc.status)) {
let non_consumed_items = frm.doc.required_items.filter((d) => {
@@ -628,6 +643,8 @@ frappe.ui.form.on("Work Order", {
if (r.message["set_scrap_wh_mandatory"]) {
frm.toggle_reqd("scrap_warehouse", true);
}
frm.trigger("toggle_hide_fields");
},
});
},

View File

@@ -15,7 +15,6 @@
"column_break1",
"qty",
"sales_order",
"track_semi_finished_goods",
"reserve_stock",
"section_break_vrpa",
"max_producible_qty",
@@ -86,6 +85,7 @@
"product_bundle_item",
"section_break_ynih",
"status",
"track_semi_finished_goods",
"column_break_cvuw",
"amended_from",
"connections_tab"
@@ -608,6 +608,7 @@
"fetch_from": "bom_no.track_semi_finished_goods",
"fieldname": "track_semi_finished_goods",
"fieldtype": "Check",
"hidden": 1,
"label": "Track Semi Finished Goods",
"read_only": 1
},
@@ -705,7 +706,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2026-03-16 10:15:28.708688",
"modified": "2026-04-17 13:42:12.374055",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@@ -162,6 +162,10 @@ class WorkOrder(Document):
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"),
)
if self.bom_no:
if based_on := frappe.get_cached_value("BOM", self.bom_no, "backflush_based_on"):
self.set_onload("backflush_raw_materials_based_on", based_on)
def show_create_job_card_button(self):
operation_details = frappe._dict(
frappe.get_all(
@@ -421,6 +425,18 @@ class WorkOrder(Document):
if self.production_plan_sub_assembly_item:
return
production_item = self.production_item
if self.material_request_item and (
mr_plan_item := frappe.get_value(
"Material Request Item", self.material_request_item, "material_request_plan_item"
)
):
if main_item_code := frappe.get_value(
"Material Request Plan Item", mr_plan_item, "main_item_code"
):
production_item = main_item_code
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -441,8 +457,8 @@ class WorkOrder(Document):
& (SalesOrder.docstatus == 1)
& (SalesOrder.name == self.sales_order)
& (
(SalesOrderItem.item_code == self.production_item)
| (ProductBundleItem.item_code == self.production_item)
(SalesOrderItem.item_code == production_item)
| (ProductBundleItem.item_code == production_item)
)
)
.run(as_dict=1)
@@ -461,7 +477,7 @@ class WorkOrder(Document):
& (SalesOrder.skip_delivery_note == 0)
& (SalesOrderItem.item_code == PackedItem.parent_item)
& (SalesOrder.docstatus == 1)
& (PackedItem.item_code == self.production_item)
& (PackedItem.item_code == production_item)
)
.run(as_dict=1)
)

View File

@@ -82,7 +82,9 @@ class Workstation(Document):
)
def before_save(self):
self.set_data_based_on_workstation_type()
if self.has_value_changed("workstation_type"):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
self.set_total_working_hours()
self.disabled_workstation()
@@ -112,9 +114,6 @@ class Workstation(Document):
@frappe.whitelist()
def set_data_based_on_workstation_type(self):
if self.workstation_costs:
return
if self.workstation_type:
data = frappe.get_all(
"Workstation Cost",
@@ -123,6 +122,9 @@ class Workstation(Document):
order_by="idx",
)
if data:
self.workstation_costs = []
for row in data:
self.append(
"workstation_costs",

View File

@@ -78,10 +78,12 @@ class MaterialRequirementsPlanningReport:
(so.docstatus == 1)
& (so.status.notin(["Closed", "Completed", "Stopped"]))
& (so_item.docstatus == 1)
& (so_item.item_code.isin(items))
)
)
if items:
query = query.where(so_item.item_code.isin(items))
if self.filters.get("warehouse"):
warehouses = [self.filters.get("warehouse")]
if frappe.db.get_value("Warehouse", self.filters.get("warehouse"), "is_group"):

View File

@@ -474,3 +474,5 @@ erpnext.patches.v16_0.update_requested_qty_packed_item
erpnext.patches.v16_0.remove_payables_receivables_workspace
erpnext.patches.v16_0.co_by_product_patch
erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.uom_category
erpnext.patches.v16_0.merge_repost_settings_to_accounts_settings

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
if allowed := frappe.get_hooks("repost_allowed_doctypes"):
accounts_settings = frappe.get_doc("Accounts Settings")
for x in allowed:
if x not in [t.document_type for t in accounts_settings.repost_allowed_types]:
accounts_settings.append("repost_allowed_types", {"document_type": x})
accounts_settings.save()

View File

@@ -0,0 +1,11 @@
import json
import frappe
def execute():
uom_data = json.loads(
open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read()
)
bulk_update_dict = {uom["uom_name"]: {"category": uom["category"]} for uom in uom_data}
frappe.db.bulk_update("UOM", bulk_update_dict)

View File

@@ -47,7 +47,7 @@
"icon": "icon-flag",
"idx": 1,
"links": [],
"modified": "2024-08-16 19:22:57.706521",
"modified": "2026-04-17 15:24:16.754519",
"modified_by": "Administrator",
"module": "Projects",
"name": "Activity Type",
@@ -79,11 +79,24 @@
{
"read": 1,
"role": "Employee"
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"translated_doctype": 1
}
}

View File

@@ -480,7 +480,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2026-03-09 17:15:24.426294",
"modified": "2026-04-14 18:17:40.676750",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -524,6 +524,14 @@
"role": "Employee",
"select": 1,
"share": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -423,7 +423,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2026-03-04 11:47:10.454548",
"modified": "2026-04-09 19:12:27.153143",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
@@ -441,6 +441,20 @@
"role": "Projects User",
"share": 1,
"write": 1
},
{
"role": "HR User",
"select": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"read": 1,
"role": "HR Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,

View File

@@ -21,7 +21,7 @@
}
],
"links": [],
"modified": "2024-03-27 13:10:51.823692",
"modified": "2026-04-17 15:32:28.598095",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task Type",
@@ -60,11 +60,19 @@
"report": 1,
"role": "Projects User",
"share": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "HR Manager",
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -315,7 +315,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-03-04 11:56:51.438298",
"modified": "2026-04-08 12:43:30.658074",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
@@ -336,21 +336,6 @@
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
@@ -389,6 +374,35 @@
"read": 1,
"role": "Accounts User",
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
const NOT_APPLICABLE_TAX = "N/A";
erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
setup() {
this.fetch_round_off_accounts();
@@ -299,6 +301,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (cint(tax.included_in_print_rate)) {
var tax_rate = this._get_tax_rate(tax, item_tax_map);
if (tax_rate === NOT_APPLICABLE_TAX) {
return [current_tax_fraction, inclusive_tax_amount_per_qty];
}
if (tax.charge_type == "On Net Total") {
current_tax_fraction = tax_rate / 100.0;
} else if (tax.charge_type == "On Previous Row Amount") {
@@ -322,9 +328,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
_get_tax_rate(tax, item_tax_map) {
return Object.keys(item_tax_map).indexOf(tax.account_head) != -1
? flt(item_tax_map[tax.account_head], precision("rate", tax))
: tax.rate;
if (tax.account_head in item_tax_map) {
let rate = item_tax_map[tax.account_head];
if (rate === NOT_APPLICABLE_TAX) {
return NOT_APPLICABLE_TAX;
}
return flt(rate, precision("rate", tax));
}
return tax.rate;
}
calculate_net_total() {
@@ -368,6 +379,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
$.each(item_tax_map, function (tax, rate) {
if (rate === NOT_APPLICABLE_TAX) {
return;
}
let found = (me.frm.doc.taxes || []).find((d) => d.account_head === tax);
if (!found) {
let child = frappe.model.add_child(me.frm.doc, "taxes");
@@ -524,6 +539,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var current_tax_amount = 0.0;
var current_net_amount = 0.0;
if (tax_rate === NOT_APPLICABLE_TAX) {
return [current_net_amount, current_tax_amount];
}
// To set row_id by default as previous row.
if (["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
if (tax.idx === 1) {
@@ -716,23 +735,21 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
disable_rounded_total = frappe.sys_defaults.disable_rounded_total;
}
if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0;
this.frm.doc.base_rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
return;
}
if (frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(
this.frm.doc.grand_total,
this.frm.doc.currency,
precision("rounded_total")
);
this.frm.doc.rounding_adjustment = flt(
this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment")
);
if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
} else {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(
this.frm.doc.grand_total,
this.frm.doc.currency,
precision("rounded_total")
);
this.frm.doc.rounding_adjustment = flt(
this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment")
);
}
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
}

View File

@@ -472,6 +472,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.msgprint(__("No pending payment schedules available."));
return;
}
schedules.forEach((schedule) => (schedule.__checked = 1));
const dialog = new frappe.ui.Dialog({
title: __("Select Payment Schedule"),
@@ -481,6 +482,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
fieldname: "payment_schedules",
label: __("Payment Schedules"),
cannot_add_rows: true,
cannot_delete_rows: true,
in_place_edit: false,
data: schedules,
fields: [
@@ -526,7 +528,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
return;
}
console.log(selected);
dialog.hide();
let me = this;
const payment_request_type = ["Sales Order", "Sales Invoice"].includes(this.frm.doc.doctype)

View File

@@ -39,7 +39,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
{
fieldtype: "Section Break",
label: __("Primary Contact Details"),
collapsible: 1,
collapsible: 0,
},
{
label: __("First Name"),
@@ -71,7 +71,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
{
fieldtype: "Section Break",
label: __("Primary Address Details"),
collapsible: 1,
collapsible: 0,
},
{
label: __("Address Line 1"),

View File

@@ -1,9 +1,12 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext import get_region
class SouthAfricaVATSettings(Document):
# begin: auto-generated types
@@ -22,4 +25,9 @@ class SouthAfricaVATSettings(Document):
vat_accounts: DF.Table[SouthAfricaVATAccount]
# end: auto-generated types
pass
def validate(self):
self.validate_company_region()
def validate_company_region(self):
if self.company and get_region(self.company) != "South Africa":
frappe.throw(_("Company {0} is not in South Africa.").format(frappe.bold(self.company)))

View File

@@ -10,6 +10,13 @@ frappe.query_reports["VAT Audit Report"] = {
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
get_query: function () {
return {
filters: {
country: "South Africa",
},
};
},
},
{
fieldname: "from_date",

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _
from frappe.utils import formatdate, get_link_to_form
from erpnext import get_region
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import get_tax_details_query
@@ -23,19 +24,10 @@ class VATAuditReport:
self.doctypes = ["Purchase Invoice", "Sales Invoice"]
def run(self):
self.validate_company_region()
self.get_sa_vat_accounts()
self.get_columns()
for doctype in self.doctypes:
self.select_columns = """
name as voucher_no,
posting_date, remarks"""
columns = (
", supplier as party, credit_to as account"
if doctype == "Purchase Invoice"
else ", customer as party, debit_to as account"
)
self.select_columns += columns
self.get_invoice_data(doctype)
if self.invoices:
@@ -45,6 +37,14 @@ class VATAuditReport:
return self.columns, self.data
def validate_company_region(self):
if self.filters.company and get_region(self.filters.company) != "South Africa":
frappe.throw(
_(
"The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa."
).format(frappe.bold(self.filters.company))
)
def get_sa_vat_accounts(self):
self.sa_vat_accounts = frappe.get_all(
"South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account"
@@ -56,27 +56,38 @@ class VATAuditReport:
frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings))
def get_invoice_data(self, doctype):
conditions = self.get_conditions()
self.invoices = frappe._dict()
invoice_data = frappe.db.sql(
f"""
SELECT
{self.select_columns}
FROM
`tab{doctype}`
WHERE
docstatus = 1 {conditions}
and is_opening = 'No'
ORDER BY
posting_date DESC
""",
self.filters,
as_dict=1,
invoice_doctype = frappe.qb.DocType(doctype)
party_field = invoice_doctype.supplier if doctype == "Purchase Invoice" else invoice_doctype.customer
account_field = (
invoice_doctype.credit_to if doctype == "Purchase Invoice" else invoice_doctype.debit_to
)
for d in invoice_data:
self.invoices.setdefault(d.voucher_no, d)
query = (
frappe.qb.from_(invoice_doctype)
.select(
invoice_doctype.name.as_("voucher_no"),
invoice_doctype.posting_date,
invoice_doctype.remarks,
party_field.as_("party"),
account_field.as_("account"),
)
.where(invoice_doctype.docstatus == 1)
.where(invoice_doctype.is_opening == "No")
.orderby(invoice_doctype.posting_date, order=frappe.qb.desc)
)
if self.filters.get("company"):
query = query.where(invoice_doctype.company == self.filters.company)
if self.filters.get("from_date"):
query = query.where(invoice_doctype.posting_date >= self.filters.from_date)
if self.filters.get("to_date"):
query = query.where(invoice_doctype.posting_date <= self.filters.to_date)
invoice_data = query.run(as_dict=True)
for row in invoice_data:
self.invoices.setdefault(row.voucher_no, row)
def get_invoice_items(self, doctype):
self.invoice_items = frappe._dict()
@@ -129,18 +140,6 @@ class VATAuditReport:
self.items_based_on_tax_rate[parent][row.rate]["net_amount"] += row.taxable_amount
self.items_based_on_tax_rate[parent][row.rate]["gross_amount"] += row.amount + row.taxable_amount
def get_conditions(self):
conditions = ""
for opts in (
("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_date)s"),
):
if self.filters.get(opts[0]):
conditions += opts[1]
return conditions
def get_data(self, doctype):
consolidated_data = self.get_consolidated_data(doctype)
section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales")

View File

@@ -14,7 +14,7 @@ from erpnext.selling.doctype.customer.customer import (
get_customer_outstanding,
parse_full_name,
)
from erpnext.tests.utils import ERPNextTestSuite, create_test_contact_and_address
from erpnext.tests.utils import ERPNextTestSuite
class TestCustomer(ERPNextTestSuite):
@@ -71,8 +71,6 @@ class TestCustomer(ERPNextTestSuite):
"customer_name": "_Test Customer",
}
create_test_contact_and_address()
frappe.db.set_value(
"Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1
)

View File

@@ -191,7 +191,7 @@ class Quotation(SellingController):
)
for row in self._items:
if row.name not in ordered_items or row.qty > ordered_items[row.name]:
if row.name not in ordered_items or row.stock_qty > ordered_items[row.name]:
return "Partially Ordered"
return "Ordered"
@@ -413,9 +413,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
target.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent):
balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0)
target.qty = balance_qty if balance_qty > 0 else 0
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0)
target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0
target.qty = flt(target.stock_qty) / flt(obj.conversion_factor)
if obj.against_blanket_order:
target.against_blanket_order = obj.against_blanket_order
@@ -429,7 +429,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
3. If no selections: Simple row: Map if adequate qty
"""
if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
if not ((item.stock_qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
return False
if not selected_rows:

View File

@@ -18,6 +18,7 @@ frappe.ui.form.on("Sales Order", {
Project: "Project",
"Payment Entry": "Payment",
"Work Order": "Work Order",
"Production Plan": "Production Plan",
};
frm.add_fetch("customer", "tax_id", "tax_id");
@@ -1059,6 +1060,14 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
__("Create")
);
}
if (frappe.model.can_create("Production Plan") && !doc.is_subcontracted) {
this.frm.add_custom_button(
__("Production Plan"),
() => this.make_production_plan(),
__("Create")
);
}
}
// sales invoice
@@ -1339,6 +1348,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
});
}
make_production_plan() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_production_plan",
frm: this.frm,
});
}
order_type() {
this.toggle_delivery_date();
}
@@ -1389,6 +1405,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_raw_material_request_dialog(r) {
var me = this;
r.message.forEach((item) => (item.__checked = 1));
var fields = [
{ fieldtype: "Check", fieldname: "include_exploded_items", label: __("Include Exploded Items") },
{
@@ -1399,7 +1416,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
{
fieldtype: "Table",
fieldname: "items",
description: __("Select BOM, Qty and For Warehouse"),
description: __("Finished Goods"),
cannot_delete_rows: true,
fields: [
{
fieldtype: "Read Only",

View File

@@ -27,6 +27,7 @@ from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
)
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
get_sales_orders,
)
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -1801,6 +1802,36 @@ def make_work_orders(items, sales_order, company, project=None):
return [p.name for p in out]
def make_production_plan(source_name, target_doc=None):
sales_order = frappe.get_doc("Sales Order", source_name)
production_plan = frappe.new_doc(
"Production Plan",
company=sales_order.company,
get_items_from="Sales Order",
posting_date=nowdate(),
)
open_so = [data.name for data in get_sales_orders(production_plan)]
if sales_order.name not in open_so:
frappe.throw(_("Sales Order {0} is not available for production").format(sales_order.name))
production_plan.append(
"sales_orders",
{
"sales_order": sales_order.name,
"sales_order_date": sales_order.transaction_date,
"customer": sales_order.customer,
"grand_total": sales_order.base_grand_total,
},
)
production_plan.get_items()
if not production_plan.get("po_items"):
frappe.throw(_("Sales Order {0} is not available for production").format(sales_order.name))
return production_plan
@frappe.whitelist()
def update_status(status, name):
so = frappe.get_doc("Sales Order", name, check_permission="submit")
@@ -2019,14 +2050,14 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
if not pending_qty:
pending_qty = stock_qty * overproduction_percentage_for_sales_order
if pending_qty > 0 and i.item_code not in product_bundle_parents:
if pending_qty > 0 and i.item_code not in product_bundle_parents and bom:
items.append(
dict(
name=i.name,
item_code=i.item_code,
item_name=i.item_name,
description=i.description,
bom=bom or "",
bom=bom,
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,

View File

@@ -8,7 +8,7 @@ import frappe
import frappe.permissions
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests import change_settings
from frappe.utils import add_days, flt, nowdate, today
from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
@@ -24,6 +24,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
create_pick_list,
make_delivery_note,
make_material_request,
make_production_plan,
make_raw_material_request,
make_sales_invoice,
make_work_orders,
@@ -213,6 +214,26 @@ class TestSalesOrder(ERPNextTestSuite):
self.assertEqual(dn.doctype, "Delivery Note")
self.assertEqual(len(dn.get("items")), len(so.get("items")))
def test_make_production_plan(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
fg_item = make_item("Test PP FG Item", {"is_stock_item": 1}).name
make_bom(item=fg_item, rate=100, raw_materials=["_Test Item"])
so = make_sales_order(item_code=fg_item, do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_production_plan, so.name)
so.submit()
pp = make_production_plan(so.name)
self.assertEqual(pp.doctype, "Production Plan")
self.assertGreater(len(pp.get("po_items")), 0)
self.assertEqual(pp.get("po_items")[0].sales_order, so.name)
self.assertEqual(pp.get("sales_orders")[0].sales_order, so.name)
self.assertEqual(getdate(pp.get("sales_orders")[0].sales_order_date), getdate(so.transaction_date))
self.assertEqual(pp.get("sales_orders")[0].customer, so.customer)
self.assertEqual(pp.get("sales_orders")[0].grand_total, so.base_grand_total)
def test_make_sales_invoice(self):
so = make_sales_order(do_not_submit=True)
@@ -2787,7 +2808,6 @@ def make_sales_order(**args):
)
so.delivery_date = add_days(so.transaction_date, 10)
if not args.do_not_save:
so.insert()
if not args.do_not_submit:

View File

@@ -1,122 +1,176 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import DocType, Field, Order
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.utils import QueryBuilder
from frappe.utils.data import comma_or
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerCommissionSummaryReport(filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerSummaryReport:
"""
Base class to generate Sales Partner Summary related Reports.
"""
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Total Commission"),
"fieldname": "total_commission",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
]
dt: DocType
date_field: str
date_label: str
columns: list
data: list
query: QueryBuilder
filters: dict
return columns
def __init__(self, filters: dict):
self.filters = filters
self.columns = []
def run(self):
self.validate_filters()
self.prepare_columns()
self.get_data()
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
return self.columns, self.data
def validate_filters(self):
if not self.filters.get("doctype"):
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))
if (
self.filters.get("from_date")
and self.filters.get("to_date")
and self.filters.get("from_date") > self.filters.get("to_date")
):
frappe.throw(_("From Date cannot be greater than To Date."))
self._set_date_field_and_label()
def _set_date_field_and_label(self):
self.date_field = (
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
)
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
def prepare_columns(self):
"""
SELECT
name, customer, territory, {} as posting_date, base_net_total as amount,
sales_partner, commission_rate, total_commission, '{}' as currency
FROM
`tab{}`
WHERE
{} and docstatus = 1 and sales_partner is not null
and sales_partner != '' order by name desc, sales_partner
""".format(date_field, company_currency, filters.get("doctype"), conditions),
filters,
as_dict=1,
)
Extend this method to add columns on the report. Use `make_column` to add more columns.
"""
raise NotImplementedError
return entries
def get_data(self):
self.build_report_query()
self.data = self.query.run(as_dict=1)
def build_report_query(self):
self._build_report_base_query()
self.extend_report_query()
self._apply_common_filters()
self.apply_filters()
def _build_report_base_query(self):
self.dt = DocType(self.filters.get("doctype"))
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
self.query = (
frappe.qb.from_(self.dt)
.select(
self.dt.name,
self.dt.customer,
self.dt.territory,
Field(self.date_field, "posting_date", table=self.dt),
self.dt.sales_partner,
self.dt.commission_rate,
ConstantColumn(company_currency).as_("currency"),
)
.where(
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
)
.orderby(self.dt.name, order=Order.desc)
.orderby(self.dt.sales_partner)
)
def extend_report_query(self):
"""
Extend this method to select more columns on the query.
"""
pass
def _apply_common_filters(self):
for field in ["company", "customer", "territory", "sales_partner"]:
if self.filters.get(field):
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
if self.filters.get("from_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
)
if self.filters.get("to_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
)
def apply_filters(self):
"""
Extend this method to add more conditions on the query.
"""
pass
def make_column(
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
):
self.columns.append(
dict(
label=label,
fieldname=fieldname,
fieldtype=fieldtype,
options=options,
width=width,
hidden=hidden,
)
)
def get_conditions(filters, date_field):
conditions = "1=1"
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
for field in ["company", "customer", "territory"]:
if filters.get(field):
conditions += f" and {field} = %({field})s"
self.make_column(_("Customer"), "customer", "Link", options="Customer")
if filters.get("sales_partner"):
conditions += " and sales_partner = %(sales_partner)s"
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
if filters.get("from_date"):
conditions += f" and {date_field} >= %(from_date)s"
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
if filters.get("to_date"):
conditions += f" and {date_field} <= %(to_date)s"
self.make_column(self.date_label, "posting_date", "Date")
return conditions
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
def extend_report_query(self):
self.query = self.query.select(
self.dt.base_net_total.as_("amount"),
self.dt.total_commission,
)

View File

@@ -0,0 +1,395 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.desk.query_report import run
from frappe.utils.data import comma_or
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SALES_TRANSACTION_DOCTYPES,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.tests.utils import ERPNextTestSuite
class SalesPartnerSummaryReportTestMixin(ERPNextTestSuite):
def assert_doctype_filters(self):
self.filters["doctype"] = "Purchase Invoice"
with self.assertRaisesRegex(
frappe.ValidationError,
_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
):
run(self.report_name, self.filters)
def assert_posting_date_label(self):
data = run(self.report_name, self.filters)
posting_date_column = next(
(column for column in data.get("columns") if column.fieldname == "posting_date"), None
)
self.assertNotEqual(posting_date_column.get("label"), "Posting Date")
self.assertEqual(posting_date_column.get("label"), "Order Date")
self.filters["doctype"] = "Sales Invoice"
data = run(self.report_name, self.filters)
posting_date_column = next(
(column for column in data.get("columns") if column.fieldname == "posting_date"), None
)
self.assertEqual(posting_date_column.get("label"), "Posting Date")
self.assertNotEqual(posting_date_column.get("label"), "Order Date")
def create_transactions(self, doctype):
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import (
POSInvoiceTestMixin,
create_pos_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
make_transaction_funcs = {
"Sales Order": make_sales_order,
"Sales Invoice": create_sales_invoice,
"Delivery Note": create_delivery_note,
"POS Invoice": create_pos_invoice,
}
self.date_field = "transaction_date" if doctype == "Sales Order" else "posting_date"
self.make_transaction_func = make_transaction_funcs[doctype]
make_stock_entry(
item_code="_Test Item 2",
qty=10,
company="_Test Company",
to_warehouse="_Test Warehouse - _TC",
purpose="Material Receipt",
posting_date="2026-01-01",
)
if doctype == "POS Invoice":
POSInvoiceTestMixin.setUp(self)
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
pos_opening_entry = create_opening_entry(self.pos_profile, self.test_user.name, get_obj=1)
self.transaction_doc_with_7pc_commision()
self.transaction_doc_with_5pc_commission()
self.transaction_doc_with_no_sales_partner()
self.transaction_doc_date_out_of_range_of_filters()
self.transaction_doc_with_revoked_commission()
self.transaction_doc_not_submitted()
self.transaction_doc_cancelled()
if doctype == "Sales Order":
return
self.transaction_doc_returned()
if doctype == "POS Invoice":
pos_opening_entry.cancel()
def transaction_doc_with_7pc_commision(self):
args = {"rate": 100, "qty": 10, self.date_field: "2026-01-14", "do_not_save": 1}
self.seven_pc_doc = self.make_transaction_func(**args)
self.seven_pc_doc.sales_partner = "_Test Sales Partner India - 1"
if self.seven_pc_doc.doctype == "POS Invoice":
self.seven_pc_doc.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
self.seven_pc_doc.save()
self.seven_pc_doc.submit()
def transaction_doc_with_5pc_commission(self):
args = {"rate": 20, "qty": 6, self.date_field: "2026-01-15", "do_not_save": 1}
self.five_pc_doc = self.make_transaction_func(**args)
self.five_pc_doc.sales_partner = "_Test Sales Partner India - 2"
self.five_pc_doc.append(
"items",
{
"item_code": "_Test Item 2",
"qty": 4,
"rate": 30,
},
)
if self.five_pc_doc.doctype == "POS Invoice":
self.five_pc_doc.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
self.five_pc_doc.save()
self.five_pc_doc.submit()
def transaction_doc_with_no_sales_partner(self):
args = {
"item_code": "_Test Item",
"rate": 50,
"qty": 10,
self.date_field: "2026-01-19",
"do_not_save": 1,
}
self.no_sp_doc = self.make_transaction_func(**args)
if self.no_sp_doc.doctype == "POS Invoice":
self.no_sp_doc.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
self.no_sp_doc.save()
self.no_sp_doc.submit()
def transaction_doc_date_out_of_range_of_filters(self):
args = {
"item_code": "_Test Item",
"rate": 60,
"qty": 10,
self.date_field: "2026-02-04",
"do_not_save": 1,
}
self.date_out_of_range_doc = self.make_transaction_func(**args)
self.date_out_of_range_doc.sales_partner = "_Test Sales Partner India - 1"
if self.date_out_of_range_doc.doctype == "POS Invoice":
self.date_out_of_range_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 600, "default": 1}
)
self.date_out_of_range_doc.save()
self.date_out_of_range_doc.submit()
def transaction_doc_with_revoked_commission(self):
try:
frappe.db.set_value("Item", "_Test Item", "grant_commission", 0)
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.revoked_comm_doc = self.make_transaction_func(**args)
self.revoked_comm_doc.sales_partner = "_Test Sales Partner India - 1"
if self.revoked_comm_doc.doctype == "POS Invoice":
self.revoked_comm_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1}
)
self.revoked_comm_doc.save()
self.revoked_comm_doc.submit()
finally:
frappe.db.set_value("Item", "_Test Item", "grant_commission", 1)
def transaction_doc_not_submitted(self):
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.doc_not_submitted = self.make_transaction_func(**args)
self.doc_not_submitted.set(self.date_field, "2026-01-26")
self.doc_not_submitted.sales_partner = "_Test Sales Partner India - 1"
if self.doc_not_submitted.doctype == "POS Invoice":
self.doc_not_submitted.append(
"payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1}
)
self.doc_not_submitted.save()
def transaction_doc_cancelled(self):
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.cancelled_doc = self.make_transaction_func(**args)
self.cancelled_doc.sales_partner = "_Test Sales Partner India - 1"
if self.cancelled_doc.doctype == "POS Invoice":
self.cancelled_doc.append("payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1})
self.cancelled_doc.save()
self.cancelled_doc.submit()
self.cancelled_doc.cancel()
def transaction_doc_returned(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
args = {
"item_code": "_Test Item",
"rate": 90,
"qty": 10,
self.date_field: "2026-01-18",
"do_not_save": 1,
}
self.to_be_returned_doc = self.make_transaction_func(**args)
self.to_be_returned_doc.sales_partner = "_Test Sales Partner India - 2"
if self.to_be_returned_doc.doctype == "POS Invoice":
self.to_be_returned_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 900, "default": 1}
)
self.to_be_returned_doc.save()
self.to_be_returned_doc.submit()
self.returned_doc = make_return_doc(self.to_be_returned_doc.doctype, self.to_be_returned_doc.name)
self.returned_doc.posting_date = "2026-01-19"
if self.returned_doc.doctype == "POS Invoice":
self.returned_doc.payments = []
self.returned_doc.append("payments", {"mode_of_payment": "Cash", "amount": -900, "default": 1})
self.returned_doc.save()
self.returned_doc.submit()
class TestSalesPartnerCommissionSummary(SalesPartnerSummaryReportTestMixin):
def setUp(self):
self.filters = {
"company": "_Test Company",
"doctype": "Sales Order",
"from_date": "2026-01-01",
"to_date": "2026-01-31",
}
self.report_name = "Sales Partner Commission Summary"
def test_doctype_filters(self):
self.assert_doctype_filters()
def test_posting_date_column_label(self):
self.assert_posting_date_label()
def test_sales_order_sp_commission_summary(self):
self.filters["doctype"] = "Sales Order"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_sales_invoice_sp_commission_summary(self):
self.filters["doctype"] = "Sales Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_delivery_note_sp_commission_summary(self):
self.filters["doctype"] = "Delivery Note"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_pos_invoice_sp_commission_summary(self):
self.filters["doctype"] = "POS Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def assert_sales_partner_commission_summary_report(self):
report_data = run(self.report_name, self.filters)
self.report_result = report_data.get("result")
self.report_result_without_total_row = self.report_result[:-1]
self.assertIsNotNone(self.report_result_without_total_row)
self.assert_7pc_commission()
self.assert_5pc_commission_with_multiple_items()
self.assert_doc_with_no_sp()
self.assert_doc_with_posting_date_out_of_range()
self.assert_doc_with_revoked_commission()
self.assert_doc_not_submitted()
self.assert_doc_cancelled()
self.assert_total_commission()
if self.filters["doctype"] != "Sales Order":
self.assert_returned_doc()
def assert_7pc_commission(self):
doc_name = self.seven_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 1000)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["total_commission"], 70)
def assert_5pc_commission_with_multiple_items(self):
doc_name = self.five_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 240)
self.assertEqual(row["commission_rate"], 5)
self.assertEqual(row["total_commission"], 12)
def assert_doc_with_no_sp(self):
doc_name = self.no_sp_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_posting_date_out_of_range(self):
doc_name = self.date_out_of_range_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_revoked_commission(self):
doc_name = self.revoked_comm_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 800)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["total_commission"], 0)
def assert_doc_not_submitted(self):
doc_name = self.doc_not_submitted.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_cancelled(self):
doc_name = self.cancelled_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_total_commission(self):
total_row = self.report_result[-1]
# Total Amount
self.assertEqual(total_row[-4], 2040)
# Total Commission
self.assertEqual(total_row[-1], 82)
def assert_returned_doc(self):
doc_name = self.to_be_returned_doc.name
returned_doc_name = self.returned_doc.name
outward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == doc_name), None
)
inward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == returned_doc_name),
None,
)
self.assertIsNotNone(outward_row)
self.assertIsNotNone(inward_row)
self.assertEqual(outward_row["amount"], 900)
self.assertEqual(outward_row["total_commission"], 45)
self.assertEqual(inward_row["amount"], -900)
self.assertEqual(inward_row["total_commission"], -45)

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Sales Partner Transaction Summary"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "sales_partner",
label: __("Sales Partner"),
@@ -28,14 +36,6 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "item_group",
label: __("Item Group"),

View File

@@ -3,144 +3,84 @@
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import Case
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SalesPartnerSummaryReport,
)
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerTransactionSummaryReport(filters=filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 100,
},
{
"label": _("Item Group"),
"fieldname": "item_group",
"fieldtype": "Link",
"options": "Item Group",
"width": 100,
},
{
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 120,
},
]
self.make_column(_("Customer"), "customer", "Link", options="Customer")
return columns
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
self.make_column(self.date_label, "posting_date", "Date")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
SELECT
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
FROM
`tab{doctype}` dt, `tab{doctype} Item` dt_item
WHERE
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
and dt.sales_partner is not null and dt.sales_partner != ''
order by dt.name desc, dt.sales_partner
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
filters,
as_dict=1,
)
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
return entries
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
def get_conditions(filters, date_field):
conditions = "1=1"
self.make_column(_("Quantity"), "qty", "Float", 120)
for field in ["company", "customer", "territory", "sales_partner"]:
if filters.get(field):
conditions += f" and dt.{field} = %({field})s"
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
if filters.get("from_date"):
conditions += f" and dt.{date_field} >= %(from_date)s"
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
if filters.get("to_date"):
conditions += f" and dt.{date_field} <= %(to_date)s"
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
if not filters.get("show_return_entries"):
conditions += " and dt_item.qty > 0.0"
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
if filters.get("brand"):
conditions += " and dt_item.brand = %(brand)s"
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
if filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
def extend_report_query(self):
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
conditions += f""" and dt_item.item_group in (select name from
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
self.query = (
self.query.join(self.dt_item)
.on(self.dt.name == self.dt_item.parent)
.select(
self.dt_item.base_net_rate.as_("rate"),
self.dt_item.qty,
self.dt_item.base_net_amount.as_("amount"),
Case()
.when(
self.dt_item.grant_commission.eq(1),
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
)
.else_(0)
.as_("commission"),
self.dt_item.brand,
self.dt_item.item_group,
self.dt_item.item_code,
)
)
return conditions
def apply_filters(self):
if not self.filters.get("show_return_entries"):
self.query = self.query.where(self.dt_item.qty > 0.0)
if self.filters.get("brand"):
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
if self.filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
if item_groups := frappe.get_all(
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
):
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))

View File

@@ -0,0 +1,183 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.desk.query_report import run
from erpnext.selling.report.sales_partner_commission_summary.test_sales_partner_commission_summary import (
SalesPartnerSummaryReportTestMixin,
)
class TestSalesPartnerTransactionSummary(SalesPartnerSummaryReportTestMixin):
def setUp(self):
self.filters = {
"company": "_Test Company",
"doctype": "Sales Order",
"from_date": "2026-01-01",
"to_date": "2026-01-31",
"show_return_entries": 1,
}
self.report_name = "Sales Partner Transaction Summary"
def test_doctype_filters(self):
self.assert_doctype_filters()
def test_posting_date_column_label(self):
self.assert_posting_date_label()
def test_sales_order_sp_transaction_summary(self):
self.filters["doctype"] = "Sales Order"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_sales_invoice_sp_transaction_summary(self):
self.filters["doctype"] = "Sales Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_delivery_note_sp_transaction_summary(self):
self.filters["doctype"] = "Delivery Note"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_pos_invoice_sp_transaction_summary(self):
self.filters["doctype"] = "POS Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def assert_sales_partner_transaction_summary_report(self):
report_data = run(self.report_name, self.filters)
self.report_result = report_data.get("result")
self.report_result_without_total_row = self.report_result[:-1]
self.assertIsNotNone(self.report_result_without_total_row)
self.assert_7pc_commission()
self.assert_5pc_commission_with_multiple_items()
self.assert_doc_with_no_sp()
self.assert_doc_with_posting_date_out_of_range()
self.assert_doc_with_revoked_commission()
self.assert_doc_not_submitted()
self.assert_doc_cancelled()
self.assert_commission()
if self.filters["doctype"] != "Sales Order":
self.assert_returned_doc()
def assert_7pc_commission(self):
doc_name = self.seven_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["customer"], "_Test Customer")
self.assertEqual(row["item_code"], "_Test Item")
self.assertEqual(row["item_group"], "_Test Item Group")
self.assertEqual(row["amount"], 1000)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["commission"], 70)
def assert_5pc_commission_with_multiple_items(self):
doc_name = self.five_pc_doc.name
row1 = next(
(
row
for row in self.report_result_without_total_row
if row.get("name") == doc_name and row.get("item_code") == "_Test Item"
),
None,
)
self.assertIsNotNone(row1)
row2 = next(
(
row
for row in self.report_result_without_total_row
if row.get("name") == doc_name and row.get("item_code") == "_Test Item 2"
),
None,
)
self.assertIsNotNone(row2)
self.assertEqual(row1["amount"], 120)
self.assertEqual(row1["commission_rate"], 5)
self.assertEqual(row1["commission"], 6)
self.assertEqual(row2["amount"], 120)
self.assertEqual(row2["commission_rate"], 5)
self.assertEqual(row2["commission"], 6)
def assert_doc_with_no_sp(self):
doc_name = self.no_sp_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_posting_date_out_of_range(self):
doc_name = self.date_out_of_range_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_revoked_commission(self):
doc_name = self.revoked_comm_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 800)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["commission"], 0)
def assert_doc_not_submitted(self):
doc_name = self.doc_not_submitted.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_cancelled(self):
doc_name = self.cancelled_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_commission(self):
total_row = self.report_result[-1]
# Total Amount
self.assertEqual(total_row[-4], 2040)
# Total Commission
self.assertEqual(total_row[-1], 82)
def assert_returned_doc(self):
doc_name = self.to_be_returned_doc.name
returned_doc_name = self.returned_doc.name
outward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == doc_name), None
)
inward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == returned_doc_name),
None,
)
self.assertIsNotNone(outward_row)
self.assertIsNotNone(inward_row)
self.assertEqual(outward_row["amount"], 900)
self.assertEqual(outward_row["commission"], 45)
self.assertEqual(inward_row["amount"], -900)
self.assertEqual(inward_row["commission"], -45)

View File

@@ -970,7 +970,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2026-03-09 17:15:33.819426",
"modified": "2026-04-17 17:11:46.586135",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -1030,6 +1030,16 @@
{
"role": "Auditor",
"select": 1
},
{
"role": "HR User",
"select": 1
},
{
"read": 1,
"report": 1,
"role": "HR Manager",
"write": 1
}
],
"row_format": "Dynamic",

View File

@@ -684,21 +684,6 @@ class Company(NestedSet):
self.db_set("disposal_account", disposal_acct)
if not self.service_expense_account:
service_expense_acct = frappe.db.get_value(
"Account",
{
"account_name": _("Marketing Expenses"),
"company": self.name,
"is_group": 0,
"root_type": "Expense",
},
"name",
)
if service_expense_acct:
self.db_set("service_expense_account", service_expense_acct)
def _set_default_account(self, fieldname, account_type):
if self.get(fieldname):
return

View File

@@ -31,7 +31,7 @@
"icon": "fa fa-bookmark",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:06:51.361961",
"modified": "2026-04-14 18:19:34.405635",
"modified_by": "Administrator",
"module": "Setup",
"name": "Designation",
@@ -40,7 +40,6 @@
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
@@ -52,12 +51,24 @@
{
"read": 1,
"role": "Sales User"
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"translated_doctype": 1
}
}

View File

@@ -129,7 +129,7 @@
],
"icon": "fa fa-user",
"links": [],
"modified": "2024-03-27 13:08:18.825438",
"modified": "2026-03-26 14:45:09.568829",
"modified_by": "Administrator",
"module": "Setup",
"name": "Driver",
@@ -144,30 +144,6 @@
"role": "Fleet Manager",
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"print": 1,
"read": 1,
@@ -186,6 +162,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "full_name",
"show_name_in_global_search": 1,
"sort_field": "creation",
@@ -193,4 +170,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
}
}

Some files were not shown because too many files have changed in this diff Show More