Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-53588

This commit is contained in:
Khushi Rawat
2026-04-13 15:41:39 +05:30
committed by GitHub
235 changed files with 7998 additions and 4725 deletions

View File

@@ -43,3 +43,6 @@ jobs:
- name: Run Semgrep rules - name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
- name: Semgrep for Test Correctness
run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml

View File

@@ -52,32 +52,9 @@ frappe.treeview_settings["Account"] = {
], ],
root_label: "Accounts", root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children", get_tree_nodes: "erpnext.accounts.utils.get_children",
on_get_node: function (nodes, deep = false) { on_node_render: function (node, deep) {
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return; const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
let accounts = [];
if (deep) {
// in case of `get_all_nodes`
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
} else {
accounts = nodes;
}
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if (value) {
const get_balances = frappe.call({
method: "erpnext.accounts.utils.get_account_balances",
args: {
accounts: accounts,
company: cur_tree.args.company,
include_default_fb_balances: true,
},
});
get_balances.then((r) => {
if (!r.message || r.message.length == 0) return;
for (let account of r.message) {
const node = cur_tree.nodes && cur_tree.nodes[account.value]; const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue; if (!node || node.is_root) continue;
@@ -90,11 +67,9 @@ frappe.treeview_settings["Account"] = {
node.parent && node.parent.find(".balance-area").remove(); node.parent && node.parent.find(".balance-area").remove();
$( $(
'<span class="balance-area pull-right">' + '<span class="balance-area pull-right">' +
(account.balance_in_account_currency (account.account_currency != account.company_currency
? format( ? format(account.balance_in_account_currency, account.account_currency) +
account.balance_in_account_currency, " / "
account.account_currency
) + " / "
: "") + : "") +
format(account.balance, account.company_currency) + format(account.balance, account.company_currency) +
" " + " " +
@@ -103,9 +78,29 @@ frappe.treeview_settings["Account"] = {
).insertBefore(node.$ul); ).insertBefore(node.$ul);
} }
} }
};
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
if (!cur_tree.account_balance_data) {
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if (value) {
frappe.call({
method: "erpnext.accounts.utils.get_account_balances_coa",
args: {
company: cur_tree.args.company,
include_default_fb_balances: true,
},
callback: function (r) {
if (!r.message || r.message.length === 0) return;
cur_tree.account_balance_data = r.message || [];
render_balances();
},
}); });
} }
}); });
} else {
render_balances();
}
}, },
add_tree_node: "erpnext.accounts.utils.add_ac", add_tree_node: "erpnext.accounts.utils.add_ac",
menu_items: [ menu_items: [

View File

@@ -26,8 +26,13 @@
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2025-10-15 03:19:47.171349", {
"link_doctype": "Account",
"link_fieldname": "account_category"
}
],
"modified": "2026-02-23 01:19:49.589393",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account Category", "name": "Account Category",

View File

@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountingDimension(ERPNextTestSuite): class TestAccountingDimension(ERPNextTestSuite):
def setUp(self):
create_dimension()
def test_dimension_against_sales_invoice(self): def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
@@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite):
si.save() si.save()
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
dimension = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
}
)
dimension.append(
"dimension_defaults",
{
"company": "_Test Company",
"reference_document": "Department",
"default_dimension": "_Test Department - _TC",
},
)
dimension.insert()
dimension.save()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Location",
}
)
dimension1.append(
"dimension_defaults",
{
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
},
)
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1
dimension1.save()
dimension2 = frappe.get_doc("Accounting Dimension", "Location")
dimension2.disabled = 1
dimension2.save()

View File

@@ -5,10 +5,6 @@ import unittest
import frappe import frappe
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
@@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountingDimensionFilter(ERPNextTestSuite): class TestAccountingDimensionFilter(ERPNextTestSuite):
def setUp(self): def setUp(self):
create_dimension()
create_accounting_dimension_filter() create_accounting_dimension_filter()
self.invoice_list = [] self.invoice_list = []

View File

@@ -65,6 +65,7 @@
"payment_options_section", "payment_options_section",
"enable_loyalty_point_program", "enable_loyalty_point_program",
"column_break_ctam", "column_break_ctam",
"fetch_payment_schedule_in_payment_request",
"invoicing_settings_tab", "invoicing_settings_tab",
"accounts_transactions_settings_section", "accounts_transactions_settings_section",
"over_billing_allowance", "over_billing_allowance",
@@ -688,6 +689,19 @@
"fieldname": "enable_accounting_dimensions", "fieldname": "enable_accounting_dimensions",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Accounting Dimensions" "label": "Enable Accounting Dimensions"
},
{
"default": "1",
"description": "Enable Subscription tracking in invoice",
"fieldname": "enable_subscription",
"fieldtype": "Check",
"label": "Enable Subscription"
},
{
"default": "1",
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -697,7 +711,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-02-27 01:04:09.415288", "modified": "2026-03-30 07:32:58.182018",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -73,6 +73,7 @@ class AccountsSettings(Document):
enable_loyalty_point_program: DF.Check enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_payment_schedule_in_payment_request: DF.Check
fetch_valuation_rate_for_internal_transaction: DF.Check fetch_valuation_rate_for_internal_transaction: DF.Check
general_ledger_remarks_length: DF.Int general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check ignore_account_closing_balance: DF.Check

View File

@@ -15,7 +15,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite): class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
""" """
Integration tests for AdvancePaymentLedgerEntry. Integration tests for AdvancePaymentLedgerEntry.
Use this class for testing interactions between multiple components. Use this class for testing interactions between multiple components.

View File

@@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party):
@frappe.whitelist() @frappe.whitelist()
def get_bank_account_details(bank_account): def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value( return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
) )

View File

@@ -15,7 +15,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite): class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -382,7 +382,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_group": "All Customer Groups", "customer_group": "Individual",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Poore Simon's", "customer_name": "Poore Simon's",
} }
@@ -413,7 +413,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_group": "All Customer Groups", "customer_group": "Individual",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Fayva", "customer_name": "Fayva",
} }

View File

