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
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,60 +52,55 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children",
on_get_node: function (nodes, deep = false) {
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
on_node_render: function (node, deep) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
let accounts = [];
if (deep) {
// in case of `get_all_nodes`
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
} else {
accounts = nodes;
}
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency);
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];
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance !== undefined) {
node.parent && node.parent.find(".balance-area").remove();
$(
'<span class="balance-area pull-right">' +
(account.balance_in_account_currency
? format(
account.balance_in_account_currency,
account.account_currency
) + " / "
: "") +
format(account.balance, account.company_currency) +
" " +
dr_or_cr +
"</span>"
).insertBefore(node.$ul);
}
}
});
if (account.balance !== undefined) {
node.parent && node.parent.find(".balance-area").remove();
$(
'<span class="balance-area pull-right">' +
(account.account_currency != account.company_currency
? format(account.balance_in_account_currency, account.account_currency) +
" / "
: "") +
format(account.balance, account.company_currency) +
" " +
dr_or_cr +
"</span>"
).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",
menu_items: [

View File

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

View File

@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountingDimension(ERPNextTestSuite):
def setUp(self):
create_dimension()
def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1)
@@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite):
si.save()
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
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.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.tests.utils import ERPNextTestSuite
@@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountingDimensionFilter(ERPNextTestSuite):
def setUp(self):
create_dimension()
create_accounting_dimension_filter()
self.invoice_list = []

View File

@@ -65,6 +65,7 @@
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
"fetch_payment_schedule_in_payment_request",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -688,6 +689,19 @@
"fieldname": "enable_accounting_dimensions",
"fieldtype": "Check",
"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,
@@ -697,7 +711,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-27 01:04:09.415288",
"modified": "2026-03-30 07:32:58.182018",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -73,6 +73,7 @@ class AccountsSettings(Document):
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check
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
general_ledger_remarks_length: DF.Int
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
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite):
class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
"""
Integration tests for AdvancePaymentLedgerEntry.
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()
def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value(
"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
class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite):
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()

View File

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

View File

@@ -2,10 +2,11 @@
# See license.txt
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):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""

View File

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

View File

@@ -3,6 +3,8 @@
frappe.ui.form.on("Financial Report Template", {
refresh(frm) {
if (frm.is_new() || frm.doc.rows.length === 0) return;
// add custom button to view missed accounts
frm.add_custom_button(__("View Account Coverage"), function () {
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) {
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_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);
},
@@ -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 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 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 = "";
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
<li><code>my_app.financial_reports.get_kpi_data</code></li>
</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>
<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>`;
} else if (data_source === "Blank Line") {
description_html = `

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType",
@@ -31,7 +30,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"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",
@@ -66,7 +66,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-14 00:11:03.508139",
"modified": "2026-02-23 01:04:05.797161",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Report Template",

View File

@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
template_name: DF.Data
# 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):
validator = TemplateValidator(self)
result = validator.validate()

View File

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

View File

@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
self.data_source = "Account Data"
self.idx = 1
self.reverse_sign = 0
self.advanced_filtering = True
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):
frappe.call(hook, newname=newname, oldname=oldname)
frappe.db.commit()
if not frappe.in_test:
frappe.db.commit()

View File

@@ -353,8 +353,11 @@ class JournalEntry(AccountsController):
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
def validate_stock_accounts(self):
if self.voucher_type == "Periodic Accounting Entry":
# Skip validation for periodic accounting entry
if (
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
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
class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite):
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()

View File

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

View File

@@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
});
});
if (frm.doc.create_missing_party) {
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
}
frm.trigger("update_party_labels");
},
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_name", "");
});
frm.clear_table("invoices");
frm.refresh_fields();
frm.trigger("update_party_labels");
},
make_dashboard: function (frm) {
@@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
}
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", {

View File

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

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _, scrub
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 erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document):
)
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
def validate_company(self):
@@ -274,7 +279,8 @@ def start_import(invoices):
doc.flags.ignore_mandatory = True
doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
if not frappe.in_test:
frappe.db.commit()
names.append(doc.name)
except Exception:
errors += 1

View File

@@ -3,10 +3,6 @@
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 (
get_temporary_opening_account,
)
@@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
def make_invoices(
self,
invoice_type="Sales",
@@ -183,26 +174,13 @@ def get_opening_invoice_creation_dict(**args):
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):
customer_name = customer or "Opening Customer"
customer = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_group": "Individual",
"customer_type": "Company",
"territory": "All Territories",
}

View File

@@ -824,7 +824,7 @@ frappe.ui.form.on("Payment Entry", {
paid_amount: function (frm) {
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;
if (!frm.doc.received_amount) {
if (frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount);
} 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)
);
if (!frm.doc.paid_amount) {
if (frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount);
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,
)
outstanding_invoices = split_invoices_based_on_payment_terms(
outstanding_invoices, args.get("company")
)
outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company"))
for d in outstanding_invoices:
d["exchange_rate"] = 1
@@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False):
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
if not data:
@@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False):
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."""
exc_rates = get_currency_data(outstanding_invoices, company)
exc_rates = get_currency_data(refdocs, company)
outstanding_invoices_after_split = []
for entry in outstanding_invoices:
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
outstanding_refdoc_after_split = []
for entry in refdocs:
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
if payment_term_template := frappe.db.get_value(
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,
)
outstanding_invoices_after_split += split_rows
outstanding_refdoc_after_split += split_rows
continue
# 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."""
exc_rates = frappe._dict()
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype]
for x in frappe.db.get_all(
doctype,
filters={"name": ["in", invoices]},
filters={"name": ["in", refdoc]},
fields=["name", "currency", "conversion_rate", "party_account_currency"],
):
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, "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):
payment_entry = frappe.new_doc("Payment Entry")

