diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index b510651e68f..4098084a802 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _, throw
-from frappe.utils import cint, cstr
+from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
@@ -481,6 +481,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
+ _ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
if not account:
return
@@ -542,6 +543,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
+ _ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -595,3 +597,27 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)
+
+
+def _ensure_idle_system():
+ # Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
+ # 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
+ # 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
+
+ if frappe.flags.in_test:
+ return
+
+ try:
+ # We also lock inserts to GL entry table with for_update here.
+ last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
+ except frappe.QueryTimeoutError:
+ # wait=False fails immediately if there's an active transaction.
+ last_gl_update = add_to_date(None, seconds=-1)
+
+ if last_gl_update > add_to_date(None, minutes=-5):
+ frappe.throw(
+ _(
+ "Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
+ ).format(pretty_date(last_gl_update)),
+ title=_("System In Use"),
+ )
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 967b12599bf..45462398e1c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1843,7 +1843,7 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
- elif self.party_type in ("Supplier", "Employee"):
+ elif self.party_type in ("Supplier", "Customer"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
@@ -3337,13 +3337,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": flt(base_loss_on_income, precision),
+ "amount": flt(base_loss_on_income, precision) * positive_negative,
},
)
@@ -3355,6 +3356,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
tax_discount_loss = {}
base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
# The same account head could be used more than once
for tax in doc.get("taxes", []):
@@ -3377,7 +3379,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"account": account,
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": flt(loss, precision),
+ "amount": flt(loss, precision) * positive_negative,
},
)
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 5883d4e2f1f..e43ba85373c 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -282,6 +282,48 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
+ def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ create_payment_terms_template_with_discount()
+ pi.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
+
+ pi.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
+ pi.save()
+ pi.submit()
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
+ self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
+ self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
+ self.assertEqual(pe_with_tax_loss.difference_amount, 0)
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe.payment_type, "Pay")
+ self.assertEqual(pe.references[0].allocated_amount, 295.0)
+ self.assertEqual(pe.paid_amount, 265.5)
+ self.assertEqual(pe.deductions[0].amount, -29.5)
+ self.assertEqual(pe.difference_amount, 0)
+
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 42861140494..c15309df294 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1623,6 +1623,5 @@
"states": [],
"timeline_field": "customer",
"title_field": "title",
- "track_changes": 1,
- "track_seen": 1
-}
\ No newline at end of file
+ "track_changes": 1
+}
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
index 46b430c6594..51dc3674594 100644
--- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
@@ -13,17 +13,15 @@
"fields": [
{
"fieldname": "voucher_type",
- "fieldtype": "Link",
+ "fieldtype": "Data",
"in_list_view": 1,
- "label": "Voucher Type",
- "options": "DocType"
+ "label": "Voucher Type"
},
{
"fieldname": "voucher_name",
- "fieldtype": "Dynamic Link",
+ "fieldtype": "Data",
"in_list_view": 1,
- "label": "Voucher Name",
- "options": "voucher_type"
+ "label": "Voucher Name"
},
{
"fieldname": "taxable_amount",
@@ -36,7 +34,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-01-13 13:40:41.479208",
+ "modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
index bc2003e2bea..dbb69a2e769 100644
--- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
parentfield: DF.Data
parenttype: DF.Data
taxable_amount: DF.Currency
- voucher_name: DF.DynamicLink | None
- voucher_type: DF.Link | None
+ voucher_name: DF.Data | None
+ voucher_type: DF.Data | None
# end: auto-generated types
pass
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 06549973242..a355e5ddf44 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -436,6 +436,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"),
company,
),
+ as_dict=1,
)
for d in journal_entries_details:
diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
index 8971dc3d37b..69f332d9800 100644
--- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD")
+ def test_account_payable_for_debit_note(self):
+ pi = self.create_purchase_invoice(do_not_submit=True)
+ pi.is_return = 1
+ pi.items[0].qty = -1
+ pi = pi.save().submit()
+
+ filters = {
+ "company": self.company,
+ "party_type": "Supplier",
+ "party": [self.supplier],
+ "report_date": today(),
+ "range": "30, 60, 90, 120",
+ }
+
+ data = execute(filters)
+ self.assertEqual(data[1][0].get("invoiced"), 300)
+
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")
pi = make_purchase_invoice(
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 1ddf9bce06f..c7a0da5afe9 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -267,6 +267,18 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
+ # when invoice has is_return marked
+ if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
+ # for Credit Note
+ if row.voucher_type == "Sales Invoice":
+ row.credit_note -= amount
+ row.credit_note_in_account_currency -= amount_in_account_currency
+ # for Debit Note
+ else:
+ row.invoiced -= amount
+ row.invoiced_in_account_currency -= amount_in_account_currency
+ return
+
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
# nosemgrep
si_list = frappe.db.sql(
"""
- select name, due_date, po_no
+ select name, due_date, po_no, is_return
from `tabSales Invoice`
where posting_date <= %s
and company = %s
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
# nosemgrep
for pi in frappe.db.sql(
"""
- select name, due_date, bill_no, bill_date
+ select name, due_date, bill_no, bill_date, is_return
from `tabPurchase Invoice`
where
posting_date <= %s
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 39ca78153c3..f3513286c9e 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
- [0, 0, 100.0, 0.0, -100.0, cr_note.name],
+ [0, 0, 0, 100.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = next(
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1]
self.assertEqual(len(report), 2)
- expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
+ expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
- [row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
+ [
+ row.invoiced or row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.remaining_balance,
+ row.future_amount,
+ ],
)
pe.cancel()
diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
index f6efc8a685c..dc6192e7544 100644
--- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
+++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0],
+ ["Purchase Invoice", "is_opening", "!=", "Yes"],
]
if report_filters.get("purchase_invoice"):
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index e540aa9993c..db42d23a839 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
and ba.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
+ and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
index 58657da9168..99b4c26ac8e 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ on_change: (report) => {
+ report.set_filter_value("name", []);
+ report.refresh();
+ },
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
+ on_change: (report) => {
+ report.set_filter_value("name", []);
+ report.refresh();
+ },
},
{
fieldname: "project",
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
{
fieldname: "name",
label: __("Purchase Order"),
- fieldtype: "Link",
+ fieldtype: "MultiSelectList",
width: "80",
options: "Purchase Order",
- get_query: () => {
- return {
- filters: { docstatus: 1 },
- };
+ get_data: function (txt) {
+ let filters = { docstatus: 1 };
+
+ const from_date = frappe.query_report.get_filter_value("from_date");
+ const to_date = frappe.query_report.get_filter_value("to_date");
+ if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
+
+ return frappe.db.get_link_options("Purchase Order", txt, filters);
},
},
{
@@ -52,9 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
label: __("Status"),
fieldtype: "MultiSelectList",
width: "80",
- options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
+ options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
get_data: function (txt) {
- let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
+ let status = [
+ "To Pay",
+ "To Bill",
+ "To Receive",
+ "To Receive and Bill",
+ "Completed",
+ "Closed",
+ ];
let options = [];
for (let option of status) {
options.push({
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
index f583ce3e6c8..b6bf1d9f8da 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
@@ -70,14 +70,16 @@ def get_data(filters):
po.company,
po_item.name,
)
- .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
+ .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
.groupby(po_item.name)
.orderby(po.transaction_date)
)
- for field in ("company", "name"):
- if filters.get(field):
- query = query.where(po[field] == filters.get(field))
+ if filters.get("company"):
+ query = query.where(po.company == filters.get("company"))
+
+ if filters.get("name"):
+ query = query.where(po.name.isin(filters.get("name")))
if filters.get("from_date") and filters.get("to_date"):
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index bb0a4070981..6ed55a0b55e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -194,6 +194,14 @@ class AccountsController(TransactionBase):
self.set_incoming_rate()
self.init_internal_values()
+ # Need to set taxes based on taxes_and_charges template
+ # before calculating taxes and totals
+ if self.meta.get_field("taxes_and_charges"):
+ self.validate_enabled_taxes_and_charges()
+ self.validate_tax_account_company()
+
+ self.set_taxes_and_charges()
+
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -204,10 +212,6 @@ class AccountsController(TransactionBase):
self.validate_all_documents_schedule()
- if self.meta.get_field("taxes_and_charges"):
- self.validate_enabled_taxes_and_charges()
- self.validate_tax_account_company()
-
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
@@ -252,8 +256,6 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
- self.set_taxes_and_charges()
-
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -821,11 +823,15 @@ class AccountsController(TransactionBase):
and item.get("use_serial_batch_fields")
)
):
- if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
- item.set("rate", ret.get("rate"))
- item.set("price_list_rate", ret.get("price_list_rate"))
item.set(fieldname, value)
+ if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
+ if ret.get("rate"):
+ item.set("rate", ret.get("rate"))
+
+ if not item.get("price_list_rate") and ret.get("price_list_rate"):
+ item.set("price_list_rate", ret.get("price_list_rate"))
+
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
fieldname
):
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index 2c46f04af71..f959cbd0488 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -931,6 +931,35 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
+ @change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
+ def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
+ # Create a Sales Taxes and Charges Template
+ if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
+ doc = frappe.new_doc("Sales Taxes and Charges Template")
+ doc.company = self.company
+ doc.title = "_Test Tax"
+ doc.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "Sales Expenses - _TC",
+ "description": "Test taxes",
+ "rate": 9,
+ },
+ )
+ doc.insert()
+
+ # Create a Sales Invoice
+ sinv = frappe.new_doc("Sales Invoice")
+ sinv.customer = self.customer
+ sinv.company = self.company
+ sinv.currency = "INR"
+ sinv.taxes_and_charges = "_Test Tax - _TC"
+ sinv.append("items", {"item_code": "_Test Item", "qty": 1, "rate": 50})
+ sinv.insert()
+
+ self.assertEqual(sinv.total_taxes_and_charges, 4.5)
+
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2b23cca886e..21f301f2450 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = """ERP made simple"""
app_icon = "fa fa-th"
app_color = "#e74c3c"
-app_email = "info@erpnext.com"
+app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -484,7 +484,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """
Sent via
-
+
ERPNext
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index 45ce95b6d58..c4fb6345c93 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -28,6 +28,8 @@ BOM_ITEM_FIELDS = [
"stock_uom",
"conversion_factor",
"do_not_explode",
+ "source_warehouse",
+ "allow_alternative_item",
]
@@ -291,7 +293,6 @@ class BOMCreator(Document):
"item": row.item_code,
"bom_type": "Production",
"quantity": row.qty,
- "allow_alternative_item": 1,
"bom_creator": self.name,
"bom_creator_item": bom_creator_item,
}
@@ -315,7 +316,6 @@ class BOMCreator(Document):
item_args.update(
{
"bom_no": bom_no,
- "allow_alternative_item": 1,
"allow_scrap_items": 1,
"include_item_in_manufacturing": 1,
}
@@ -428,6 +428,7 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1,
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
+ "allow_alternative_item": kwargs.allow_alternative_item,
},
)
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index 1726f898751..a6e67b956cf 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -15,6 +15,7 @@
"is_expandable",
"sourced_by_supplier",
"bom_created",
+ "allow_alternative_item",
"description_section",
"description",
"quantity_and_rate_section",
@@ -225,12 +226,18 @@
"label": "BOM Created",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "allow_alternative_item",
+ "fieldtype": "Check",
+ "label": "Allow Alternative Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-06-03 18:45:24.339532",
+ "modified": "2025-02-19 13:25:15.732496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
index e172f36224d..fdd3f77ae26 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -14,6 +14,7 @@ class BOMCreatorItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
+ allow_alternative_item: DF.Check
amount: DF.Currency
base_amount: DF.Currency
base_rate: DF.Currency
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index cb10fcce1c4..df1b885ad18 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -394,3 +394,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_post
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime
+erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index ff77fbb91ec..d4350d8f9a1 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -82,6 +82,9 @@ def get_asset_depreciation_schedules_map():
.orderby(ds.idx)
).run(as_dict=True)
+ if len(records) > 20000:
+ frappe.db.auto_commit_on_many_writes = True
+
asset_depreciation_schedules_map = frappe._dict()
for d in records:
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index b1019f67ca9..6d24751792c 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -210,6 +210,13 @@ class BOMConfigurator {
[
{ label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
{ label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
+ {
+ label: __("Allow Alternative Item"),
+ fieldname: "allow_alternative_item",
+ default: 1.0,
+ fieldtype: "Check",
+ reqd: 1,
+ },
],
(data) => {
if (!node.data.parent_id) {
@@ -224,6 +231,7 @@ class BOMConfigurator {
item_code: data.item_code,
fg_reference_id: node.data.name || this.frm.doc.name,
qty: data.qty,
+ allow_alternative_item: data.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -258,6 +266,7 @@ class BOMConfigurator {
fg_item: node.data.value,
fg_reference_id: node.data.name || this.frm.doc.name,
bom_item: bom_item,
+ allow_alternative_item: bom_item.allow_alternative_item,
},
callback: (r) => {
view.events.load_tree(r, node);
@@ -278,6 +287,14 @@ class BOMConfigurator {
reqd: 1,
read_only: read_only,
},
+ {
+ label: __("Allow Alternative Item"),
+ fieldname: "allow_alternative_item",
+ default: 1.0,
+ fieldtype: "Check",
+ reqd: 1,
+ read_only: read_only,
+ },
{ fieldtype: "Column Break" },
{
label: __("Qty"),
diff --git a/erpnext/public/js/utils/landed_taxes_and_charges_common.js b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
index 2cb30160453..7d801ca91e6 100644
--- a/erpnext/public/js/utils/landed_taxes_and_charges_common.js
+++ b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
@@ -14,6 +14,10 @@ erpnext.landed_cost_taxes_and_charges = {
"Income Account",
"Expenses Included In Valuation",
"Expenses Included In Asset Valuation",
+ "Expense Account",
+ "Direct Expense",
+ "Indirect Expense",
+ "Stock Received But Not Billed",
],
],
company: frm.doc.company,
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index de1faf36ef5..e02d7a3d785 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -540,6 +540,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
has_batch_no: this.item.has_batch_no,
qty: qty,
based_on: based_on,
+ posting_date: this.frm.doc.posting_date,
+ posting_time: this.frm.doc.posting_time,
},
callback: (r) => {
if (r.message) {
diff --git a/erpnext/regional/address_template/templates/united_states.html b/erpnext/regional/address_template/templates/united_states.html
index 77fce46b9d7..f00f99c1299 100644
--- a/erpnext/regional/address_template/templates/united_states.html
+++ b/erpnext/regional/address_template/templates/united_states.html
@@ -1,4 +1,4 @@
{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}
-{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}
{% endif -%}
+{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}
{% if country != "United States" %}{{ country }}{% endif -%}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 47d42b0a9d5..003ffd5ac82 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2097,6 +2097,45 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
+ def test_delivery_note_rate_on_change_of_warehouse(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ item = make_item(
+ "_Test Batch Item for Delivery Note Rate",
+ {
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BH-SDDTBIFRM-.#####",
+ },
+ )
+
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
+ so = make_sales_order(
+ item_code=item.name, rate=27648.00, price_list_rate=27648.00, qty=1, do_not_submit=True
+ )
+
+ so.items[0].rate = 90
+ so.save()
+ self.assertTrue(so.items[0].discount_amount == 27558.0)
+ so.submit()
+
+ warehouse = create_warehouse("NW Warehouse FOR Rate", company=so.company)
+
+ make_stock_entry(
+ item_code=item.name,
+ qty=2,
+ target=warehouse,
+ basic_rate=100,
+ company=so.company,
+ use_serial_batch_fields=1,
+ )
+
+ dn = make_delivery_note(so.name)
+ dn.items[0].warehouse = warehouse
+ dn.save()
+
+ self.assertEqual(dn.items[0].rate, 90)
+
def test_credit_limit_on_so_reopning(self):
# set credit limit
company = "_Test Company"
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index b86a87983d5..7f758f4c8db 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -320,13 +320,13 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
- invoices_by_customer = frappe.db.get_all(
+ invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields,
page_length=limit,
)
- invoices_by_name = frappe.db.get_all(
+ invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields,
@@ -335,7 +335,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name
elif status:
- invoice_list = frappe.db.get_all(
+ invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
)
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
index 5866fcbc845..b7f7a34c1b8 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ on_change: (report) => {
+ report.set_filter_value("sales_order", []);
+ report.refresh();
+ },
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
+ on_change: (report) => {
+ report.set_filter_value("sales_order", []);
+ report.refresh();
+ },
},
{
fieldname: "sales_order",
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
options: "Sales Order",
get_data: function (txt) {
- return frappe.db.get_link_options("Sales Order", txt);
- },
- get_query: () => {
- return {
- filters: { docstatus: 1 },
- };
+ let filters = { docstatus: 1 };
+
+ const from_date = frappe.query_report.get_filter_value("from_date");
+ const to_date = frappe.query_report.get_filter_value("to_date");
+ if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
+
+ return frappe.db.get_link_options("Sales Order", txt, filters);
},
},
{
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status",
label: __("Status"),
fieldtype: "MultiSelectList",
- options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
+ options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed", "Closed"],
width: "80",
get_data: function (txt) {
- let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"];
+ let status = [
+ "To Pay",
+ "To Bill",
+ "To Deliver",
+ "To Deliver and Bill",
+ "Completed",
+ "Closed",
+ ];
let options = [];
for (let option of status) {
options.push({
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 8fcf29bd7a6..90c33c323ce 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -86,7 +86,7 @@ def get_data(conditions, filters):
ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
soi.parent = so.name
- and so.status not in ('Stopped', 'Closed', 'On Hold')
+ and so.status not in ('Stopped', 'On Hold')
and so.docstatus = 1
{conditions}
GROUP BY soi.name
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 97ec418d955..23ffd49de5d 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -16,7 +16,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
default_mail_footer = """