@@ -2,10 +2,11 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests import UnitTestCase
from erpnext.tests.utils import ERPNextTestSuite
class TestBankTransactionFees(UnitTestCase): class TestBankTransactionFees(ERPNextTestSuite):
def test_included_fee_throws(self): def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the """A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself.""" withdrawal itself."""

View File

@@ -13,7 +13,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestExchangeRateRevaluation(AccountsTestMixin, ERPNextTestSuite): class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_usd_receivable_account() self.create_usd_receivable_account()

View File

@@ -3,6 +3,8 @@
frappe.ui.form.on("Financial Report Template", { frappe.ui.form.on("Financial Report Template", {
refresh(frm) { refresh(frm) {
if (frm.is_new() || frm.doc.rows.length === 0) return;
// add custom button to view missed accounts // add custom button to view missed accounts
frm.add_custom_button(__("View Account Coverage"), function () { frm.add_custom_button(__("View Account Coverage"), function () {
let selected_rows = frm.get_field("rows").grid.get_selected_children(); let selected_rows = frm.get_field("rows").grid.get_selected_children();
@@ -20,7 +22,7 @@ frappe.ui.form.on("Financial Report Template", {
}); });
}, },
validate(frm) { after_save(frm) {
if (!frm.doc.rows || frm.doc.rows.length === 0) { if (!frm.doc.rows || frm.doc.rows.length === 0) {
frappe.msgprint(__("At least one row is required for a financial report template")); frappe.msgprint(__("At least one row is required for a financial report template"));
} }
@@ -34,14 +36,6 @@ frappe.ui.form.on("Financial Report Row", {
update_formula_label(frm, row.data_source); update_formula_label(frm, row.data_source);
update_formula_description(frm, row.data_source); update_formula_description(frm, row.data_source);
if (row.data_source !== "Account Data") {
frappe.model.set_value(cdt, cdn, "balance_type", "");
}
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
}
set_up_filters_editor(frm, cdt, cdn); set_up_filters_editor(frm, cdt, cdn);
}, },
@@ -322,6 +316,8 @@ function update_formula_description(frm, data_source) {
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`; const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`; const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`; const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
const code_style = `style="background: var(--bg-light-gray); padding: var(--padding-xs); border-radius: var(--border-radius); font-size: 0.85em; width: max-content; margin-bottom: var(--margin-sm);"`;
const pre_style = `style="margin: 0; border-radius: var(--border-radius)"`;
let description_html = ""; let description_html = "";
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
<li><code>my_app.financial_reports.get_kpi_data</code></li> <li><code>my_app.financial_reports.get_kpi_data</code></li>
</ul> </ul>
<h6 ${subtitle_style}>Method Signature:</h6>
<div ${code_style}>
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br>&nbsp; # filters: dict — report filters (company, period, etc.) <br>&nbsp; # periods: list[dict] — period definitions <br>&nbsp; # row: dict — the current report row <br><br>&nbsp; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
</div>
<h6 ${subtitle_style}>Return Format:</h6> <h6 ${subtitle_style}>Return Format:</h6>
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p> <p ${text_style}>A list of numbers, one for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
</div>`; </div>`;
} else if (data_source === "Blank Line") { } else if (data_source === "Blank Line") {
description_html = ` description_html = `

View File

@@ -1,6 +1,5 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1,
"autoname": "field:template_name", "autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541", "creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType", "doctype": "DocType",
@@ -31,7 +30,8 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Report Type", "label": "Report Type",
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement" "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement",
"reqd": 1
}, },
{ {
"depends_on": "eval:frappe.boot.developer_mode", "depends_on": "eval:frappe.boot.developer_mode",
@@ -66,7 +66,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-11-14 00:11:03.508139", "modified": "2026-02-23 01:04:05.797161",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Financial Report Template", "name": "Financial Report Template",

View File

@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
template_name: DF.Data template_name: DF.Data
# end: auto-generated types # end: auto-generated types
def before_validate(self):
self.clear_hidden_fields()
def clear_hidden_fields(self):
style_data_sources = {"Blank Line", "Column Break", "Section Break"}
for row in self.rows:
if row.data_source != "Account Data":
row.balance_type = None
if row.data_source in style_data_sources:
row.calculation_formula = None
def validate(self): def validate(self):
validator = TemplateValidator(self) validator = TemplateValidator(self)
result = validator.validate() result = validator.validate()

View File

@@ -70,8 +70,8 @@ class ValidationResult:
self.warnings.append(issue) self.warnings.append(issue)
def notify_user(self) -> None: def notify_user(self) -> None:
warnings = "<br><br>".join(str(w) for w in self.warnings) warnings = "<br><br>".join(str(w) for w in self.warnings if w)
errors = "<br><br>".join(str(e) for e in self.issues) errors = "<br><br>".join(str(e) for e in self.issues if e)
if warnings: if warnings:
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange") frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
@@ -99,9 +99,8 @@ class TemplateValidator:
result.merge(validator.validate(self.template)) result.merge(validator.validate(self.template))
# Run row-level validations # Run row-level validations
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
for row in self.template.rows: for row in self.template.rows:
result.merge(self.formula_validator.validate(row, account_fields)) result.merge(self.formula_validator.validate(row))
return result return result
@@ -383,7 +382,8 @@ class AccountFilterValidator(Validator):
"""Validates account filter expressions used in Account Data rows""" """Validates account filter expressions used in Account Data rows"""
def __init__(self, account_fields: set | None = None): def __init__(self, account_fields: set | None = None):
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns) self.account_meta = frappe.get_meta("Account")
self.account_fields = account_fields or set(self.account_meta._valid_columns)
def validate(self, row) -> ValidationResult: def validate(self, row) -> ValidationResult:
result = ValidationResult() result = ValidationResult()
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
try: try:
filter_config = json.loads(row.calculation_formula) filter_config = json.loads(row.calculation_formula)
error = self._validate_filter_structure(filter_config, self.account_fields) error = self._validate_filter_structure(
filter_config,
self.account_fields,
row.advanced_filtering,
)
if error: if error:
result.add_error( result.add_error(
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
return result return result
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None: def _validate_filter_structure(
self,
filter_config,
account_fields: set,
advanced_filtering: bool = False,
) -> str | None:
# simple condition: [field, operator, value] # simple condition: [field, operator, value]
if isinstance(filter_config, list): if isinstance(filter_config, list):
if len(filter_config) != 3: if len(filter_config) != 3:
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
if not isinstance(field, str) or not isinstance(operator, str): if not isinstance(field, str) or not isinstance(operator, str):
return "Field and operator must be strings" return "Field and operator must be strings"
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
if field not in account_fields: if field not in account_fields:
return f"Field '{field}' is not a valid account field" return f"Field '{display}' is not a valid Account field"
if operator.casefold() not in OPERATOR_MAP: if operator.casefold() not in OPERATOR_MAP:
return f"Invalid operator '{operator}'" return f"Invalid operator '{operator}'"
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
# recursive # recursive
for condition in conditions: for condition in conditions:
error = self._validate_filter_structure(condition, account_fields) error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
if error: if error:
return error return error
else: else:
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
self.calculation_validator = CalculationFormulaValidator(reference_codes) self.calculation_validator = CalculationFormulaValidator(reference_codes)
self.account_filter_validator = AccountFilterValidator() self.account_filter_validator = AccountFilterValidator()
def validate(self, row, account_fields: set) -> ValidationResult: def validate(self, row) -> ValidationResult:
result = ValidationResult() result = ValidationResult()
if not row.calculation_formula: if not row.calculation_formula:
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
return self.calculation_validator.validate(row) return self.calculation_validator.validate(row)
elif row.data_source == "Account Data": elif row.data_source == "Account Data":
# Update account fields if provided
if account_fields:
self.account_filter_validator.account_fields = account_fields
return self.account_filter_validator.validate(row) return self.account_filter_validator.validate(row)
elif row.data_source == "Custom API": elif row.data_source == "Custom API":

View File

@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
self.data_source = "Account Data" self.data_source = "Account Data"
self.idx = 1 self.idx = 1
self.reverse_sign = 0 self.reverse_sign = 0
self.advanced_filtering = True
return MockReportRow(formula, reference_code) return MockReportRow(formula, reference_code)

View File

@@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype):
for hook in frappe.get_hooks(hook_type): for hook in frappe.get_hooks(hook_type):
frappe.call(hook, newname=newname, oldname=oldname) frappe.call(hook, newname=newname, oldname=oldname)
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()

View File

@@ -353,8 +353,11 @@ class JournalEntry(AccountsController):
frappe.throw(_("Account {0} should be of type Expense").format(d.account)) frappe.throw(_("Account {0} should be of type Expense").format(d.account))
def validate_stock_accounts(self): def validate_stock_accounts(self):
if self.voucher_type == "Periodic Accounting Entry": if (
# Skip validation for periodic accounting entry not erpnext.is_perpetual_inventory_enabled(self.company)
or self.voucher_type == "Periodic Accounting Entry"
):
# Skip validation for periodic accounting entry and Perpetual Inventory Disabled Company.
return return
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts) stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)

View File

@@ -10,7 +10,7 @@ from erpnext.accounts.utils import run_ledger_health_checks
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite): class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -71,6 +71,7 @@ def start_merge(docname):
ledger_merge.account, ledger_merge.account,
) )
row.db_set("merged", 1) row.db_set("merged", 1)
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
successful_merges += 1 successful_merges += 1
frappe.publish_realtime( frappe.publish_realtime(
@@ -78,6 +79,7 @@ def start_merge(docname):
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total}, {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
) )
except Exception: except Exception:
if not frappe.in_test:
frappe.db.rollback() frappe.db.rollback()
ledger_merge.log_error("Ledger merge failed") ledger_merge.log_error("Ledger merge failed")
finally: finally:

View File

@@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
}); });
}); });
if (frm.doc.create_missing_party) { frm.trigger("update_party_labels");
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
}
}, },
setup_company_filters: function (frm) { setup_company_filters: function (frm) {
@@ -127,7 +125,9 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
frappe.model.set_value(row.doctype, row.name, "party", ""); frappe.model.set_value(row.doctype, row.name, "party", "");
frappe.model.set_value(row.doctype, row.name, "party_name", ""); frappe.model.set_value(row.doctype, row.name, "party_name", "");
}); });
frm.clear_table("invoices");
frm.refresh_fields(); frm.refresh_fields();
frm.trigger("update_party_labels");
}, },
make_dashboard: function (frm) { make_dashboard: function (frm) {
@@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
} }
frm.refresh_field("invoices"); frm.refresh_field("invoices");
}, },
update_party_labels: function (frm) {
let is_sales = frm.doc.invoice_type == "Sales";
frm.fields_dict["invoices"].grid.update_docfield_property(
"party",
"label",
is_sales ? "Customer ID" : "Supplier ID"
);
frm.fields_dict["invoices"].grid.update_docfield_property(
"party_name",
"label",
is_sales ? "Customer Name" : "Supplier Name"
);
frm.set_df_property(
"create_missing_party",
"description",
is_sales
? __("If party does not exist, create it using the Customer Name field.")
: __("If party does not exist, create it using the Supplier Name field.")
);
frm.refresh_field("invoices");
frm.refresh_field("create_missing_party");
},
}); });
frappe.ui.form.on("Opening Invoice Creation Tool Item", { frappe.ui.form.on("Opening Invoice Creation Tool Item", {

View File

@@ -7,10 +7,11 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"section_break_ynel",
"company", "company",
"create_missing_party",
"column_break_3", "column_break_3",
"invoice_type", "invoice_type",
"create_missing_party",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
@@ -25,11 +26,11 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"remember_last_selected_value": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"default": "0", "default": "0",
"description": "If party does not exist, create it using the Party Name field.",
"fieldname": "create_missing_party", "fieldname": "create_missing_party",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Create Missing Party" "label": "Create Missing Party"
@@ -79,12 +80,17 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "section_break_ynel",
"fieldtype": "Section Break",
"hide_border": 1
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-03-23 00:32:15.600086", "modified": "2026-03-31 01:47:20.360352",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Opening Invoice Creation Tool", "name": "Opening Invoice Creation Tool",

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import escape_html, flt, nowdate
from frappe.utils.background_jobs import enqueue, is_job_enqueued from frappe.utils.background_jobs import enqueue, is_job_enqueued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document):
) )
prepare_invoice_summary(doctype, invoices) prepare_invoice_summary(doctype, invoices)
invoices_summary_companies = list(invoices_summary.keys())
for company in invoices_summary_companies:
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
return invoices_summary, max_count return invoices_summary, max_count
def validate_company(self): def validate_company(self):
@@ -274,6 +279,7 @@ def start_import(invoices):
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.insert(set_name=invoice_number) doc.insert(set_name=invoice_number)
doc.submit() doc.submit()
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
names.append(doc.name) names.append(doc.name)
except Exception: except Exception:

View File

@@ -3,10 +3,6 @@
import frappe import frappe
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account, get_temporary_opening_account,
) )
@@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestOpeningInvoiceCreationTool(ERPNextTestSuite): class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
def make_invoices( def make_invoices(
self, self,
invoice_type="Sales", invoice_type="Sales",
@@ -183,26 +174,13 @@ def get_opening_invoice_creation_dict(**args):
return invoice_dict return invoice_dict
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
company = frappe.new_doc("Company")
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "Pakistan"
company.insert()
return company
def make_customer(customer=None): def make_customer(customer=None):
customer_name = customer or "Opening Customer" customer_name = customer or "Opening Customer"
customer = frappe.get_doc( customer = frappe.get_doc(
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_name": customer_name, "customer_name": customer_name,
"customer_group": "All Customer Groups", "customer_group": "Individual",
"customer_type": "Company", "customer_type": "Company",
"territory": "All Territories", "territory": "All Territories",
} }

View File

@@ -824,7 +824,7 @@ frappe.ui.form.on("Payment Entry", {
paid_amount: function (frm) { paid_amount: function (frm) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (!frm.doc.received_amount) { if (frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount); frm.set_value("received_amount", frm.doc.paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) { } else if (company_currency == frm.doc.paid_to_account_currency) {
@@ -845,7 +845,7 @@ frappe.ui.form.on("Payment Entry", {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate) flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
); );
if (!frm.doc.paid_amount) { if (frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount); frm.set_value("paid_amount", frm.doc.received_amount);
if (frm.doc.target_exchange_rate) { if (frm.doc.target_exchange_rate) {

View File

@@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False):
vouchers=args.get("vouchers") or None, vouchers=args.get("vouchers") or None,
) )
outstanding_invoices = split_invoices_based_on_payment_terms( outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company"))
outstanding_invoices, args.get("company")
)
for d in outstanding_invoices: for d in outstanding_invoices:
d["exchange_rate"] = 1 d["exchange_rate"] = 1
@@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False):
filters=args, filters=args,
) )
orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company"))
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data: if not data:
@@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False):
return data return data
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: def split_refdocs_based_on_payment_terms(refdocs, company) -> list:
"""Split a list of invoices based on their payment terms.""" """Split a list of invoices based on their payment terms."""
exc_rates = get_currency_data(outstanding_invoices, company) exc_rates = get_currency_data(refdocs, company)
outstanding_invoices_after_split = [] outstanding_refdoc_after_split = []
for entry in outstanding_invoices: for entry in refdocs:
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
if payment_term_template := frappe.db.get_value( if payment_term_template := frappe.db.get_value(
entry.voucher_type, entry.voucher_no, "payment_terms_template" entry.voucher_type, entry.voucher_no, "payment_terms_template"
): ):
@@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
), ),
alert=True, alert=True,
) )
outstanding_invoices_after_split += split_rows outstanding_refdoc_after_split += split_rows
continue continue
# If not an invoice or no payment terms template, add as it is # If not an invoice or no payment terms template, add as it is
outstanding_invoices_after_split.append(entry) outstanding_refdoc_after_split.append(entry)
return outstanding_invoices_after_split return outstanding_refdoc_after_split
def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict:
"""Get currency and conversion data for a list of invoices.""" """Get currency and conversion data for a list of invoices."""
exc_rates = frappe._dict() exc_rates = frappe._dict()
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype]
for x in frappe.db.get_all( for x in frappe.db.get_all(
doctype, doctype,
filters={"name": ["in", invoices]}, filters={"name": ["in", refdoc]},
fields=["name", "currency", "conversion_rate", "party_account_currency"], fields=["name", "currency", "conversion_rate", "party_account_currency"],
): ):
exc_rates[x.name] = frappe._dict( exc_rates[x.name] = frappe._dict(

View File

@@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def test_outstanding_orders_split_by_payment_terms(self):
create_payment_terms_template()
so = make_sales_order(do_not_save=1, qty=1, rate=200)
so.payment_terms_template = "Test Receivable Template"
so.save().submit()
args = {
"posting_date": nowdate(),
"company": so.company,
"party_type": "Customer",
"payment_type": "Receive",
"party": so.customer,
"party_account": "Debtors - _TC",
"get_orders_to_be_billed": True,
}
references = get_outstanding_reference_documents(args)
self.assertEqual(len(references), 2)
self.assertEqual(references[0].voucher_no, so.name)
self.assertEqual(references[1].voucher_no, so.name)
self.assertEqual(references[0].payment_term, "Basic Amount Receivable")
self.assertEqual(references[1].payment_term, "Tax Receivable")
def test_outstanding_orders_no_split_when_allocate_disabled(self):
create_payment_terms_template()
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
template.allocate_payment_based_on_payment_terms = 0
template.save()
so = make_sales_order(do_not_save=1, qty=1, rate=200)
so.payment_terms_template = "Test Receivable Template"
so.save().submit()
args = {
"posting_date": nowdate(),
"company": so.company,
"party_type": "Customer",
"payment_type": "Receive",
"party": so.customer,
"party_account": "Debtors - _TC",
"get_orders_to_be_billed": True,
}
references = get_outstanding_reference_documents(args)
self.assertEqual(len(references), 1)
self.assertIsNone(references[0].payment_term)
template.allocate_payment_based_on_payment_terms = 1
template.save()
def test_outstanding_multicurrency_sales_order_split(self):
create_payment_terms_template()
so = make_sales_order(
customer="_Test Customer USD",
currency="USD",
qty=1,
rate=100,
do_not_submit=True,
)
so.payment_terms_template = "Test Receivable Template"
so.conversion_rate = 50
so.save().submit()
args = {
"posting_date": nowdate(),
"company": so.company,
"party_type": "Customer",
"payment_type": "Receive",
"party": so.customer,
"party_account": "Debtors - _TC",
"get_orders_to_be_billed": True,
}
references = get_outstanding_reference_documents(args)
# Should split without throwing currency errors
self.assertEqual(len(references), 2)
for ref in references:
self.assertEqual(ref.voucher_no, so.name)
self.assertIsNotNone(ref.payment_term)
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -750,6 +750,7 @@ def make_payment_request(**args):
pr.submit() pr.submit()
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
frappe.local.response["type"] = "redirect" frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = pr.get_payment_url() frappe.local.response["location"] = pr.get_payment_url()

View File

@@ -4,10 +4,6 @@ import unittest
import frappe import frappe
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening, make_closing_entry_from_opening,
) )
@@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
test case to check whether we can create POS Closing Entry without mandatory accounting dimension test case to check whether we can create POS Closing Entry without mandatory accounting dimension
""" """
create_dimension()
location = frappe.get_doc("Accounting Dimension", "Location") location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True location.dimension_defaults[0].mandatory_for_bs = True
location.save() location.save()
@@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
) )
accounting_dimension_department.mandatory_for_bs = 0 accounting_dimension_department.mandatory_for_bs = 0
accounting_dimension_department.save() accounting_dimension_department.save()
disable_dimension()
def test_merging_into_sales_invoice_for_batched_item(self): def test_merging_into_sales_invoice_for_batched_item(self):
frappe.flags.print_message = False frappe.flags.print_message = False

View File

@@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
new_from_date = add_months(new_to_date, -1 * doc.filter_duration) new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()))
if doc.report == "General Ledger": if doc.report == "General Ledger":
doc.db_set("to_date", new_to_date, commit=True) frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date)
doc.db_set("from_date", new_from_date, commit=True) frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date)
else: else:
doc.db_set("posting_date", new_to_date, commit=True) frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date)
return True return True
else: else:
return False return False

View File

@@ -14,7 +14,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestProcessStatementOfAccounts(AccountsTestMixin, ERPNextTestSuite): class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0) frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey") letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")

View File

@@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", {
selling: function (frm) { selling: function (frm) {
frm.trigger("set_options_for_applicable_for"); frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("buying", !frm.doc.selling);
}, },
buying: function (frm) { buying: function (frm) {
frm.trigger("set_options_for_applicable_for"); frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("selling", !frm.doc.buying);
}, },
set_options_for_applicable_for: function (frm) { set_options_for_applicable_for: function (frm) {

View File

@@ -8,7 +8,6 @@
"email_append_to": 1, "email_append_to": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"naming_series", "naming_series",
"supplier", "supplier",
"supplier_name", "supplier_name",
@@ -209,16 +208,6 @@
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
{
"allow_on_submit": 1,
"default": "{supplier_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@@ -1693,7 +1682,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-03-17 20:44:00.221219", "modified": "2026-03-25 11:45:38.696888",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",
@@ -1756,6 +1745,6 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"timeline_field": "supplier", "timeline_field": "supplier",
"title_field": "title", "title_field": "supplier_name",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController):
taxes_and_charges_deducted: DF.Currency taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None tc_name: DF.Link | None
terms: DF.TextEditor | None terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None to_date: DF.Date | None
total: DF.Currency total: DF.Currency
total_advance: DF.Currency total_advance: DF.Currency
@@ -333,9 +332,6 @@ class PurchaseInvoice(BuyingController):
if self.bill_date: if self.bill_date:
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date)) self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
else:
self.remarks = _("No Remarks")
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
if not self.credit_to: if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company) self.credit_to = get_party_account("Supplier", self.supplier, self.company)
@@ -617,12 +613,13 @@ class PurchaseInvoice(BuyingController):
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account) frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
def po_required(self): def po_required(self):
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes": if (
if frappe.get_value( frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
and not self.is_internal_transfer()
and not frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
)
): ):
return
for d in self.get("items"): for d in self.get("items"):
if not d.purchase_order: if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
@@ -983,6 +980,10 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items: if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts() self.get_provisional_accounts()
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate): if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code: if item.item_code:
@@ -1161,7 +1162,11 @@ class PurchaseInvoice(BuyingController):
) )
# check if the exchange rate has changed # check if the exchange rate has changed
if item.get("purchase_receipt") and self.auto_accounting_for_stock: if (
not adjust_incoming_rate
and item.get("purchase_receipt")
and self.auto_accounting_for_stock
):
if ( if (
exchange_rate_map[item.purchase_receipt] exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt] and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
@@ -1198,6 +1203,7 @@ class PurchaseInvoice(BuyingController):
item=item, item=item,
) )
) )
if ( if (
self.auto_accounting_for_stock self.auto_accounting_for_stock
and self.is_opening == "No" and self.is_opening == "No"

View File

@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice, make_purchase_invoice as create_purchase_invoice,
) )
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
# fetching the latest GL Entry with exchange gain and loss account account # fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value( amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit" "GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
) )
discrepancy_caused_by_exchange_rate_diff = abs( discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[0].base_net_amount - pr.items[0].base_net_amount pi.items[0].base_net_amount - pr.items[0].base_net_amount
) )
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self): def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice, make_purchase_invoice as create_purchase_invoice,
@@ -2189,11 +2200,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
def test_offsetting_entries_for_accounting_dimensions(self): def test_offsetting_entries_for_accounting_dimensions(self):
from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.report.trial_balance.test_trial_balance import (
clear_dimension_defaults,
create_accounting_dimension,
disable_dimension,
)
create_account( create_account(
account_name="Offsetting", account_name="Offsetting",
@@ -2201,7 +2207,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
parent_account="Temporary Accounts - _TC", parent_account="Temporary Accounts - _TC",
) )
create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") dim = frappe.get_doc("Accounting Dimension", "Branch")
dim.append(
"dimension_defaults",
{
"company": "_Test Company",
"reference_document": "Branch",
"offsetting_account": "Offsetting - _TC",
},
)
dim.save()
branch1 = frappe.new_doc("Branch") branch1 = frappe.new_doc("Branch")
branch1.branch = "Location 1" branch1.branch = "Location 1"
@@ -2238,8 +2253,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
voucher_type="Purchase Invoice", voucher_type="Purchase Invoice",
additional_columns=["branch"], additional_columns=["branch"],
) )
clear_dimension_defaults("Branch")
disable_dimension()
def test_repost_accounting_entries(self): def test_repost_accounting_entries(self):
# update repost settings # update repost settings

View File

@@ -190,6 +190,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Received Qty", "label": "Received Qty",
"no_copy": 1, "no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -206,7 +207,8 @@
{ {
"fieldname": "rejected_qty", "fieldname": "rejected_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Rejected Qty" "label": "Rejected Qty",
"print_hide": 1
}, },
{ {
"depends_on": "eval:doc.uom != doc.stock_uom", "depends_on": "eval:doc.uom != doc.stock_uom",
@@ -226,6 +228,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "UOM", "label": "UOM",
"options": "UOM", "options": "UOM",
"print_hide": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -261,14 +264,16 @@
"depends_on": "price_list_rate", "depends_on": "price_list_rate",
"fieldname": "discount_percentage", "fieldname": "discount_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Discount on Price List Rate (%)" "label": "Discount on Price List Rate (%)",
"print_hide": 1
}, },
{ {
"depends_on": "price_list_rate", "depends_on": "price_list_rate",
"fieldname": "discount_amount", "fieldname": "discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Discount Amount", "label": "Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"fieldname": "col_break3", "fieldname": "col_break3",
@@ -401,12 +406,14 @@
{ {
"fieldname": "weight_per_unit", "fieldname": "weight_per_unit",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Weight Per Unit" "label": "Weight Per Unit",
"print_hide": 1
}, },
{ {
"fieldname": "total_weight", "fieldname": "total_weight",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Total Weight", "label": "Total Weight",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -417,7 +424,8 @@
"fieldname": "weight_uom", "fieldname": "weight_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Weight UOM", "label": "Weight UOM",
"options": "UOM" "options": "UOM",
"print_hide": 1
}, },
{ {
"depends_on": "eval:parent.update_stock", "depends_on": "eval:parent.update_stock",
@@ -429,7 +437,8 @@
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Accepted Warehouse", "label": "Accepted Warehouse",
"options": "Warehouse" "options": "Warehouse",
"print_hide": 1
}, },
{ {
"fieldname": "rejected_warehouse", "fieldname": "rejected_warehouse",
@@ -674,7 +683,8 @@
"fieldname": "asset_location", "fieldname": "asset_location",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Asset Location", "label": "Asset Location",
"options": "Location" "options": "Location",
"print_hide": 1
}, },
{ {
"fieldname": "po_detail", "fieldname": "po_detail",
@@ -730,7 +740,6 @@
"label": "Valuation Rate", "label": "Valuation Rate",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -796,6 +805,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Asset Category", "label": "Asset Category",
"options": "Asset Category", "options": "Asset Category",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -828,6 +838,7 @@
"label": "Rate of Stock UOM", "label": "Rate of Stock UOM",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -866,6 +877,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate With Margin", "label": "Rate With Margin",
"options": "currency", "options": "currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -892,7 +904,8 @@
"default": "1", "default": "1",
"fieldname": "apply_tds", "fieldname": "apply_tds",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Consider for Tax Withholding" "label": "Consider for Tax Withholding",
"print_hide": 1
}, },
{ {
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
@@ -918,7 +931,8 @@
"fieldname": "wip_composite_asset", "fieldname": "wip_composite_asset",
"fieldtype": "Link", "fieldtype": "Link",
"label": "WIP Composite Asset", "label": "WIP Composite Asset",
"options": "Asset" "options": "Asset",
"print_hide": 1
}, },
{ {
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0", "depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
@@ -930,7 +944,8 @@
"default": "0", "default": "0",
"fieldname": "use_serial_batch_fields", "fieldname": "use_serial_batch_fields",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Serial No / Batch Fields" "label": "Use Serial No / Batch Fields",
"print_hide": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
@@ -977,7 +992,8 @@
"fieldname": "distributed_discount_amount", "fieldname": "distributed_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Distributed Discount Amount", "label": "Distributed Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"fieldname": "tax_withholding_category", "fieldname": "tax_withholding_category",
@@ -991,7 +1007,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-02-15 21:07:49.455930", "modified": "2026-04-07 15:40:45.687554",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -9,29 +9,25 @@ from frappe.utils import add_days, nowdate, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite): class TestRepostAccountingLedger(ERPNextTestSuite):
def setUp(self): def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0) frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
update_repost_settings() update_repost_settings()
def test_01_basic_functions(self): def test_01_basic_functions(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item="_Test Item",
company=self.company, company="_Test Company",
customer=self.customer, customer="_Test Customer",
debit_to=self.debit_to, debit_to="Debtors - _TC",
parent_cost_center=self.cost_center, parent_cost_center="Main - _TC",
cost_center=self.cost_center, cost_center="Main - _TC",
rate=100, rate=100,
) )
@@ -48,7 +44,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# Test Validation Error # Test Validation Error
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = "_Test Company"
ral.delete_cancelled_entries = True ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append( ral.append(
@@ -65,7 +61,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
ral.save() ral.save()
# manually set an incorrect debit amount in DB # manually set an incorrect debit amount in DB
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to}) gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": "Debtors - _TC"})
frappe.db.set_value("GL Entry", gle[0], "debit", 90) frappe.db.set_value("GL Entry", gle[0], "debit", 90)
gl = qb.DocType("GL Entry") gl = qb.DocType("GL Entry")
@@ -94,23 +90,23 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_02_deferred_accounting_valiations(self): def test_02_deferred_accounting_valiations(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item="_Test Item",
company=self.company, company="_Test Company",
customer=self.customer, customer="_Test Customer",
debit_to=self.debit_to, debit_to="Debtors - _TC",
parent_cost_center=self.cost_center, parent_cost_center="Main - _TC",
cost_center=self.cost_center, cost_center="Main - _TC",
rate=100, rate=100,
do_not_submit=True, do_not_submit=True,
) )
si.items[0].enable_deferred_revenue = True si.items[0].enable_deferred_revenue = True
si.items[0].deferred_revenue_account = self.deferred_revenue si.items[0].deferred_revenue_account = "Deferred Revenue - _TC"
si.items[0].service_start_date = nowdate() si.items[0].service_start_date = nowdate()
si.items[0].service_end_date = add_days(nowdate(), 90) si.items[0].service_end_date = add_days(nowdate(), 90)
si.save().submit() si.save().submit()
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = "_Test Company"
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
self.assertRaises(frappe.ValidationError, ral.save) self.assertRaises(frappe.ValidationError, ral.save)
@@ -118,35 +114,35 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_04_pcv_validation(self): def test_04_pcv_validation(self):
# Clear old GL entries so PCV can be submitted. # Clear old GL entries so PCV can be submitted.
gl = frappe.qb.DocType("GL Entry") gl = frappe.qb.DocType("GL Entry")
qb.from_(gl).delete().where(gl.company == self.company).run() qb.from_(gl).delete().where(gl.company == "_Test Company").run()
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item="_Test Item",
company=self.company, company="_Test Company",
customer=self.customer, customer="_Test Customer",
debit_to=self.debit_to, debit_to="Debtors - _TC",
parent_cost_center=self.cost_center, parent_cost_center="Main - _TC",
cost_center=self.cost_center, cost_center="Main - _TC",
rate=100, rate=100,
) )
fy = get_fiscal_year(today(), company=self.company) fy = get_fiscal_year(today(), company="_Test Company")
pcv = frappe.get_doc( pcv = frappe.get_doc(
{ {
"doctype": "Period Closing Voucher", "doctype": "Period Closing Voucher",
"transaction_date": today(), "transaction_date": today(),
"period_start_date": fy[1], "period_start_date": fy[1],
"period_end_date": today(), "period_end_date": today(),
"company": self.company, "company": "_Test Company",
"fiscal_year": fy[0], "fiscal_year": fy[0],
"cost_center": self.cost_center, "cost_center": "Main - _TC",
"closing_account_head": self.retained_earnings, "closing_account_head": "Retained Earnings - _TC",
"remarks": "test", "remarks": "test",
} }
) )
pcv.save().submit() pcv.save().submit()
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = "_Test Company"
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
self.assertRaises(frappe.ValidationError, ral.save) self.assertRaises(frappe.ValidationError, ral.save)
@@ -156,12 +152,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_03_deletion_flag_and_preview_function(self): def test_03_deletion_flag_and_preview_function(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item="_Test Item",
company=self.company, company="_Test Company",
customer=self.customer, customer="_Test Customer",
debit_to=self.debit_to, debit_to="Debtors - _TC",
parent_cost_center=self.cost_center, parent_cost_center="Main - _TC",
cost_center=self.cost_center, cost_center="Main - _TC",
rate=100, rate=100,
) )
@@ -170,7 +166,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# with deletion flag set # with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = "_Test Company"
ral.delete_cancelled_entries = True ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
@@ -181,12 +177,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_05_without_deletion_flag(self): def test_05_without_deletion_flag(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item="_Test Item",
company=self.company, company="_Test Company",
customer=self.customer, customer="_Test Customer",
debit_to=self.debit_to, debit_to="Debtors - _TC",
parent_cost_center=self.cost_center, parent_cost_center="Main - _TC",
cost_center=self.cost_center, cost_center="Main - _TC",
rate=100, rate=100,
) )
@@ -195,7 +191,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# without deletion flag set # without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = "_Test Company"
ral.delete_cancelled_entries = False ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
@@ -210,16 +206,16 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
provisional_account = create_account( provisional_account = create_account(
account_name="Provision Account", account_name="Provision Account",
parent_account="Current Liabilities - _TC", parent_account="Current Liabilities - _TC",
company=self.company, company="_Test Company",
) )
another_provisional_account = create_account( another_provisional_account = create_account(
account_name="Another Provision Account", account_name="Another Provision Account",
parent_account="Current Liabilities - _TC", parent_account="Current Liabilities - _TC",
company=self.company, company="_Test Company",
) )
company = frappe.get_doc("Company", self.company) company = frappe.get_doc("Company", "_Test Company")
company.enable_provisional_accounting_for_non_stock_items = 1 company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account company.default_provisional_account = provisional_account
company.save() company.save()
@@ -229,7 +225,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
item = make_item(properties={"is_stock_item": 0}) item = make_item(properties={"is_stock_item": 0})
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0) pr = make_purchase_receipt(company="_Test Company", item_code=item.name, rate=1000.0, qty=1.0)
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles = [ expected_pr_gles = [
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, {"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
@@ -246,7 +242,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
) )
repost_doc = frappe.new_doc("Repost Accounting Ledger") repost_doc = frappe.new_doc("Repost Accounting Ledger")
repost_doc.company = self.company repost_doc.company = "_Test Company"
repost_doc.delete_cancelled_entries = True repost_doc.delete_cancelled_entries = True
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name}) repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
repost_doc.save().submit() repost_doc.save().submit()

View File

@@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
); );
} }
} }
this.toggle_get_items();
// Show buttons only when pos view is active
if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.delivery_note_btn();
this.frm.cscript.quotation_btn();
}
this.set_default_print_format(); this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
@@ -260,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
} }
} }
toggle_get_items() {
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
buttons.forEach((label) => {
this.frm.remove_custom_button(label, "Get Items From");
});
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
return;
}
if (!this.frm.doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.quotation_btn();
this.frm.cscript.timesheet_btn();
}
this.frm.cscript.delivery_note_btn();
}
timesheet_btn() {
var me = this;
me.frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: me.frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: me.frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
me.frm.events.add_timesheet_data(me.frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
sales_order_btn() { sales_order_btn() {
var me = this; var me = this;
@@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button( this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"), __("Delivery Note"),
function () { function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({ erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note", source_doctype: "Delivery Note",
@@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = { var filters = {
docstatus: 1, docstatus: 1,
company: me.frm.doc.company, company: me.frm.doc.company,
is_return: 0, is_return: me.frm.doc.is_return,
}; };
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer; if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return { return {
@@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
apply_tds(frm) { apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries"); this.frm.clear_table("tax_withholding_entries");
} }
is_return() {
this.toggle_get_items();
}
}; };
// for backward compatibility: combine new and previous states // for backward compatibility: combine new and previous states
@@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", {
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) { if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against")); frm.set_df_property("return_against", "label", __("Adjustment Against"));
} }

View File

@@ -1101,9 +1101,6 @@ class SalesInvoice(SellingController):
if self.po_date: if self.po_date:
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date)) self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended
if self.is_new() and self.amended_from: if self.is_new() and self.amended_from:

View File

@@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True}) @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True})
def test_rounding_adjustment_3(self): def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension
# Dimension creates custom field, which does an implicit DB commit as it is a DDL command
# Ensure dimension don't have any mandatory fields
create_dimension()
# rollback from tearDown() happens till here
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)
si.items = [] si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@@ -2894,7 +2887,7 @@ class TestSalesInvoice(ERPNextTestSuite):
si.submit() si.submit()
# Check if adjustment entry is created # Check if adjustment entry is created
self.assertTrue( self.assertFalse(
frappe.db.exists( frappe.db.exists(
"GL Entry", "GL Entry",
{ {

View File

@@ -207,6 +207,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
"options": "UOM", "options": "UOM",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -310,7 +311,8 @@
"fieldname": "discount_amount", "fieldname": "discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Discount Amount", "label": "Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
@@ -853,6 +855,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate of Stock UOM", "label": "Rate of Stock UOM",
"no_copy": 1, "no_copy": 1,
"print_hide": 1,
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
}, },
@@ -869,6 +872,7 @@
"fieldname": "grant_commission", "fieldname": "grant_commission",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Grant Commission", "label": "Grant Commission",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -926,7 +930,8 @@
"default": "0", "default": "0",
"fieldname": "use_serial_batch_fields", "fieldname": "use_serial_batch_fields",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Serial No / Batch Fields" "label": "Use Serial No / Batch Fields",
"print_hide": 1
}, },
{ {
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
@@ -941,7 +946,8 @@
"fieldname": "distributed_discount_amount", "fieldname": "distributed_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Distributed Discount Amount", "label": "Distributed Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"fieldname": "available_quantity_section", "fieldname": "available_quantity_section",
@@ -1010,7 +1016,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-02-23 14:37:14.853941", "modified": "2026-02-24 14:37:16.853941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -772,6 +772,7 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No
try: try:
subscription = frappe.get_doc("Subscription", subscription_name) subscription = frappe.get_doc("Subscription", subscription_name)
subscription.process(posting_date) subscription.process(posting_date)
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
except frappe.ValidationError: except frappe.ValidationError:
frappe.db.rollback() frappe.db.rollback()

View File

@@ -128,6 +128,7 @@ class TaxWithholdingDetails:
self.party_type = party_type self.party_type = party_type
self.party = party self.party = party
self.company = company self.company = company
self.tax_id = get_tax_id_for_party(self.party_type, self.party)
def get(self) -> list: def get(self) -> list:
""" """
@@ -161,6 +162,7 @@ class TaxWithholdingDetails:
disable_cumulative_threshold=doc.disable_cumulative_threshold, disable_cumulative_threshold=doc.disable_cumulative_threshold,
disable_transaction_threshold=doc.disable_transaction_threshold, disable_transaction_threshold=doc.disable_transaction_threshold,
taxable_amount=0, taxable_amount=0,
tax_id=self.tax_id,
) )
# ldc (only if valid based on posting date) # ldc (only if valid based on posting date)
@@ -181,17 +183,13 @@ class TaxWithholdingDetails:
if self.party_type != "Supplier": if self.party_type != "Supplier":
return ldc_details return ldc_details
# NOTE: This can be a configurable option
# To check if filter by tax_id is needed
tax_id = get_tax_id_for_party(self.party_type, self.party)
# ldc details # ldc details
ldc_records = self.get_valid_ldc_records(tax_id) ldc_records = self.get_valid_ldc_records(self.tax_id)
if not ldc_records: if not ldc_records:
return ldc_details return ldc_details
ldc_names = [ldc.name for ldc in ldc_records] ldc_names = [ldc.name for ldc in ldc_records]
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id) ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, self.tax_id)
# map # map
for ldc in ldc_records: for ldc in ldc_records:
@@ -254,4 +252,5 @@ class TaxWithholdingDetails:
@allow_regional @allow_regional
def get_tax_id_for_party(party_type, party): def get_tax_id_for_party(party_type, party):
return None # cannot use tax_id from doc because payment and journal entry do not have tax_id field.\
return frappe.db.get_value(party_type, party, "tax_id")

View File

@@ -2,6 +2,7 @@
# See license.txt # See license.txt
import datetime import datetime
from unittest.mock import patch
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -18,7 +19,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
# create relevant supplier, etc # create relevant supplier, etc
create_records() create_records()
create_tax_withholding_category_records() create_tax_withholding_category_records()
make_pan_no_field()
def validate_tax_withholding_entries(self, doctype, docname, expected_entries): def validate_tax_withholding_entries(self, doctype, docname, expected_entries):
"""Validate tax withholding entries for a document""" """Validate tax withholding entries for a document"""
@@ -3542,6 +3542,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000) entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save) self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save)
def test_tax_id_is_set_in_all_generated_entries_from_party_doctype(self):
self.setup_party_with_category("Supplier", "Test TDS Supplier3", "New TDS Category")
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_id", "ABCTY1234D")
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=40000)
pi.submit()
entries = frappe.get_all(
"Tax Withholding Entry",
filters={"parenttype": "Purchase Invoice", "parent": pi.name},
fields=["name", "tax_id"],
)
self.assertTrue(entries)
self.assertTrue(all(entry.tax_id == "ABCTY1234D" for entry in entries))
def test_threshold_considers_two_parties_with_same_tax_id_with_overrided_hook(self):
self.setup_party_with_category("Supplier", "Test TDS Supplier1", "Cumulative Threshold TDS")
self.setup_party_with_category("Supplier", "Test TDS Supplier2", "Cumulative Threshold TDS")
with patch(
"erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category.get_tax_id_for_party",
return_value="AAAPL1234C",
):
pi1 = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000)
pi1.submit()
pi2 = create_purchase_invoice(supplier="Test TDS Supplier2", rate=20000)
pi2.submit()
entries = frappe.get_all(
"Tax Withholding Entry",
filters={"parenttype": "Purchase Invoice", "parent": pi2.name},
fields=["status", "withholding_amount"],
)
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0].status, "Settled")
self.assertEqual(entries[0].withholding_amount, 2000.0)
def create_purchase_invoice(**args): def create_purchase_invoice(**args):
# return sales invoice doc object # return sales invoice doc object
@@ -3998,18 +4039,3 @@ def create_lower_deduction_certificate(
"certificate_limit": limit, "certificate_limit": limit,
} }
).insert() ).insert()
def make_pan_no_field():
pan_field = {
"Supplier": [
{
"fieldname": "pan",
"label": "PAN",
"fieldtype": "Data",
"translatable": 0,
}
]
}
create_custom_fields(pan_field, update=1)

View File

@@ -344,7 +344,6 @@ class TaxWithholdingEntry(Document):
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
TaxWithholdingDetails, TaxWithholdingDetails,
get_tax_id_for_party,
) )
@@ -646,8 +645,11 @@ class TaxWithholdingController:
# NOTE: This can be a configurable option # NOTE: This can be a configurable option
# To check if filter by tax_id is needed # To check if filter by tax_id is needed
tax_id = get_tax_id_for_party(self.party_type, self.party) query = (
query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party) query.where(entry.tax_id == category.tax_id)
if category.tax_id
else query.where(entry.party == self.party)
)
return query return query
@@ -686,6 +688,7 @@ class TaxWithholdingController:
"company": self.doc.company, "company": self.doc.company,
"party_type": self.party_type, "party_type": self.party_type,
"party": self.party, "party": self.party,
"tax_id": category.tax_id,
"tax_withholding_category": category.name, "tax_withholding_category": category.name,
"tax_withholding_group": category.tax_withholding_group, "tax_withholding_group": category.tax_withholding_group,
"tax_rate": category.tax_rate, "tax_rate": category.tax_rate,
@@ -1052,6 +1055,7 @@ class TaxWithholdingController:
"party_type": self.party_type, "party_type": self.party_type,
"party": self.party, "party": self.party,
"company": self.doc.company, "company": self.doc.company,
"tax_id": category.tax_id,
} }
) )
return entry return entry

View File

@@ -14,7 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite): class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite): class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -10,7 +10,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
@@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] expected_data = [[100, 30], [100, 50], [100, 20]]
for i in range(3): for i in range(3):
row = report[1][i - 1] row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
# check invoice grand total, invoiced, paid and outstanding column's value after payment # check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name) self.create_payment_entry(si.name)
report = execute(filters) report = execute(filters)
@@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] expected_data = [[100, 30], [100, 50], [100, 20]]
for i in range(3): for i in range(3):
row = report[1][i - 1] row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
# check invoice grand total, invoiced, paid and outstanding column's value after credit note # check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True) cr_note = self.create_credit_note(si.name, do_not_submit=True)
@@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
row = report[1][0] row = report[1][0]
expected_data = [8000, 8000, "No Remarks"] # Data in company currency expected_data = [8000, 8000] # Data in company currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
# CASE 2: Transaction currency and party account currency are the same # CASE 2: Transaction currency and party account currency are the same
self.create_customer( self.create_customer(
@@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
row = report[1][0] row = report[1][0]
expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency expected_data = [100, 100] # Data in Part Account Currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
# View in Company currency # View in Company currency
filters.pop("in_party_currency") filters.pop("in_party_currency")
report = execute(filters) report = execute(filters)
row = report[1][0] row = report[1][0]
expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency expected_data = [8000, 8000] # Data in Company Currency
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
def test_accounts_receivable_with_partial_payment(self): def test_accounts_receivable_with_partial_payment(self):
filters = { filters = {
@@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]] expected_data = [[200, 60], [200, 100], [200, 40]]
for i in range(3): for i in range(3):
row = report[1][i - 1] row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
# check invoice grand total, invoiced, paid and outstanding column's value after payment # check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name) self.create_payment_entry(si.name)
@@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters) report = execute(filters)
expected_data = [100, 100, "No Remarks"] expected_data = [100, 100]
self.assertEqual(len(report[1]), 1) self.assertEqual(len(report[1]), 1)
row = report[1][0] row = report[1][0]
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
self.assertFalse(row.get("remarks"))
# check invoice grand total, invoiced, paid and outstanding column's value after payment # check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name) self.create_payment_entry(si.name)

View File

@@ -8,7 +8,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.maxDiff = None self.maxDiff = None
self.create_company() self.create_company()

View File

@@ -10,7 +10,7 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite): class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -48,6 +48,9 @@ class Deferred_Item:
Generate report data for output Generate report data for output
""" """
ret_data = frappe._dict({"name": self.item_name}) ret_data = frappe._dict({"name": self.item_name})
ret_data.service_start_date = self.service_start_date
ret_data.service_end_date = self.service_end_date
ret_data.amount = self.base_net_amount
for period in self.period_total: for period in self.period_total:
ret_data[period.key] = period.total ret_data[period.key] = period.total
ret_data.indent = 1 ret_data.indent = 1
@@ -205,6 +208,9 @@ class Deferred_Invoice:
for item in self.uniq_items: for item in self.uniq_items:
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
# roll-up amount from all deferred items
self.amount_total = sum(item.base_net_amount for item in self.items)
def calculate_invoice_revenue_expense_for_period(self): def calculate_invoice_revenue_expense_for_period(self):
""" """
calculate deferred revenue/expense for all items in invoice calculate deferred revenue/expense for all items in invoice
@@ -232,7 +238,7 @@ class Deferred_Invoice:
generate report data for invoice, includes invoice total generate report data for invoice, includes invoice total
""" """
ret_data = [] ret_data = []
inv_total = frappe._dict({"name": self.name}) inv_total = frappe._dict({"name": self.name, "amount": self.amount_total})
for x in self.period_total: for x in self.period_total:
inv_total[x.key] = x.total inv_total[x.key] = x.total
inv_total.indent = 0 inv_total.indent = 0
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
def get_columns(self): def get_columns(self):
columns = [] columns = []
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
columns.append(
{
"label": _("Service Start Date"),
"fieldname": "service_start_date",
"fieldtype": "Date",
"read_only": 1,
}
)
columns.append(
{
"label": _("Service End Date"),
"fieldname": "service_end_date",
"fieldtype": "Date",
"read_only": 1,
}
)
columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1})
for period in self.period_list: for period in self.period_list:
columns.append( columns.append(
{ {
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
elif self.filters.type == "Expense": elif self.filters.type == "Expense":
total_row = frappe._dict({"name": "Total Deferred Expense"}) total_row = frappe._dict({"name": "Total Deferred Expense"})
total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices)
for idx, period in enumerate(self.period_list, 0): for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total total_row[period.key] = self.period_total[idx].total
ret.append(total_row) ret.append(total_row)

View File

@@ -142,6 +142,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
total = 0 total = 0
row = { row = {
"account": d.name, "account": d.name,
"is_group": d.is_group,
"parent_account": d.parent_account, "parent_account": d.parent_account,
"indent": d.indent, "indent": d.indent,
"from_date": filters.from_date, "from_date": filters.from_date,

View File

@@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = {
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type", options: "party_type",
depends_on: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestItemWisePurchaseRegister(AccountsTestMixin, ERPNextTestSuite): class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_supplier() self.create_supplier()

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite): class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -12,7 +12,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestProfitAndLossStatement(AccountsTestMixin, ERPNextTestSuite): class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite): class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestSupplierLedgerSummary(AccountsTestMixin, ERPNextTestSuite): class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_supplier() self.create_supplier()

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestTaxWithholdingDetails(AccountsTestMixin, ERPNextTestSuite): class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.clear_old_entries() self.clear_old_entries()

View File

@@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
self.company = create_company()
create_cost_center( create_cost_center(
cost_center_name="Test Cost Center", cost_center_name="Test Cost Center",
company="Trial Balance Company", company="Trial Balance Company",
@@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite):
parent_account="Temporary Accounts - TBC", parent_account="Temporary Accounts - TBC",
) )
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
create_accounting_dimension() dim = frappe.get_doc("Accounting Dimension", "Branch")
dim.append(
"dimension_defaults",
{
"company": "Trial Balance Company",
"automatically_post_balancing_accounting_entry": 1,
"offsetting_account": "Offsetting - TBC",
},
)
dim.save()
def test_offsetting_entries_for_accounting_dimensions(self): def test_offsetting_entries_for_accounting_dimensions(self):
""" """
@@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite):
branch2.insert(ignore_if_duplicate=True) branch2.insert(ignore_if_duplicate=True)
si = create_sales_invoice( si = create_sales_invoice(
company=self.company, company="Trial Balance Company",
debit_to="Debtors - TBC", debit_to="Debtors - TBC",
cost_center="Test Cost Center - TBC", cost_center="Test Cost Center - TBC",
income_account="Sales - TBC", income_account="Sales - TBC",
@@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite):
si.submit() si.submit()
filters = frappe._dict( filters = frappe._dict(
{"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} {"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
) )
total_row = execute(filters)[1][-1] total_row = execute(filters)[1][-1]
self.assertEqual(total_row["debit"], total_row["credit"]) self.assertEqual(total_row["debit"], total_row["credit"])
def create_company(**args):
args = frappe._dict(args)
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": args.company_name or "Trial Balance Company",
"country": args.country or "India",
"default_currency": args.currency or "INR",
"parent_company": args.get("parent_company"),
"is_group": args.get("is_group"),
}
)
company.insert(ignore_if_duplicate=True)
return company.name
def create_accounting_dimension(**args):
args = frappe._dict(args)
document_type = args.document_type or "Branch"
if frappe.db.exists("Accounting Dimension", document_type):
accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
accounting_dimension.disabled = 0
else:
accounting_dimension = frappe.new_doc("Accounting Dimension")
accounting_dimension.document_type = document_type
accounting_dimension.insert()
accounting_dimension.set("dimension_defaults", [])
accounting_dimension.append(
"dimension_defaults",
{
"company": args.company or "Trial Balance Company",
"automatically_post_balancing_accounting_entry": 1,
"offsetting_account": args.offsetting_account or "Offsetting - TBC",
},
)
accounting_dimension.save()
def disable_dimension(**args):
args = frappe._dict(args)
document_type = args.document_type or "Branch"
dimension = frappe.get_doc("Accounting Dimension", document_type)
dimension.disabled = 1
dimension.save()
def clear_dimension_defaults(dimension_name):
accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
accounting_dimension.dimension_defaults = []
accounting_dimension.save()

View File

@@ -229,23 +229,3 @@ class AccountsTestMixin:
] ]
for doctype in doctype_list: for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_price_list(self):
pl_name = "Mixin Price List"
if not frappe.db.exists("Price List", pl_name):
self.price_list = (
frappe.get_doc(
{
"doctype": "Price List",
"currency": "INR",
"enabled": True,
"selling": True,
"buying": True,
"price_list_name": pl_name,
}
)
.insert()
.name
)
else:
self.price_list = frappe.get_doc("Price List", pl_name).name

View File

@@ -1404,6 +1404,78 @@ def get_account_balances(accounts, company, finance_book=None, include_default_f
return accounts return accounts
@frappe.whitelist()
def get_account_balances_coa(company: str, include_default_fb_balances: bool = False):
company_currency = frappe.get_cached_value("Company", company, "default_currency")
Account = DocType("Account")
account_list = (
frappe.qb.from_(Account)
.select(Account.name, Account.parent_account, Account.account_currency)
.where(Account.company == company)
.orderby(Account.lft)
.run(as_dict=True)
)
account_balances_cc = {account.get("name"): 0 for account in account_list}
account_balances_ac = {account.get("name"): 0 for account in account_list}
GLEntry = DocType("GL Entry")
precision = get_currency_precision()
get_ledger_balances_query = (
frappe.qb.from_(GLEntry)
.select(
GLEntry.account,
(Sum(Round(GLEntry.debit, precision)) - Sum(Round(GLEntry.credit, precision))).as_("balance"),
(
Sum(Round(GLEntry.debit_in_account_currency, precision))
- Sum(Round(GLEntry.credit_in_account_currency, precision))
).as_("balance_in_account_currency"),
)
.groupby(GLEntry.account)
)
condition_list = [GLEntry.company == company, GLEntry.is_cancelled == 0]
default_finance_book = None
if include_default_fb_balances:
default_finance_book = frappe.get_cached_value("Company", company, "default_finance_book")
if default_finance_book:
condition_list.append(
(GLEntry.finance_book == default_finance_book) | (GLEntry.finance_book.isnull())
)
for condition in condition_list:
get_ledger_balances_query = get_ledger_balances_query.where(condition)
ledger_balances = get_ledger_balances_query.run(as_dict=True)
for ledger_entry in ledger_balances:
account_balances_cc[ledger_entry.get("account")] = ledger_entry.get("balance")
account_balances_ac[ledger_entry.get("account")] = ledger_entry.get("balance_in_account_currency")
for account in reversed(account_list):
parent = account.get("parent_account")
if parent:
account_balances_cc[parent] += account_balances_cc.get(account.get("name"))
accounts_data = [
{
"value": account.get("name"),
"company_currency": company_currency,
"balance": account_balances_cc.get(account.get("name")),
"account_currency": account.get("account_currency"),
"balance_in_account_currency": account_balances_ac.get(account.get("name")),
}
for account in account_list
]
return accounts_data
def create_payment_gateway_account(gateway, payment_channel="Email", company=None): def create_payment_gateway_account(gateway, payment_channel="Email", company=None):
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
@@ -1531,6 +1603,10 @@ def parse_naming_series_variable(doc, variable):
else: else:
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"} data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
if doc and doc.doctype in ["Batch", "Serial No"] and doc.reference_doctype and doc.reference_name:
doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
date = ( date = (
( (
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime")) getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))

View File

@@ -61,7 +61,9 @@ def book_depreciation_entries(date):
accounting_dimensions, accounting_dimensions,
) )
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
failed_assets.append(asset_name) failed_assets.append(asset_name)
@@ -71,6 +73,7 @@ def book_depreciation_entries(date):
if failed_assets: if failed_assets:
set_depr_entry_posting_status_for_failed_assets(failed_assets) set_depr_entry_posting_status_for_failed_assets(failed_assets)
notify_depr_entry_posting_error(failed_assets, error_logs) notify_depr_entry_posting_error(failed_assets, error_logs)
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()

View File

@@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite):
"asset_type": "Composite Component", "asset_type": "Composite Component",
"purchase_date": pr.posting_date, "purchase_date": pr.posting_date,
"available_for_use_date": pr.posting_date, "available_for_use_date": pr.posting_date,
"location": "Test Location",
} }
) )
consumed_asset_doc.save() consumed_asset_doc.save()

View File

@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
}); });
}, },
onload: (frm) => { refresh: (frm) => {
frm.trigger("set_required_fields"); frm.trigger("set_required_fields");
}, },

View File

@@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite):
frappe.db.set_value( frappe.db.set_value(
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
) )
make_location()
def test_movement(self): def test_movement(self):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
@@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite):
if asset.docstatus == 0: if asset.docstatus == 0:
asset.submit() asset.submit()
# check asset movement is created
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
create_asset_movement( create_asset_movement(
purpose="Transfer", purpose="Transfer",
company=asset.company, company=asset.company,
@@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite):
if asset.docstatus == 0: if asset.docstatus == 0:
asset.submit() asset.submit()
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name})
self.assertRaises(frappe.ValidationError, movement.cancel) self.assertRaises(frappe.ValidationError, movement.cancel)
@@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite):
asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.save().submit() asset.save().submit()
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
asset_creation_date = frappe.db.get_value( asset_creation_date = frappe.db.get_value(
"Asset Movement", "Asset Movement",
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]], [["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
@@ -197,9 +186,3 @@ def create_asset_movement(**args):
movement.submit() movement.submit()
return movement return movement
def make_location():
for location in ["Pune", "Mumbai", "Nagpur"]:
if not frappe.db.exists("Location", location):
frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True)

View File

@@ -130,7 +130,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Asset", "label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]", "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\"]]]",
"options": "Asset", "options": "Asset",
"reqd": 1 "reqd": 1
}, },

View File

@@ -9,7 +9,6 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"supplier_section", "supplier_section",
"title",
"naming_series", "naming_series",
"supplier", "supplier",
"supplier_name", "supplier_name",
@@ -172,17 +171,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"options": "fa fa-user" "options": "fa fa-user"
}, },
{
"allow_on_submit": 1,
"default": "{supplier_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1,
"reqd": 1
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@@ -1328,7 +1316,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-03-09 17:15:29.184682", "modified": "2026-03-25 11:46:18.748951",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController):
taxes_and_charges_deducted: DF.Currency taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None tc_name: DF.Link | None
terms: DF.TextEditor | None terms: DF.TextEditor | None
title: DF.Data
to_date: DF.Date | None to_date: DF.Date | None
total: DF.Currency total: DF.Currency
total_net_weight: DF.Float total_net_weight: DF.Float
@@ -780,6 +779,7 @@ def make_purchase_invoice_from_portal(purchase_order_name):
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
frappe.throw(_("Not Permitted"), frappe.PermissionError) frappe.throw(_("Not Permitted"), frappe.PermissionError)
doc.save() doc.save()
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
frappe.response["type"] = "redirect" frappe.response["type"] = "redirect"
frappe.response.location = "/purchase-invoices/" + doc.name frappe.response.location = "/purchase-invoices/" + doc.name
@@ -802,7 +802,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.set_payment_schedule() target.set_payment_schedule()
target.credit_to = get_party_account("Supplier", source.supplier, source.company) target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent):
def get_billed_qty(po_item_name): def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
@@ -814,6 +813,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
) )
return query.run(pluck="qty")[0] or 0 return query.run(pluck="qty")[0] or 0
def update_item(obj, target, source_parent):
billed_qty = flt(get_billed_qty(obj.name)) billed_qty = flt(get_billed_qty(obj.name))
target.qty = flt(obj.qty) - billed_qty target.qty = flt(obj.qty) - billed_qty
@@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"wip_composite_asset": "wip_composite_asset", "wip_composite_asset": "wip_composite_asset",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) "condition": lambda doc: (
doc.base_amount == 0
or abs(doc.billed_amt) < abs(doc.amount)
or doc.qty > flt(get_billed_qty(doc.name))
)
and select_item(doc), and select_item(doc),
}, },
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},

View File

@@ -291,6 +291,30 @@ class TestPurchaseOrder(ERPNextTestSuite):
# ordered qty should decrease (back to initial) on row deletion # ordered qty should decrease (back to initial) on row deletion
self.assertEqual(get_ordered_qty(), existing_ordered_qty) self.assertEqual(get_ordered_qty(), existing_ordered_qty)
def test_discount_amount_partial_purchase_receipt(self):
po = create_purchase_order(qty=4, rate=100, do_not_save=1)
po.apply_discount_on = "Grand Total"
po.discount_amount = 120
po.save()
po.submit()
self.assertEqual(po.grand_total, 280)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 3
pr1.save()
pr1.submit()
self.assertEqual(pr1.discount_amount, 120)
self.assertEqual(pr1.grand_total, 180)
pr2 = make_purchase_receipt(po.name)
pr2.save()
pr2.submit()
self.assertEqual(pr2.discount_amount, 0)
self.assertEqual(pr2.grand_total, 100)
def test_update_child_perm(self): def test_update_child_perm(self):
po = create_purchase_order(item_code="_Test Item", qty=4) po = create_purchase_order(item_code="_Test Item", qty=4)
@@ -1386,6 +1410,34 @@ class TestPurchaseOrder(ERPNextTestSuite):
self.assertEqual(pi_2.status, "Paid") self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed") self.assertEqual(po.status, "Completed")
def test_purchase_order_over_billing_missing_item(self):
item1 = make_item(
"_Test Item for Overbilling",
).name
item2 = make_item(
"_Test Item for Overbilling 2",
).name
po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1)
po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"})
po.taxes = []
po.insert()
po.submit()
pi1 = make_pi_from_po(po.name)
pi1.items[0].qty = 8
pi1.items[0].rate = 1250
pi1.remove(pi1.items[1])
pi1.insert()
pi1.submit()
self.assertEqual(pi1.grand_total, 10000.0)
self.assertTrue(len(pi1.items) == 1)
pi2 = make_pi_from_po(po.name)
self.assertEqual(len(pi2.items), 2)
def create_po_for_sc_testing(): def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -280,14 +280,16 @@
"depends_on": "price_list_rate", "depends_on": "price_list_rate",
"fieldname": "discount_percentage", "fieldname": "discount_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Discount on Price List Rate (%)" "label": "Discount on Price List Rate (%)",
"print_hide": 1
}, },
{ {
"depends_on": "price_list_rate", "depends_on": "price_list_rate",
"fieldname": "discount_amount", "fieldname": "discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Discount Amount", "label": "Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"fieldname": "col_break3", "fieldname": "col_break3",
@@ -428,6 +430,7 @@
"fieldname": "weight_per_unit", "fieldname": "weight_per_unit",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Weight Per Unit", "label": "Weight Per Unit",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -763,6 +766,7 @@
"label": "Rate of Stock UOM", "label": "Rate of Stock UOM",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -779,6 +783,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Available Qty at Company", "label": "Available Qty at Company",
"no_copy": 1, "no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -878,7 +883,8 @@
"fieldname": "fg_item_qty", "fieldname": "fg_item_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Finished Good Qty", "label": "Finished Good Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"print_hide": 1
}, },
{ {
"depends_on": "eval:parent.is_internal_supplier", "depends_on": "eval:parent.is_internal_supplier",
@@ -923,7 +929,8 @@
"fieldname": "distributed_discount_amount", "fieldname": "distributed_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Distributed Discount Amount", "label": "Distributed Discount Amount",
"options": "currency" "options": "currency",
"print_hide": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -934,6 +941,7 @@
"label": "Subcontracted Quantity", "label": "Subcontracted Quantity",
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
} }
], ],
@@ -942,7 +950,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-10-30 16:51:56.761673", "modified": "2025-11-30 16:51:57.761673",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", {
fieldname: "print_format", fieldname: "print_format",
options: "Print Format", options: "Print Format",
placeholder: "Standard", placeholder: "Standard",
default: frappe.get_meta("Request for Quotation").default_print_format || "",
get_query: () => { get_query: () => {
return { return {
filters: { filters: {

View File

@@ -9,8 +9,6 @@
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company", "company",
"billing_address",
"billing_address_display",
"vendor", "vendor",
"column_break1", "column_break1",
"transaction_date", "transaction_date",
@@ -43,7 +41,13 @@
"select_print_heading", "select_print_heading",
"letter_head", "letter_head",
"more_info", "more_info",
"opportunity" "opportunity",
"address_and_contact_tab",
"billing_address",
"billing_address_display",
"column_break_czul",
"shipping_address",
"shipping_address_display"
], ],
"fields": [ "fields": [
{ {
@@ -346,6 +350,27 @@
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"label": "Use HTML" "label": "Use HTML"
},
{
"fieldname": "address_and_contact_tab",
"fieldtype": "Tab Break",
"label": "Address & Contact"
},
{
"fieldname": "column_break_czul",
"fieldtype": "Column Break"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Company Shipping Address",
"options": "Address"
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Text Editor",
"label": "Shipping Address Details",
"read_only": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -353,7 +378,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-03-09 17:15:29.774614", "modified": "2026-03-19 15:27:56.730649",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController):
select_print_heading: DF.Link | None select_print_heading: DF.Link | None
send_attached_files: DF.Check send_attached_files: DF.Check
send_document_print: DF.Check send_document_print: DF.Check
shipping_address: DF.Link | None
shipping_address_display: DF.TextEditor | None
status: DF.Literal["", "Draft", "Submitted", "Cancelled"] status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
subject: DF.Data subject: DF.Data
suppliers: DF.Table[RequestforQuotationSupplier] suppliers: DF.Table[RequestforQuotationSupplier]
@@ -283,7 +285,7 @@ class RequestforQuotation(BuyingController):
} }
) )
user.save(ignore_permissions=True) user.save(ignore_permissions=True)
update_password_link = user.reset_password() update_password_link = user._reset_password()
return user, update_password_link return user, update_password_link

View File

@@ -167,6 +167,15 @@ def create_supplier(**args):
if not args.without_supplier_group: if not args.without_supplier_group:
doc.supplier_group = args.supplier_group or "Services" doc.supplier_group = args.supplier_group or "Services"
if args.get("party_account"):
doc.append(
"accounts",
{
"company": frappe.db.get_value("Account", args.get("party_account"), "company"),
"account": args.get("party_account"),
},
)
doc.insert() doc.insert()
return doc return doc

File diff suppressed because one or more lines are too long

View File

@@ -4324,6 +4324,8 @@ def get_missing_company_details(doctype, docname):
company = frappe.db.get_value(doctype, docname, "company") company = frappe.db.get_value(doctype, docname, "company")
if doctype in ["Purchase Order", "Purchase Invoice"]: if doctype in ["Purchase Order", "Purchase Invoice"]:
company_address = frappe.db.get_value(doctype, docname, "billing_address") company_address = frappe.db.get_value(doctype, docname, "billing_address")
elif doctype in ["Request for Quotation"]:
company_address = frappe.db.get_value(doctype, docname, "shipping_address")
else: else:
company_address = frappe.db.get_value(doctype, docname, "company_address") company_address = frappe.db.get_value(doctype, docname, "company_address")

View File

@@ -457,7 +457,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
) )
net_rate = item.base_net_amount net_rate = item.qty * item.base_net_rate
if item.sales_incoming_rate: # for internal transfer if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate net_rate = item.qty * item.sales_incoming_rate
@@ -503,11 +503,15 @@ class BuyingController(SubcontractingController):
if d.category not in ["Valuation", "Valuation and Total"]: if d.category not in ["Valuation", "Valuation and Total"]:
continue continue
amount = flt(d.base_tax_amount_after_discount_amount) * (
-1 if d.get("add_deduct_tax") == "Deduct" else 1
)
if d.charge_type == "On Net Total": if d.charge_type == "On Net Total":
total_valuation_amount += flt(d.base_tax_amount_after_discount_amount) total_valuation_amount += amount
tax_accounts.append(d.account_head) tax_accounts.append(d.account_head)
else: else:
total_actual_tax_amount += flt(d.base_tax_amount_after_discount_amount) total_actual_tax_amount += amount
return tax_accounts, total_valuation_amount, total_actual_tax_amount return tax_accounts, total_valuation_amount, total_actual_tax_amount

View File

@@ -364,38 +364,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): def get_delivery_notes_to_be_billed(
doctype = "Delivery Note" doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
):
DeliveryNote = frappe.qb.DocType("Delivery Note")
fields = get_fields(doctype, ["name", "customer", "posting_date"]) fields = get_fields(doctype, ["name", "customer", "posting_date"])
return frappe.db.sql( original_dn = (
""" frappe.qb.from_(DeliveryNote)
select {fields} .select(DeliveryNote.name)
from `tabDelivery Note` .where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
where `tabDelivery Note`.`{key}` like {txt} and )
`tabDelivery Note`.docstatus = 1
and status not in ('Stopped', 'Closed') {fcond} query = (
and ( frappe.qb.from_(DeliveryNote)
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) .select(*[DeliveryNote[f] for f in fields])
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100) .where(
or ( (DeliveryNote.docstatus == 1)
`tabDelivery Note`.is_return = 1 & (DeliveryNote.status.notin(["Stopped", "Closed"]))
and return_against in (select name from `tabDelivery Note` where per_billed < 100) & (DeliveryNote[searchfield].like(f"%{txt}%"))
& (
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
| (
(DeliveryNote.is_return == 1)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.return_against.isin(original_dn))
) )
) )
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
""".format(
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
key=searchfield,
fcond=get_filters_cond(doctype, filters, []),
mcond=get_match_cond(doctype),
start=start,
page_len=page_len,
txt="%(txt)s",
),
{"txt": ("%%%s%%" % txt)},
as_dict=as_dict,
) )
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)
@frappe.whitelist() @frappe.whitelist()
@@ -1002,3 +1007,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
limit_page_length=page_len, limit_page_length=page_len,
as_list=1, as_list=1,
) )
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
table = frappe.qb.DocType(doctype)
child_table = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(table)
.inner_join(child_table)
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
.select(table.name)
.where(
(child_table.link_name == filters.get("warehouse"))
& (table.disabled == 0)
& (child_table.link_doctype == "Warehouse")
& (table.name.like(f"%{txt}%"))
)
.offset(start)
.limit(page_len)
)
return query.run(as_list=1)

View File

@@ -634,11 +634,11 @@ class SellingController(StockController):
if allow_at_arms_length_price: if allow_at_arms_length_price:
continue continue
rate = flt( rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"), if flt(d.rate, d.precision("incoming_rate")) != flt(
) rate, d.precision("incoming_rate")
if d.rate != rate: ):
d.rate = rate d.rate = rate
frappe.msgprint( frappe.msgprint(
_( _(

View File

@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
elif self.doctype == "Stock Entry" and row.t_warehouse: elif self.doctype == "Stock Entry" and row.t_warehouse:
qi_required = True # inward stock needs inspection qi_required = True # inward stock needs inspection
if row.get("is_scrap_item"): if row.get("type") or row.get("is_legacy_scrap_item"):
continue continue
if qi_required: # validate row only if inspection is required on item level if qi_required: # validate row only if inspection is required on item level

View File

@@ -160,7 +160,7 @@ class SubcontractingController(StockController):
).format(item.idx, get_link_to_form("Item", item.item_code)) ).format(item.idx, get_link_to_form("Item", item.item_code))
) )
if not item.get("is_scrap_item"): if not item.get("type") and not item.get("is_legacy_scrap_item"):
if not is_sub_contracted_item: if not is_sub_contracted_item:
frappe.throw( frappe.throw(
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
@@ -206,7 +206,7 @@ class SubcontractingController(StockController):
).format(item.idx, item.item_name) ).format(item.idx, item.item_name)
) )
if self.doctype != "Subcontracting Inward Order": if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
item.amount = item.qty * item.rate item.amount = item.qty * item.rate
if item.bom: if item.bom:
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
and self._doc_before_save and self._doc_before_save
): ):
for row in self._doc_before_save.get("items"): for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0)) item_dict[row.name] = (row.item_code, row.received_qty)
return item_dict return item_dict
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
self.__reference_name.append(row.name) self.__reference_name.append(row.name)
if (row.name not in item_dict) or ( if (row.name not in item_dict) or (
row.item_code, row.item_code,
row.qty + (row.get("rejected_qty") or 0), row.received_qty,
) != item_dict[row.name]: ) != item_dict[row.name]:
self.__changed_name.append(row.name) self.__changed_name.append(row.name)
@@ -962,7 +962,7 @@ class SubcontractingController(StockController):
): ):
qty = ( qty = (
flt(bom_item.qty_consumed_per_unit) flt(bom_item.qty_consumed_per_unit)
* flt(row.qty + (row.get("rejected_qty") or 0)) * flt(row.get("received_qty") or (row.qty + (row.get("rejected_qty") or 0)))
* row.conversion_factor * row.conversion_factor
) )
bom_item.main_item_code = row.item_code bom_item.main_item_code = row.item_code
@@ -1285,22 +1285,28 @@ class SubcontractingController(StockController):
if self.total_additional_costs: if self.total_additional_costs:
if self.distribute_additional_costs_based_on == "Amount": if self.distribute_additional_costs_based_on == "Amount":
total_amt = sum( total_amt = sum(
flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item") flt(item.amount)
for item in self.get("items")
if not item.get("type") and not item.get("is_legacy_scrap_item")
) )
for item in self.items: for item in self.items:
if not item.get("is_scrap_item"): if not item.get("type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = ( item.additional_cost_per_qty = (
(item.amount * self.total_additional_costs) / total_amt (item.amount * self.total_additional_costs) / total_amt
) / item.qty ) / item.qty
else: else:
total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item")) total_qty = sum(
flt(item.qty)
for item in self.get("items")
if not item.get("type") and not item.get("is_legacy_scrap_item")
)
additional_cost_per_qty = self.total_additional_costs / total_qty additional_cost_per_qty = self.total_additional_costs / total_qty
for item in self.items: for item in self.items:
if not item.get("is_scrap_item"): if not item.get("type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = additional_cost_per_qty item.additional_cost_per_qty = additional_cost_per_qty
else: else:
for item in self.items: for item in self.items:
if not item.get("is_scrap_item"): if not item.get("type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = 0 item.additional_cost_per_qty = 0
@frappe.whitelist() @frappe.whitelist()

View File

@@ -1,3 +1,5 @@
from collections import defaultdict
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.query_builder import Case from frappe.query_builder import Case
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
def on_submit_subcontracting_inward(self): def on_submit_subcontracting_inward(self):
self.update_inward_order_item() self.update_inward_order_item()
self.update_inward_order_received_items() self.update_inward_order_received_items()
self.update_inward_order_scrap_items() self.update_inward_order_secondary_items()
self.create_stock_reservation_entries_for_inward() self.create_stock_reservation_entries_for_inward()
self.update_inward_order_status() self.update_inward_order_status()
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
self.validate_delivery() self.validate_delivery()
self.validate_receive_from_customer_cancel() self.validate_receive_from_customer_cancel()
self.update_inward_order_received_items() self.update_inward_order_received_items()
self.update_inward_order_scrap_items() self.update_inward_order_secondary_items()
self.remove_reference_for_additional_items() self.remove_reference_for_additional_items()
self.update_inward_order_status() self.update_inward_order_status()
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
item item
for item in self.get("items") for item in self.get("items")
if not item.is_finished_item if not item.is_finished_item
and not item.is_scrap_item and not item.type
and not item.is_legacy_scrap_item
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item") and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
] ]
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
if self.subcontracting_inward_order: if self.subcontracting_inward_order:
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]: if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
for item in self.items: for item in self.items:
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0: if (
item.is_finished_item or item.type or item.is_legacy_scrap_item
) and item.valuation_rate == 0:
item.allow_zero_valuation_rate = 1 item.allow_zero_valuation_rate = 1
def validate_warehouse_(self): def validate_warehouse_(self):
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
self.validate_delivery_on_save() self.validate_delivery_on_save()
else: else:
for item in self.items: for item in self.items:
if not item.is_scrap_item: if not item.type and not item.is_legacy_scrap_item:
delivered_qty, returned_qty = frappe.get_value( delivered_qty, returned_qty = frappe.get_value(
"Subcontracting Inward Order Item", "Subcontracting Inward Order Item",
item.scio_detail, item.scio_detail,
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
if max_allowed_qty: if max_allowed_qty:
max_allowed_qty = max_allowed_qty[0] max_allowed_qty = max_allowed_qty[0]
else: else:
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item") table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
query = ( query = (
frappe.qb.from_(table) frappe.qb.from_(table)
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty")) .select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
bold( bold(
frappe.get_cached_value( frappe.get_cached_value(
"Subcontracting Inward Order Item" "Subcontracting Inward Order Item"
if not item.is_scrap_item if not item.type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Scrap Item", else "Subcontracting Inward Order Secondary Item",
item.scio_detail, item.scio_detail,
"stock_uom", "stock_uom",
) )
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
) )
for item in [item for item in self.items if not item.is_finished_item]: for item in [item for item in self.items if not item.is_finished_item]:
if item.is_scrap_item: if item.type or item.is_legacy_scrap_item:
scio_scrap_item = frappe.get_value( scio_secondary_item = frappe.get_value(
"Subcontracting Inward Order Scrap Item", "Subcontracting Inward Order Secondary Item",
{ {
"docstatus": 1, "docstatus": 1,
"item_code": item.item_code, "item_code": item.item_code,
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
as_dict=True, as_dict=True,
) )
if ( if (
scio_scrap_item scio_secondary_item
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty and scio_secondary_item.delivered_qty
> scio_secondary_item.produced_qty - item.transfer_qty
): ):
frappe.throw( frappe.throw(
_( _(
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered." "Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered."
).format(item.idx, get_link_to_form("Item", item.item_code)) ).format(item.idx, get_link_to_form("Item", item.item_code))
) )
else: else:
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
for item in self.items: for item in self.items:
doctype = ( doctype = (
"Subcontracting Inward Order Item" "Subcontracting Inward Order Item"
if not item.is_scrap_item if not item.type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Scrap Item" else "Subcontracting Inward Order Secondary Item"
) )
frappe.db.set_value( frappe.db.set_value(
doctype, doctype,
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
customer_warehouse = frappe.get_cached_value( customer_warehouse = frappe.get_cached_value(
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
) )
items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item] items = [
item
for item in self.items
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
]
item_code_wh = frappe._dict( item_code_wh = frappe._dict(
{ {
( (
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
doc.insert() doc.insert()
doc.submit() doc.submit()
def update_inward_order_scrap_items(self): def update_inward_order_secondary_items(self):
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture": if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
scrap_items_list = [item for item in self.items if item.is_scrap_item] secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
scrap_items = frappe._dict(
{ secondary_items = defaultdict(float)
(item.item_code, item.t_warehouse): item.transfer_qty for item in secondary_items_list:
if self._action == "submit" secondary_items[(item.item_code, item.t_warehouse)] += (
else -item.transfer_qty item.transfer_qty if self._action == "submit" else -item.transfer_qty
for item in scrap_items_list
}
) )
if scrap_items: secondary_items = frappe._dict(secondary_items)
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
if secondary_items:
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
item_codes = list(item_codes) item_codes = list(item_codes)
warehouses = list(warehouses) warehouses = list(warehouses)
result = frappe.get_all( result = frappe.get_all(
"Subcontracting Inward Order Scrap Item", "Subcontracting Inward Order Secondary Item",
filters={ filters={
"item_code": ["in", item_codes], "item_code": ["in", item_codes],
"warehouse": ["in", warehouses], "warehouse": ["in", warehouses],
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
) )
if result: if result:
scrap_item_dict = frappe._dict( secondary_items_dict = frappe._dict(
{ {
(d.item_code, d.warehouse): frappe._dict( (d.item_code, d.warehouse): frappe._dict(
{"name": d.name, "produced_qty": d.produced_qty} {"name": d.name, "produced_qty": d.produced_qty}
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
) )
deleted_docs = [] deleted_docs = []
case_expr = Case() case_expr = Case()
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item") table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
for key, value in scrap_item_dict.items(): for key, value in secondary_items_dict.items():
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0: if (
self._action == "cancel"
and value.produced_qty - abs(secondary_items.get(key)) == 0
):
deleted_docs.append(value.name) deleted_docs.append(value.name)
frappe.delete_doc("Subcontracting Inward Order Scrap Item", value.name) frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
else: else:
case_expr = case_expr.when( case_expr = case_expr.when(
table.name == value.name, value.produced_qty + scrap_items.get(key) table.name == value.name, value.produced_qty + secondary_items.get(key)
) )
if final_list := list( if final_list := list(
set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs) set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
): ):
frappe.qb.update(table).set(table.produced_qty, case_expr).where( frappe.qb.update(table).set(table.produced_qty, case_expr).where(
(table.name.isin(final_list)) & (table.docstatus == 1) (table.name.isin(final_list)) & (table.docstatus == 1)
).run() ).run()
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
for scrap_item in [ for secondary_item in [
item item
for item in scrap_items_list for item in secondary_items_list
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result] if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
]: ]:
doc = frappe.new_doc( doc = frappe.new_doc(
"Subcontracting Inward Order Scrap Item", "Subcontracting Inward Order Secondary Item",
parent=scio, parent=scio,
parenttype="Subcontracting Inward Order", parenttype="Subcontracting Inward Order",
parentfield="scrap_items", parentfield="secondary_items",
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1, idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
item_code=scrap_item.item_code, + 1,
item_code=secondary_item.item_code,
fg_item_code=fg_item_code, fg_item_code=fg_item_code,
stock_uom=scrap_item.stock_uom, stock_uom=secondary_item.stock_uom,
warehouse=scrap_item.t_warehouse, warehouse=secondary_item.t_warehouse,
produced_qty=scrap_item.transfer_qty, produced_qty=secondary_item.transfer_qty,
type=secondary_item.type,
delivered_qty=0, delivered_qty=0,
reference_name=frappe.get_value( reference_name=frappe.get_value(
"Work Order", self.work_order, "subcontracting_inward_order_item" "Work Order", self.work_order, "subcontracting_inward_order_item"
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
and ( and (
not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail) not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail) and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
and not frappe.db.exists("Subcontracting Inward Order Scrap Item", item.scio_detail) and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
) )
] ]
for item in items: for item in items:

View File

@@ -164,8 +164,14 @@ class calculate_taxes_and_totals:
return return
if not self.discount_amount_applied: if not self.discount_amount_applied:
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
do_not_round_fields = ["valuation_rate", "incoming_rate"]
for item in self.doc.items: for item in self.doc.items:
self.doc.round_floats_in(item) self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
if item.discount_percentage == 100: if item.discount_percentage == 100:
item.rate = 0.0 item.rate = 0.0
@@ -225,7 +231,13 @@ class calculate_taxes_and_totals:
elif not item.qty and self.doc.get("is_debit_note"): elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount")) item.amount = flt(item.rate, item.precision("amount"))
else: else:
item.amount = flt(item.rate * item.qty, item.precision("amount")) qty = (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice
and self.doc.doctype == "Purchase Receipt"
else item.qty
)
item.amount = flt(item.rate * qty, item.precision("amount"))
item.net_amount = item.amount item.net_amount = item.amount
@@ -285,6 +297,13 @@ class calculate_taxes_and_totals:
self.doc._item_wise_tax_details = item_wise_tax_details self.doc._item_wise_tax_details = item_wise_tax_details
self.doc.item_wise_tax_details = [] self.doc.item_wise_tax_details = []
for tax in self.doc.get("taxes"):
if not tax.get("dont_recompute_tax"):
tax._running_txn_tax_total = 0.0
tax._running_base_tax_total = 0.0
tax._running_txn_taxable_total = 0.0
tax._running_base_taxable_total = 0.0
def determine_exclusive_rate(self): def determine_exclusive_rate(self):
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return return
@@ -372,9 +391,16 @@ class calculate_taxes_and_totals:
self.doc.total self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 ) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
for item in self._items: for item in self._items:
self.doc.total += item.amount self.doc.total += item.amount
self.doc.total_qty += item.qty self.doc.total_qty += (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt"
else item.qty
)
self.doc.base_total += item.base_amount self.doc.base_total += item.base_amount
self.doc.net_total += item.net_amount self.doc.net_total += item.net_amount
self.doc.base_net_total += item.base_net_amount self.doc.base_net_total += item.base_net_amount
@@ -521,7 +547,6 @@ class calculate_taxes_and_totals:
actual_breakup = tax._total_tax_breakup actual_breakup = tax._total_tax_breakup
diff = flt(expected_amount - actual_breakup, 5) diff = flt(expected_amount - actual_breakup, 5)
# TODO: fix rounding difference issues
if abs(diff) <= 0.5: if abs(diff) <= 0.5:
detail_row = self.doc._item_wise_tax_details[last_idx] detail_row = self.doc._item_wise_tax_details[last_idx]
detail_row["amount"] = flt(detail_row["amount"] + diff, 5) detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
@@ -597,14 +622,29 @@ class calculate_taxes_and_totals:
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount): def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
# store tax breakup for each item # store tax breakup for each item
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1 multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
item_wise_tax_amount = flt(
current_tax_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") # Error diffusion: derive each item's base amount as a delta of the running cumulative total
# so the sum always equals base_tax_amount_after_discount_amount.
tax._running_txn_tax_total += current_tax_amount * multiplier
new_base_tax_total = flt(
flt(tax._running_txn_tax_total, tax.precision("tax_amount")) * self.doc.conversion_rate,
tax.precision("base_tax_amount"),
) )
item_wise_tax_amount = flt(
new_base_tax_total - tax._running_base_tax_total, tax.precision("base_tax_amount")
)
tax._running_base_tax_total = new_base_tax_total
if tax.charge_type != "On Item Quantity": if tax.charge_type != "On Item Quantity":
item_wise_taxable_amount = flt( tax._running_txn_taxable_total += current_net_amount * multiplier
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") new_base_taxable_total = flt(
flt(tax._running_txn_taxable_total, tax.precision("net_amount")) * self.doc.conversion_rate,
tax.precision("base_net_amount"),
) )
item_wise_taxable_amount = flt(
new_base_taxable_total - tax._running_base_taxable_total, tax.precision("base_net_amount")
)
tax._running_base_taxable_total = new_base_taxable_total
else: else:
item_wise_taxable_amount = 0.0 item_wise_taxable_amount = 0.0
@@ -788,7 +828,8 @@ class calculate_taxes_and_totals:
discount_amount += total_return_discount discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount # validate that discount amount cannot exceed the total before discount
if ( # only during save (i.e. when `_action` is set)
if self.doc.get("_action") and (
(grand_total >= 0 and discount_amount > grand_total) (grand_total >= 0 and discount_amount > grand_total)
or (grand_total < 0 and discount_amount < grand_total) # returns or (grand_total < 0 and discount_amount < grand_total) # returns
): ):

View File

@@ -1567,25 +1567,10 @@ class TestAccountsController(ERPNextTestSuite):
frappe.db.set_value("Company", self.company, "cost_center", cc) frappe.db.set_value("Company", self.company, "cost_center", cc)
def setup_dimensions(self):
# create dimension
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
)
create_dimension()
# make it non-mandatory
loc = frappe.get_doc("Accounting Dimension", "Location")
for x in loc.dimension_defaults:
x.mandatory_for_bs = False
x.mandatory_for_pl = False
loc.save()
def test_90_dimensions_filter(self): def test_90_dimensions_filter(self):
""" """
Test workings of dimension filters Test workings of dimension filters
""" """
self.setup_dimensions()
rate_in_account_currency = 1 rate_in_account_currency = 1
# Invoices # Invoices
@@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite):
self.assertEqual(len(pr.payments), 1) self.assertEqual(len(pr.payments), 1)
def test_91_cr_note_should_inherit_dimension(self): def test_91_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1 rate_in_account_currency = 1
# Invoice # Invoice
@@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite):
def test_92_dimension_inhertiance_exc_gain_loss(self): def test_92_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency # Sales Invoice in Foreign Currency
self.setup_dimensions()
rate_in_account_currency = 1 rate_in_account_currency = 1
dpt = "Research & Development - _TC" dpt = "Research & Development - _TC"
@@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite):
) )
def test_93_dimension_inheritance_on_advance(self): def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development - _TC" dpt = "Research & Development - _TC"
adv = self.create_payment_entry(amount=1, source_exc_rate=85) adv = self.create_payment_entry(amount=1, source_exc_rate=85)

View File

@@ -1,10 +1,9 @@
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals 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.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestTaxesAndTotals(AccountsTestMixin, ERPNextTestSuite): class TestTaxesAndTotals(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_distributed_discount_amount(self): def test_distributed_discount_amount(self):
so = make_sales_order(do_not_save=1) so = make_sales_order(do_not_save=1)

View File

@@ -1,9 +1,9 @@
import json import json
import frappe import frappe
from frappe.utils import flt
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.tests.utils import ERPNextTestSuite, change_settings
from erpnext.tests.utils import ERPNextTestSuite
class TestTaxesAndTotals(ERPNextTestSuite): class TestTaxesAndTotals(ERPNextTestSuite):
@@ -124,3 +124,180 @@ class TestTaxesAndTotals(ERPNextTestSuite):
] ]
self.assertEqual(actual_values, expected_values) self.assertEqual(actual_values, expected_values)
@change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_item_wise_tax_detail_high_conversion_rate(self):
"""
With a high conversion rate (e.g. USD -> KRW ~1300), independently rounding
each item's base tax amount causes per-item errors that accumulate and exceed
the 0.5-unit safety threshold, raising a validation error.
Error diffusion fixes this: the cumulative base total after the last item
equals base_tax_amount_after_discount_amount exactly, so the sum of all
per-item amounts is always exact regardless of item count or rate magnitude.
Analytically with conversion_rate=1300, rate=7.77 x3 items, VAT 16%:
per-item txn tax = 1.2432
OLD independent: flt(1.2432 * 1300, 2) = 1616.16 -> sum 4848.48
expected base: flt(flt(3.7296, 2) * 1300, 0) = flt(3.73 * 1300, 0) = 4849
diff = 0.52 -> exceeds 0.5 threshold -> would throw with old code
"""
doc = frappe.get_doc(
{
"doctype": "Sales Invoice",
"customer": "_Test Customer",
"company": "_Test Company",
"currency": "USD",
"debit_to": "_Test Receivable USD - _TC",
"conversion_rate": 1300,
"items": [
{
"item_code": "_Test Item",
"qty": 1,
"rate": 7.77,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
{
"item_code": "_Test Item",
"qty": 1,
"rate": 7.77,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
{
"item_code": "_Test Item",
"qty": 1,
"rate": 7.77,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"rate": 16,
},
{
"charge_type": "On Previous Row Amount",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 10,
"row_id": 1,
},
],
}
)
doc.save()
details_by_tax = {}
for detail in doc.item_wise_tax_details:
bucket = details_by_tax.setdefault(detail.tax_row, 0.0)
details_by_tax[detail.tax_row] = bucket + detail.amount
for tax in doc.taxes:
self.assertEqual(details_by_tax[tax.name], tax.base_tax_amount_after_discount_amount)
@change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_rounding_in_item_wise_tax_details(self):
"""
This test verifies the amounts are properly rounded.
"""
doc = frappe.get_doc(
{
"doctype": "Sales Invoice",
"customer": "_Test Customer",
"company": "_Test Company",
"currency": "INR",
"conversion_rate": 1,
"items": [
{
"item_code": "_Test Item",
"qty": 5,
"rate": 20,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
{
"item_code": "_Test Item",
"qty": 3,
"rate": 19,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
{
"item_code": "_Test Item",
"qty": 1,
"rate": 1000,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"rate": 9,
},
],
}
)
doc.save()
# item 1: taxable=100, tax=9.0; item 2: taxable=57, tax=5.13; item 3: taxable=1000, tax=90.0
# error diffusion: 14.13 - 9.0 = 5.130000000000001 without rounding
for detail in doc.item_wise_tax_details:
self.assertEqual(detail.amount, flt(detail.amount, detail.precision("amount")))
def test_item_wise_tax_detail_with_multi_currency_with_single_item(self):
"""
When the tax amount (in transaction currency) has more decimals than
the field precision, rounding must happen *before* multiplying by
conversion_rate — the same order used by _set_in_company_currency.
"""
doc = frappe.get_doc(
{
"doctype": "Sales Invoice",
"customer": "_Test Customer",
"company": "_Test Company",
"currency": "USD",
"debit_to": "_Test Receivable USD - _TC",
"conversion_rate": 129.99,
"items": [
{
"item_code": "_Test Item",
"qty": 1,
"rate": 47.41,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}
],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"rate": 16,
},
],
}
)
doc.save()
tax = doc.taxes[0]
detail = doc.item_wise_tax_details[0]
self.assertEqual(detail.amount, tax.base_tax_amount_after_discount_amount)

View File

@@ -68,7 +68,7 @@ class TestTaxes(ERPNextTestSuite):
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_name": uuid4(), "customer_name": uuid4(),
"customer_group": "All Customer Groups", "customer_group": "Individual",
} }
).insert() ).insert()
self.supplier = frappe.get_doc( self.supplier = frappe.get_doc(

View File

@@ -2,36 +2,17 @@ import frappe
from frappe import qb from frappe import qb
from frappe.utils import today from frappe.utils import today
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import disable_dimension
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestReactivity(AccountsTestMixin, ERPNextTestSuite): class TestReactivity(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.create_price_list()
self.clear_old_entries()
def disable_dimensions(self):
res = frappe.db.get_all("Accounting Dimension", filters={"disabled": False})
for x in res:
dim = frappe.get_doc("Accounting Dimension", x.name)
dim.disabled = True
dim.save()
def test_01_basic_item_details(self): def test_01_basic_item_details(self):
self.disable_dimensions()
# set Item Price # set Item Price
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Item Price", "doctype": "Item Price",
"item_code": self.item, "item_code": "_Test Item",
"price_list": self.price_list, "price_list": "Standard Selling",
"price_list_rate": 90, "price_list_rate": 90,
"selling": True, "selling": True,
"rate": 90, "rate": 90,
@@ -42,17 +23,18 @@ class TestReactivity(AccountsTestMixin, ERPNextTestSuite):
si = frappe.get_doc( si = frappe.get_doc(
{ {
"doctype": "Sales Invoice", "doctype": "Sales Invoice",
"company": self.company, "company": "_Test Company",
"customer": self.customer, "customer": "_Test Customer",
"debit_to": self.debit_to, "debit_to": "Debtors - _TC",
"posting_date": today(), "posting_date": today(),
"cost_center": self.cost_center, "cost_center": "Main - _TC",
"currency": "INR",
"conversion_rate": 1, "conversion_rate": 1,
"selling_price_list": self.price_list, "selling_price_list": "Standard Selling",
} }
) )
itm = si.append("items") itm = si.append("items")
itm.item_code = self.item itm.item_code = "_Test Item"
si.process_item_selection(itm.idx) si.process_item_selection(itm.idx)
self.assertEqual(itm.rate, 90) self.assertEqual(itm.rate, 90)

View File

@@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr1.items[0].qty = 2 scr1.items[0].qty = 2
add_second_row_in_scr(scr1) add_second_row_in_scr(scr1)
scr1.flags.ignore_mandatory = True scr1.flags.ignore_mandatory = True
scr1.save()
scr1.set_missing_values() scr1.set_missing_values()
scr1.save()
scr1.submit() scr1.submit()
for _key, value in get_supplied_items(scr1).items(): for _key, value in get_supplied_items(scr1).items():
@@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr2.items[0].qty = 2 scr2.items[0].qty = 2
add_second_row_in_scr(scr2) add_second_row_in_scr(scr2)
scr2.flags.ignore_mandatory = True scr2.flags.ignore_mandatory = True
scr2.save()
scr2.set_missing_values() scr2.set_missing_values()
scr2.save()
scr2.submit() scr2.submit()
for _key, value in get_supplied_items(scr2).items(): for _key, value in get_supplied_items(scr2).items():
@@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr3 = make_subcontracting_receipt(sco.name) scr3 = make_subcontracting_receipt(sco.name)
scr3.items[0].qty = 2 scr3.items[0].qty = 2
scr3.flags.ignore_mandatory = True scr3.flags.ignore_mandatory = True
scr3.save()
scr3.set_missing_values() scr3.set_missing_values()
scr3.save()
scr3.submit() scr3.submit()
for _key, value in get_supplied_items(scr3).items(): for _key, value in get_supplied_items(scr3).items():
@@ -1164,6 +1164,54 @@ class TestSubcontractingController(ERPNextTestSuite):
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected) self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
def test_co_by_product(self):
frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)
fg_item = make_item("FG Item", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
make_bom(
item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
).name
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 11",
"qty": 5,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 5,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr1 = make_subcontracting_receipt(sco.name)
scr1.get_secondary_items()
scr1.save()
self.assertEqual(scr1.items[0].received_qty, 5)
self.assertEqual(scr1.items[0].process_loss_qty, 0.5)
self.assertEqual(scr1.items[0].qty, 4.5)
self.assertEqual(scr1.items[0].rate, 200)
self.assertEqual(scr1.items[0].amount, 900)
self.assertEqual(scr1.items[1].item_code, scrap_item)
self.assertEqual(scr1.items[1].received_qty, 5)
self.assertEqual(scr1.items[1].process_loss_qty, 0.5)
self.assertEqual(scr1.items[1].qty, 4.5)
self.assertEqual(flt(scr1.items[1].rate, 3), 11.111)
self.assertEqual(scr1.items[1].amount, 50)
frappe.set_value("UOM", "Nos", "must_be_whole_number", 1)
def add_second_row_in_scr(scr): def add_second_row_in_scr(scr):
item_dict = {} item_dict = {}

View File

@@ -120,6 +120,7 @@ class Appointment(Document):
self.auto_assign() self.auto_assign()
self.create_calendar_event() self.create_calendar_event()
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
if not frappe.in_test:
frappe.db.commit() frappe.db.commit()
def create_lead_and_link(self): def create_lead_and_link(self):

View File

@@ -56,7 +56,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2024-03-27 13:06:46.495091", "modified": "2026-03-25 19:27:19.162421",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Contract Template", "name": "Contract Template",
@@ -75,42 +75,34 @@
"write": 1 "write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1
"write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Purchase Manager", "role": "Purchase Manager",
"share": 1, "share": 1
"write": 1
}, },
{ {
"create": 1,
"delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"share": 1, "share": 1
"write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
# called from hooks on doc_event Email Unsubscribe # called from hooks on doc_event Email Unsubscribe
def unsubscribe_recipient(unsubscribe, method): def unsubscribe_recipient(unsubscribe, method):
if unsubscribe.reference_doctype == "Email Campaign": if unsubscribe.reference_doctype != "Email Campaign":
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") return
email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name)
if email_campaign.email_campaign_for == "Email Group":
if unsubscribe.email:
frappe.db.set_value(
"Email Group Member",
{"email_group": email_campaign.recipient, "email": unsubscribe.email},
"unsubscribed",
1,
)
else:
# For Lead or Contact
frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed")
# called through hooks to update email campaign status daily # called through hooks to update email campaign status daily

View File

@@ -4,6 +4,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Date
Opportunity = DocType("Opportunity")
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
def execute(filters=None): def execute(filters=None):
@@ -66,58 +72,48 @@ def get_columns():
def get_data(filters): def get_data(filters):
return frappe.db.sql( query = (
f""" frappe.qb.from_(Opportunity)
SELECT .left_join(OpportunityLostReasonDetail)
`tabOpportunity`.name, .on(
`tabOpportunity`.opportunity_from, (OpportunityLostReasonDetail.parenttype == "Opportunity")
`tabOpportunity`.party_name, & (OpportunityLostReasonDetail.parent == Opportunity.name)
`tabOpportunity`.customer_name, )
`tabOpportunity`.opportunity_type, .select(
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason, Opportunity.name,
`tabOpportunity`.sales_stage, Opportunity.opportunity_from,
`tabOpportunity`.territory Opportunity.party_name,
FROM Opportunity.customer_name,
`tabOpportunity` Opportunity.opportunity_type,
{get_join(filters)} GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
WHERE Opportunity.sales_stage,
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s Opportunity.territory,
AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s )
{get_conditions(filters)} .where(
GROUP BY (Opportunity.status == "Lost")
`tabOpportunity`.name & (Opportunity.company == filters.get("company"))
ORDER BY & (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date")))
`tabOpportunity`.creation asc """, )
filters, .groupby(Opportunity.name)
as_dict=1, .orderby(Opportunity.creation)
) )
query = get_conditions(filters, query)
def get_conditions(filters): return query.run(as_dict=1)
conditions = []
def get_conditions(filters, query):
if filters.get("territory"): if filters.get("territory"):
conditions.append(" and `tabOpportunity`.territory=%(territory)s") query = query.where(Opportunity.territory == filters.get("territory"))
if filters.get("opportunity_from"): if filters.get("opportunity_from"):
conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s") query = query.where(Opportunity.opportunity_from == filters.get("opportunity_from"))
if filters.get("party_name"): if filters.get("party_name"):
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s") query = query.where(Opportunity.party_name == filters.get("party_name"))
return " ".join(conditions) if conditions else ""
def get_join(filters):
join = """LEFT JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name"""
if filters.get("lost_reason"): if filters.get("lost_reason"):
join = """JOIN `tabOpportunity Lost Reason Detail` query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason"))
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
""".format(filters.get("lost_reason"))
return join return query

View File

@@ -2,8 +2,8 @@
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests.utils import FrappeTestCase from erpnext.tests.utils import ERPNextTestSuite
class TestCodeList(FrappeTestCase): class TestCodeList(ERPNextTestSuite):
pass pass

View File

@@ -2,8 +2,8 @@
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests.utils import FrappeTestCase from erpnext.tests.utils import ERPNextTestSuite
class TestCommonCode(FrappeTestCase): class TestCommonCode(ERPNextTestSuite):
pass pass

View File

@@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard # setup wizard
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages"
setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo"
after_install = "erpnext.setup.install.after_install" after_install = "erpnext.setup.install.after_install"

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