View File

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

View File

@@ -4,10 +4,6 @@ import unittest
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 (
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
"""
create_dimension()
location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True
location.save()
@@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
)
accounting_dimension_department.mandatory_for_bs = 0
accounting_dimension_department.save()
disable_dimension()
def test_merging_into_sales_invoice_for_batched_item(self):
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)
doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()))
if doc.report == "General Ledger":
doc.db_set("to_date", new_to_date, commit=True)
doc.db_set("from_date", new_from_date, commit=True)
frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date)
frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date)
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
else:
return False

View File

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

View File

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

View File

@@ -8,7 +8,6 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"title",
"naming_series",
"supplier",
"supplier_name",
@@ -209,16 +208,6 @@
"connections_tab"
],
"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",
"fieldtype": "Select",
@@ -1693,7 +1682,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-03-17 20:44:00.221219",
"modified": "2026-03-25 11:45:38.696888",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1756,6 +1745,6 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title",
"title_field": "supplier_name",
"track_changes": 1
}

View File

@@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency
@@ -333,9 +332,6 @@ class PurchaseInvoice(BuyingController):
if 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):
if not self.credit_to:
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)
def po_required(self):
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
if frappe.get_value(
if (
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"
):
return
)
):
for d in self.get("items"):
if not d.purchase_order:
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:
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"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
@@ -1161,7 +1162,11 @@ class PurchaseInvoice(BuyingController):
)
# 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 (
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,
)
)
if (
self.auto_accounting_for_stock
and self.is_opening == "No"

View File

@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
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(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
# fetching the latest GL Entry with exchange gain and loss account account
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(
pi.items[0].base_net_amount - pr.items[0].base_net_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):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,
@@ -2189,11 +2200,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
def test_offsetting_entries_for_accounting_dimensions(self):
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(
account_name="Offsetting",
@@ -2201,7 +2207,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
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.branch = "Location 1"
@@ -2238,8 +2253,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
voucher_type="Purchase Invoice",
additional_columns=["branch"],
)
clear_dimension_defaults("Branch")
disable_dimension()
def test_repost_accounting_entries(self):
# update repost settings

View File

@@ -190,6 +190,7 @@
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
@@ -206,7 +207,8 @@
{
"fieldname": "rejected_qty",
"fieldtype": "Float",
"label": "Rejected Qty"
"label": "Rejected Qty",
"print_hide": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
@@ -226,6 +228,7 @@
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"print_hide": 1,
"reqd": 1
},
{
@@ -261,14 +264,16 @@
"depends_on": "price_list_rate",
"fieldname": "discount_percentage",
"fieldtype": "Percent",
"label": "Discount on Price List Rate (%)"
"label": "Discount on Price List Rate (%)",
"print_hide": 1
},
{
"depends_on": "price_list_rate",
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Discount Amount",
"options": "currency"
"options": "currency",
"print_hide": 1
},
{
"fieldname": "col_break3",
@@ -401,12 +406,14 @@
{
"fieldname": "weight_per_unit",
"fieldtype": "Float",
"label": "Weight Per Unit"
"label": "Weight Per Unit",
"print_hide": 1
},
{
"fieldname": "total_weight",
"fieldtype": "Float",
"label": "Total Weight",
"print_hide": 1,
"read_only": 1
},
{
@@ -417,7 +424,8 @@
"fieldname": "weight_uom",
"fieldtype": "Link",
"label": "Weight UOM",
"options": "UOM"
"options": "UOM",
"print_hide": 1
},
{
"depends_on": "eval:parent.update_stock",
@@ -429,7 +437,8 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Accepted Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"print_hide": 1
},
{
"fieldname": "rejected_warehouse",
@@ -674,7 +683,8 @@
"fieldname": "asset_location",
"fieldtype": "Link",
"label": "Asset Location",
"options": "Location"
"options": "Location",
"print_hide": 1
},
{
"fieldname": "po_detail",
@@ -730,7 +740,6 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -796,6 +805,7 @@
"fieldtype": "Link",
"label": "Asset Category",
"options": "Asset Category",
"print_hide": 1,
"read_only": 1
},
{
@@ -828,6 +838,7 @@
"label": "Rate of Stock UOM",
"no_copy": 1,
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
@@ -866,6 +877,7 @@
"fieldtype": "Currency",
"label": "Rate With Margin",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
@@ -892,7 +904,8 @@
"default": "1",
"fieldname": "apply_tds",
"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",
@@ -918,7 +931,8 @@
"fieldname": "wip_composite_asset",
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
"options": "Asset",
"print_hide": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
@@ -930,7 +944,8 @@
"default": "0",
"fieldname": "use_serial_batch_fields",
"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",
@@ -977,7 +992,8 @@
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
"options": "currency",
"print_hide": 1
},
{
"fieldname": "tax_withholding_category",
@@ -991,7 +1007,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-15 21:07:49.455930",
"modified": "2026-04-07 15:40:45.687554",
"modified_by": "Administrator",
"module": "Accounts",
"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_request.payment_request import make_payment_request
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.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.tests.utils import ERPNextTestSuite
class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
class TestRepostAccountingLedger(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
update_repost_settings()
def test_01_basic_functions(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
item="_Test Item",
company="_Test Company",
customer="_Test Customer",
debit_to="Debtors - _TC",
parent_cost_center="Main - _TC",
cost_center="Main - _TC",
rate=100,
)
@@ -48,7 +44,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# Test Validation Error
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.company = "_Test Company"
ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append(
@@ -65,7 +61,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
ral.save()
# 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)
gl = qb.DocType("GL Entry")
@@ -94,23 +90,23 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_02_deferred_accounting_valiations(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
item="_Test Item",
company="_Test Company",
customer="_Test Customer",
debit_to="Debtors - _TC",
parent_cost_center="Main - _TC",
cost_center="Main - _TC",
rate=100,
do_not_submit=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_end_date = add_days(nowdate(), 90)
si.save().submit()
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})
self.assertRaises(frappe.ValidationError, ral.save)
@@ -118,35 +114,35 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_04_pcv_validation(self):
# Clear old GL entries so PCV can be submitted.
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(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
item="_Test Item",
company="_Test Company",
customer="_Test Customer",
debit_to="Debtors - _TC",
parent_cost_center="Main - _TC",
cost_center="Main - _TC",
rate=100,
)
fy = get_fiscal_year(today(), company=self.company)
fy = get_fiscal_year(today(), company="_Test Company")
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": today(),
"period_start_date": fy[1],
"period_end_date": today(),
"company": self.company,
"company": "_Test Company",
"fiscal_year": fy[0],
"cost_center": self.cost_center,
"closing_account_head": self.retained_earnings,
"cost_center": "Main - _TC",
"closing_account_head": "Retained Earnings - _TC",
"remarks": "test",
}
)
pcv.save().submit()
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})
self.assertRaises(frappe.ValidationError, ral.save)
@@ -156,12 +152,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
def test_03_deletion_flag_and_preview_function(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
item="_Test Item",
company="_Test Company",
customer="_Test Customer",
debit_to="Debtors - _TC",
parent_cost_center="Main - _TC",
cost_center="Main - _TC",
rate=100,
)
@@ -170,7 +166,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.company = "_Test Company"
ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.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):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
item="_Test Item",
company="_Test Company",
customer="_Test Customer",
debit_to="Debtors - _TC",
parent_cost_center="Main - _TC",
cost_center="Main - _TC",
rate=100,
)
@@ -195,7 +191,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.company = "_Test Company"
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
@@ -210,16 +206,16 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
company="_Test Company",
)
another_provisional_account = create_account(
account_name="Another Provision Account",
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.default_provisional_account = provisional_account
company.save()
@@ -229,7 +225,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
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)
expected_pr_gles = [
{"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.company = self.company
repost_doc.company = "_Test Company"
repost_doc.delete_cancelled_entries = True
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
repost_doc.save().submit()

View File

@@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
);
}
}
// 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.toggle_get_items();
this.set_default_print_format();
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() {
var me = this;
@@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note",
@@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = {
docstatus: 1,
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;
return {
@@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
is_return() {
this.toggle_get_items();
}
};
// for backward compatibility: combine new and previous states
@@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", {
},
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) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}

View File

@@ -1101,9 +1101,6 @@ class SalesInvoice(SellingController):
if self.po_date:
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended
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})
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.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@@ -2894,7 +2887,7 @@ class TestSalesInvoice(ERPNextTestSuite):
si.submit()
# Check if adjustment entry is created
self.assertTrue(
self.assertFalse(
frappe.db.exists(
"GL Entry",
{

View File

@@ -207,6 +207,7 @@
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"print_hide": 1,
"read_only": 1
},
{
@@ -310,7 +311,8 @@
"fieldname": "discount_amount",
"fieldtype": "Currency",
"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",
@@ -853,6 +855,7 @@
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"no_copy": 1,
"print_hide": 1,
"options": "currency",
"read_only": 1
},
@@ -869,6 +872,7 @@
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"print_hide": 1,
"read_only": 1
},
{
@@ -926,7 +930,8 @@
"default": "0",
"fieldname": "use_serial_batch_fields",
"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",
@@ -941,7 +946,8 @@
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
"options": "currency",
"print_hide": 1
},
{
"fieldname": "available_quantity_section",
@@ -1010,7 +1016,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-23 14:37:14.853941",
"modified": "2026-02-24 14:37:16.853941",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

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

View File

@@ -128,6 +128,7 @@ class TaxWithholdingDetails:
self.party_type = party_type
self.party = party
self.company = company
self.tax_id = get_tax_id_for_party(self.party_type, self.party)
def get(self) -> list:
"""
@@ -161,6 +162,7 @@ class TaxWithholdingDetails:
disable_cumulative_threshold=doc.disable_cumulative_threshold,
disable_transaction_threshold=doc.disable_transaction_threshold,
taxable_amount=0,
tax_id=self.tax_id,
)
# ldc (only if valid based on posting date)
@@ -181,17 +183,13 @@ class TaxWithholdingDetails:
if self.party_type != "Supplier":
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_records = self.get_valid_ldc_records(tax_id)
ldc_records = self.get_valid_ldc_records(self.tax_id)
if not ldc_records:
return ldc_details
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
for ldc in ldc_records:
@@ -254,4 +252,5 @@ class TaxWithholdingDetails:
@allow_regional
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
import datetime
from unittest.mock import patch
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -18,7 +19,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
# create relevant supplier, etc
create_records()
create_tax_withholding_category_records()
make_pan_no_field()
def validate_tax_withholding_entries(self, doctype, docname, expected_entries):
"""Validate tax withholding entries for a document"""
@@ -3542,6 +3542,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
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):
# return sales invoice doc object
@@ -3998,18 +4039,3 @@ def create_lower_deduction_certificate(
"certificate_limit": limit,
}
).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 (
TaxWithholdingDetails,
get_tax_id_for_party,
)
@@ -646,8 +645,11 @@ class TaxWithholdingController:
# 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)
query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party)
query = (
query.where(entry.tax_id == category.tax_id)
if category.tax_id
else query.where(entry.party == self.party)
)
return query
@@ -686,6 +688,7 @@ class TaxWithholdingController:
"company": self.doc.company,
"party_type": self.party_type,
"party": self.party,
"tax_id": category.tax_id,
"tax_withholding_category": category.name,
"tax_withholding_group": category.tax_withholding_group,
"tax_rate": category.tax_rate,
@@ -1052,6 +1055,7 @@ class TaxWithholdingController:
"party_type": self.party_type,
"party": self.party,
"company": self.doc.company,
"tax_id": category.tax_id,
}
)
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
class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite):
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite):
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
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
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
@@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
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):
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
self.create_payment_entry(si.name)
report = execute(filters)
@@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
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):
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
cr_note = self.create_credit_note(si.name, do_not_submit=True)
@@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters)
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
self.create_customer(
@@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters)
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
filters.pop("in_party_currency")
report = execute(filters)
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):
filters = {
@@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
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):
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
self.create_payment_entry(si.name)
@@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
report = execute(filters)
expected_data = [100, 100, "No Remarks"]
expected_data = [100, 100]
self.assertEqual(len(report[1]), 1)
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
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
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.maxDiff = None
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
class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -48,6 +48,9 @@ class Deferred_Item:
Generate report data for output
"""
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:
ret_data[period.key] = period.total
ret_data.indent = 1
@@ -205,6 +208,9 @@ class Deferred_Invoice:
for item in self.uniq_items:
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):
"""
calculate deferred revenue/expense for all items in invoice
@@ -232,7 +238,7 @@ class Deferred_Invoice:
generate report data for invoice, includes invoice total
"""
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:
inv_total[x.key] = x.total
inv_total.indent = 0
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
def get_columns(self):
columns = []
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:
columns.append(
{
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
elif self.filters.type == "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):
total_row[period.key] = self.period_total[idx].total
ret.append(total_row)

View File

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

View File

@@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = {
label: __("Party"),
fieldtype: "MultiSelectList",
options: "party_type",
depends_on: "party_type",
get_data: function (txt) {
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
class TestItemWisePurchaseRegister(AccountsTestMixin, ERPNextTestSuite):
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_supplier()

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.tests.utils import ERPNextTestSuite
class TestTaxWithholdingDetails(AccountsTestMixin, ERPNextTestSuite):
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
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.utils import get_fiscal_year
self.company = create_company()
create_cost_center(
cost_center_name="Test Cost Center",
company="Trial Balance Company",
@@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite):
parent_account="Temporary Accounts - TBC",
)
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):
"""
@@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite):
branch2.insert(ignore_if_duplicate=True)
si = create_sales_invoice(
company=self.company,
company="Trial Balance Company",
debit_to="Debtors - TBC",
cost_center="Test Cost Center - TBC",
income_account="Sales - TBC",
@@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite):
si.submit()
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]
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:
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
@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):
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
@@ -1531,6 +1603,10 @@ def parse_naming_series_variable(doc, variable):
else:
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 = (
(
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,
)
frappe.db.commit()
if not frappe.in_test:
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
failed_assets.append(asset_name)
@@ -71,7 +73,8 @@ def book_depreciation_entries(date):
if failed_assets:
set_depr_entry_posting_status_for_failed_assets(failed_assets)
notify_depr_entry_posting_error(failed_assets, error_logs)
frappe.db.commit()
if not frappe.in_test:
frappe.db.commit()
def get_depreciable_assets_data(date):

View File

@@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite):
"asset_type": "Composite Component",
"purchase_date": pr.posting_date,
"available_for_use_date": pr.posting_date,
"location": "Test Location",
}
)
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");
},

