mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 20:48:27 +00:00
Merge pull request #54437 from frappe/version-16-hotfix
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}%")})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
"""
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
37
erpnext/controllers/tests/test_taxes_and_totals.py
Normal file
37
erpnext/controllers/tests/test_taxes_and_totals.py
Normal 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)
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
11
erpnext/patches/v16_0/uom_category.py
Normal file
11
erpnext/patches/v16_0/uom_category.py
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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"),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user