View File

@@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite):
frappe.db.set_value(
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
)
make_location()
def test_movement(self):
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:
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(
purpose="Transfer",
company=asset.company,
@@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite):
if asset.docstatus == 0:
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})
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.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 Movement",
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
@@ -197,9 +186,3 @@ def create_asset_movement(**args):
movement.submit()
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",
"in_list_view": 1,
"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",
"reqd": 1
},

View File

@@ -9,7 +9,6 @@
"engine": "InnoDB",
"field_order": [
"supplier_section",
"title",
"naming_series",
"supplier",
"supplier_name",
@@ -172,17 +171,6 @@
"fieldtype": "Section Break",
"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",
"fieldtype": "Select",
@@ -1328,7 +1316,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-09 17:15:29.184682",
"modified": "2026-03-25 11:46:18.748951",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data
to_date: DF.Date | None
total: DF.Currency
total_net_weight: DF.Float
@@ -780,7 +779,8 @@ 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"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
doc.save()
frappe.db.commit()
if not frappe.in_test:
frappe.db.commit()
frappe.response["type"] = "redirect"
frappe.response.location = "/purchase-invoices/" + doc.name
@@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.set_payment_schedule()
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
def update_item(obj, target, source_parent):
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
billed_qty = flt(get_billed_qty(obj.name))
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",
},
"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),
},
"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
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):
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(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():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

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

View File

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

View File

@@ -9,8 +9,6 @@
"field_order": [
"naming_series",
"company",
"billing_address",
"billing_address_display",
"vendor",
"column_break1",
"transaction_date",
@@ -43,7 +41,13 @@
"select_print_heading",
"letter_head",
"more_info",
"opportunity"
"opportunity",
"address_and_contact_tab",
"billing_address",
"billing_address_display",
"column_break_czul",
"shipping_address",
"shipping_address_display"
],
"fields": [
{
@@ -346,6 +350,27 @@
"fieldtype": "Check",
"hidden": 1,
"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,
@@ -353,7 +378,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-03-09 17:15:29.774614",
"modified": "2026-03-19 15:27:56.730649",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

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

View File

@@ -167,6 +167,15 @@ def create_supplier(**args):
if not args.without_supplier_group:
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()
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")
if doctype in ["Purchase Order", "Purchase Invoice"]:
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:
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
)
net_rate = item.base_net_amount
net_rate = item.qty * item.base_net_rate
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
@@ -503,11 +503,15 @@ class BuyingController(SubcontractingController):
if d.category not in ["Valuation", "Valuation and Total"]:
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":
total_valuation_amount += flt(d.base_tax_amount_after_discount_amount)
total_valuation_amount += amount
tax_accounts.append(d.account_head)
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

View File

@@ -364,38 +364,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
doctype = "Delivery Note"
def get_delivery_notes_to_be_billed(
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"])
return frappe.db.sql(
"""
select {fields}
from `tabDelivery Note`
where `tabDelivery Note`.`{key}` like {txt} and
`tabDelivery Note`.docstatus = 1
and status not in ('Stopped', 'Closed') {fcond}
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
original_dn = (
frappe.qb.from_(DeliveryNote)
.select(DeliveryNote.name)
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (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()
@@ -1002,3 +1007,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
limit_page_length=page_len,
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:
continue
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
if flt(d.rate, d.precision("incoming_rate")) != flt(
rate, d.precision("incoming_rate")
):
d.rate = rate
frappe.msgprint(
_(

View File

@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
elif self.doctype == "Stock Entry" and row.t_warehouse:
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
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))
)
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:
frappe.throw(
_("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)
)
if self.doctype != "Subcontracting Inward Order":
if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
item.amount = item.qty * item.rate
if item.bom:
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
and self._doc_before_save
):
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
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
self.__reference_name.append(row.name)
if (row.name not in item_dict) or (
row.item_code,
row.qty + (row.get("rejected_qty") or 0),
row.received_qty,
) != item_dict[row.name]:
self.__changed_name.append(row.name)
@@ -962,7 +962,7 @@ class SubcontractingController(StockController):
):
qty = (
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
)
bom_item.main_item_code = row.item_code
@@ -1285,22 +1285,28 @@ class SubcontractingController(StockController):
if self.total_additional_costs:
if self.distribute_additional_costs_based_on == "Amount":
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:
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.amount * self.total_additional_costs) / total_amt
) / item.qty
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
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
else:
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
@frappe.whitelist()

View File

@@ -1,3 +1,5 @@
from collections import defaultdict
import frappe
from frappe import _, bold
from frappe.query_builder import Case
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
def on_submit_subcontracting_inward(self):
self.update_inward_order_item()
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.update_inward_order_status()
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
self.validate_delivery()
self.validate_receive_from_customer_cancel()
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.update_inward_order_status()
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
item
for item in self.get("items")
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")
]
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
if self.subcontracting_inward_order:
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
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
def validate_warehouse_(self):
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
self.validate_delivery_on_save()
else:
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(
"Subcontracting Inward Order Item",
item.scio_detail,
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
if max_allowed_qty:
max_allowed_qty = max_allowed_qty[0]
else:
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
query = (
frappe.qb.from_(table)
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
bold(
frappe.get_cached_value(
"Subcontracting Inward Order Item"
if not item.is_scrap_item
else "Subcontracting Inward Order Scrap Item",
if not item.type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Secondary Item",
item.scio_detail,
"stock_uom",
)
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
)
for item in [item for item in self.items if not item.is_finished_item]:
if item.is_scrap_item:
scio_scrap_item = frappe.get_value(
"Subcontracting Inward Order Scrap Item",
if item.type or item.is_legacy_scrap_item:
scio_secondary_item = frappe.get_value(
"Subcontracting Inward Order Secondary Item",
{
"docstatus": 1,
"item_code": item.item_code,
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
as_dict=True,
)
if (
scio_scrap_item
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty
scio_secondary_item
and scio_secondary_item.delivered_qty
> scio_secondary_item.produced_qty - item.transfer_qty
):
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))
)
else:
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
for item in self.items:
doctype = (
"Subcontracting Inward Order Item"
if not item.is_scrap_item
else "Subcontracting Inward Order Scrap Item"
if not item.type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Secondary Item"
)
frappe.db.set_value(
doctype,
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
customer_warehouse = frappe.get_cached_value(
"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(
{
(
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
doc.insert()
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":
scrap_items_list = [item for item in self.items if item.is_scrap_item]
scrap_items = frappe._dict(
{
(item.item_code, item.t_warehouse): item.transfer_qty
if self._action == "submit"
else -item.transfer_qty
for item in scrap_items_list
}
)
if scrap_items:
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
secondary_items = defaultdict(float)
for item in secondary_items_list:
secondary_items[(item.item_code, item.t_warehouse)] += (
item.transfer_qty if self._action == "submit" else -item.transfer_qty
)
secondary_items = frappe._dict(secondary_items)
if secondary_items:
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
item_codes = list(item_codes)
warehouses = list(warehouses)
result = frappe.get_all(
"Subcontracting Inward Order Scrap Item",
"Subcontracting Inward Order Secondary Item",
filters={
"item_code": ["in", item_codes],
"warehouse": ["in", warehouses],
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
)
if result:
scrap_item_dict = frappe._dict(
secondary_items_dict = frappe._dict(
{
(d.item_code, d.warehouse): frappe._dict(
{"name": d.name, "produced_qty": d.produced_qty}
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
)
deleted_docs = []
case_expr = Case()
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
for key, value in scrap_item_dict.items():
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0:
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
for key, value in secondary_items_dict.items():
if (
self._action == "cancel"
and value.produced_qty - abs(secondary_items.get(key)) == 0
):
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:
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(
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(
(table.name.isin(final_list)) & (table.docstatus == 1)
).run()
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
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]
]:
doc = frappe.new_doc(
"Subcontracting Inward Order Scrap Item",
"Subcontracting Inward Order Secondary Item",
parent=scio,
parenttype="Subcontracting Inward Order",
parentfield="scrap_items",
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1,
item_code=scrap_item.item_code,
parentfield="secondary_items",
idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
+ 1,
item_code=secondary_item.item_code,
fg_item_code=fg_item_code,
stock_uom=scrap_item.stock_uom,
warehouse=scrap_item.t_warehouse,
produced_qty=scrap_item.transfer_qty,
stock_uom=secondary_item.stock_uom,
warehouse=secondary_item.t_warehouse,
produced_qty=secondary_item.transfer_qty,
type=secondary_item.type,
delivered_qty=0,
reference_name=frappe.get_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
and (
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 Scrap Item", item.scio_detail)
and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
)
]
for item in items:

View File

@@ -164,8 +164,14 @@ class calculate_taxes_and_totals:
return
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:
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:
item.rate = 0.0
@@ -225,7 +231,13 @@ class calculate_taxes_and_totals:
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
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
@@ -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 = []
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):
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
@@ -372,9 +391,16 @@ class calculate_taxes_and_totals:
self.doc.total
) = 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:
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.net_total += item.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
diff = flt(expected_amount - actual_breakup, 5)
# TODO: fix rounding difference issues
if abs(diff) <= 0.5:
detail_row = self.doc._item_wise_tax_details[last_idx]
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):
# store tax breakup for each item
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":
item_wise_taxable_amount = flt(
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
tax._running_txn_taxable_total += current_net_amount * multiplier
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:
item_wise_taxable_amount = 0.0
@@ -788,7 +828,8 @@ class calculate_taxes_and_totals:
discount_amount += total_return_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)
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)
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):
"""
Test workings of dimension filters
"""
self.setup_dimensions()
rate_in_account_currency = 1
# Invoices
@@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite):
self.assertEqual(len(pr.payments), 1)
def test_91_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
# Invoice
@@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite):
def test_92_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate_in_account_currency = 1
dpt = "Research & Development - _TC"
@@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite):
)
def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development - _TC"
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.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
class TestTaxesAndTotals(AccountsTestMixin, ERPNextTestSuite):
class TestTaxesAndTotals(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_distributed_discount_amount(self):
so = make_sales_order(do_not_save=1)

View File

@@ -1,9 +1,9 @@
import json
import frappe
from frappe.utils import flt
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.tests.utils import ERPNextTestSuite
from erpnext.tests.utils import ERPNextTestSuite, change_settings
class TestTaxesAndTotals(ERPNextTestSuite):
@@ -124,3 +124,180 @@ class TestTaxesAndTotals(ERPNextTestSuite):
]
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",
"customer_name": uuid4(),
"customer_group": "All Customer Groups",
"customer_group": "Individual",
}
).insert()
self.supplier = frappe.get_doc(

View File

@@ -2,36 +2,17 @@ import frappe
from frappe import qb
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
class TestReactivity(AccountsTestMixin, 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()
class TestReactivity(ERPNextTestSuite):
def test_01_basic_item_details(self):
self.disable_dimensions()
# set Item Price
frappe.get_doc(
{
"doctype": "Item Price",
"item_code": self.item,
"price_list": self.price_list,
"item_code": "_Test Item",
"price_list": "Standard Selling",
"price_list_rate": 90,
"selling": True,
"rate": 90,
@@ -42,17 +23,18 @@ class TestReactivity(AccountsTestMixin, ERPNextTestSuite):
si = frappe.get_doc(
{
"doctype": "Sales Invoice",
"company": self.company,
"customer": self.customer,
"debit_to": self.debit_to,
"company": "_Test Company",
"customer": "_Test Customer",
"debit_to": "Debtors - _TC",
"posting_date": today(),
"cost_center": self.cost_center,
"cost_center": "Main - _TC",
"currency": "INR",
"conversion_rate": 1,
"selling_price_list": self.price_list,
"selling_price_list": "Standard Selling",
}
)
itm = si.append("items")
itm.item_code = self.item
itm.item_code = "_Test Item"
si.process_item_selection(itm.idx)
self.assertEqual(itm.rate, 90)

View File

@@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr1.items[0].qty = 2
add_second_row_in_scr(scr1)
scr1.flags.ignore_mandatory = True
scr1.save()
scr1.set_missing_values()
scr1.save()
scr1.submit()
for _key, value in get_supplied_items(scr1).items():
@@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr2.items[0].qty = 2
add_second_row_in_scr(scr2)
scr2.flags.ignore_mandatory = True
scr2.save()
scr2.set_missing_values()
scr2.save()
scr2.submit()
for _key, value in get_supplied_items(scr2).items():
@@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite):
scr3 = make_subcontracting_receipt(sco.name)
scr3.items[0].qty = 2
scr3.flags.ignore_mandatory = True
scr3.save()
scr3.set_missing_values()
scr3.save()
scr3.submit()
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)
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):
item_dict = {}

View File

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

View File

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

View File

@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
# called from hooks on doc_event Email Unsubscribe
def unsubscribe_recipient(unsubscribe, method):
if unsubscribe.reference_doctype == "Email Campaign":
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
if unsubscribe.reference_doctype != "Email Campaign":
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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
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"

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