Merge pull request #49423 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-09-02 19:45:59 +05:30
committed by GitHub
32 changed files with 192 additions and 72 deletions

View File

@@ -142,8 +142,10 @@ def validate_expense_against_budget(args, expense_amount=0):
if not frappe.get_all("Budget", limit=1):
return
if args.get("company") and not args.fiscal_year:
if not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
if args.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", args.get("company"), "exception_budget_approver_role"
)

View File

@@ -134,7 +134,8 @@ class ExchangeRateRevaluation(Document):
accounts = self.get_accounts_data()
if accounts:
for acc in accounts:
self.append("accounts", acc)
if acc.get("gain_loss"):
self.append("accounts", acc)
@frappe.whitelist()
def get_accounts_data(self):

View File

@@ -311,7 +311,7 @@ def validate_balance_type(account, adv_adj=False):
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where account = %s""",
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
account,
)[0][0]

View File

@@ -75,6 +75,17 @@ class PeriodClosingVoucher(AccountsController):
return
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if previous_fiscal_year_closed:
return
gle_exists_in_previous_year = frappe.db.exists(
"GL Entry",
{
@@ -86,16 +97,7 @@ class PeriodClosingVoucher(AccountsController):
if not gle_exists_in_previous_year:
return
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if not previous_fiscal_year_closed:
frappe.throw(_("Previous Year is not closed, please close it first"))
frappe.throw(_("Previous Year is not closed, please close it first"))
def block_if_future_closing_voucher_exists(self):
future_closing_voucher = self.get_future_closing_voucher()

View File

@@ -668,7 +668,13 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency"
)
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
super(SalesInvoice, self).set_missing_values(for_validate)

View File

@@ -174,6 +174,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "is_cumulative",
"fieldtype": "Check",
"label": "Is Cumulative"
@@ -656,7 +657,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-02-17 18:15:39.824639",
"modified": "2025-08-20 11:40:07.096854",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@@ -583,11 +583,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field):
continue
if (
d.validate_applied_rule
and doc.get(field) is not None
and doc.get(field) < d.get(pr_field)
):
if d.validate_applied_rule and (doc.get(field) or 0) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}").format(doc.name))
else:
if not d.coupon_code_based:

View File

@@ -93,12 +93,14 @@
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "mixed_conditions",
"fieldtype": "Check",
"label": "Mixed Conditions"
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "is_cumulative",
"fieldtype": "Check",
"label": "Is Cumulative"
@@ -278,7 +280,7 @@
}
],
"links": [],
"modified": "2021-05-06 16:20:22.039078",
"modified": "2025-08-20 11:48:23.231081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme",
@@ -336,4 +338,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -644,7 +644,7 @@ frappe.ui.form.on("Purchase Invoice", {
},
add_custom_buttons: function (frm) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100 && frm.doc.update_stock == 0) {
frm.add_custom_button(
__("Purchase Receipt"),
() => {

View File

@@ -340,7 +340,12 @@ class PurchaseInvoice(BuyingController):
)
if not self.due_date:
self.due_date = get_due_date(
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
self.posting_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
template_name=self.payment_terms_template,
)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")

View File

@@ -532,6 +532,7 @@ def get_accounting_entries(
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
query = query.force_index("posting_date_company_index")
if ignore_opening_entries and not ignore_is_opening:
query = query.where(gl_entry.is_opening == "No")

View File

@@ -9,7 +9,6 @@ from frappe.query_builder import functions as fn
from frappe.utils import cstr, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
from pypika import Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_values_for_columns

View File

@@ -4,15 +4,25 @@
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory": 1,
"options": "Company",
"wildcard_filter": 0
}
],
"idx": 0,
"is_standard": "Yes",
"modified": "2019-01-17 17:20:42.374958",
"modified": "2025-08-28 19:06:54.273322",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance (Simple)",
"owner": "Administrator",
"prepared_report": 0,
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance (Simple)",
"report_type": "Query Report",

View File

@@ -39,7 +39,9 @@
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
"auto_create_purchase_receipt"
"auto_create_purchase_receipt",
"request_for_quotation_tab",
"fixed_email"
],
"fields": [
{
@@ -255,6 +257,19 @@
"fieldname": "set_valuation_rate_for_rejected_materials",
"fieldtype": "Check",
"label": "Set Valuation Rate for Rejected Materials"
},
{
"fieldname": "request_for_quotation_tab",
"fieldtype": "Tab Break",
"label": "Request for Quotation"
},
{
"description": "If set, the system does not use the user's Email or the standard outgoing Email account for sending request for quotations.",
"fieldname": "fixed_email",
"fieldtype": "Link",
"label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account"
}
],
"grid_page_length": 50,
@@ -263,7 +278,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-16 15:56:38.321369",
"modified": "2025-08-20 22:13:38.506889",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -30,6 +30,7 @@ class BuyingSettings(Document):
blanket_order_allowance: DF.Float
buying_price_list: DF.Link | None
disable_last_purchase_rate: DF.Check
fixed_email: DF.Link | None
maintain_same_rate: DF.Check
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
over_transfer_allowance: DF.Float

View File

@@ -289,7 +289,11 @@ class RequestforQuotation(BuyingController):
email_template = frappe.get_doc("Email Template", self.email_template)
message = frappe.render_template(email_template.response_, doc_args)
subject = frappe.render_template(email_template.subject, doc_args)
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
fixed_procurement_email = frappe.db.get_single_value("Buying Settings", "fixed_email")
if fixed_procurement_email:
sender = frappe.db.get_value("Email Account", fixed_procurement_email, "email_id")
else:
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
if preview:
return {"message": message, "subject": subject}

View File

@@ -188,30 +188,10 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
"link_count": 3,
"link_count": 1,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "GoCardless Settings",
"link_count": 0,
"link_to": "GoCardless Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Mpesa Settings",
"link_count": 0,
"link_to": "Mpesa Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,

View File

@@ -116,6 +116,20 @@ frappe.ui.form.on("Work Order", {
frm.set_indicator_formatter("operation", function (doc) {
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
});
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
args: {
bom_no: frm.doc.bom_no,
},
callback: function (r) {
if (r.message["set_scrap_wh_mandatory"]) {
frm.toggle_reqd("scrap_warehouse", true);
}
},
});
}
},
onload: function (frm) {

View File

@@ -390,7 +390,7 @@ erpnext.patches.v15_0.enable_allow_existing_serial_no
erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts
erpnext.patches.v15_0.update_asset_status_to_work_in_progress
erpnext.patches.v15_0.rename_manufacturing_settings_field
erpnext.patches.v15_0.sync_auto_reconcile_config
erpnext.patches.v15_0.sync_auto_reconcile_config #2025-08-26
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment

View File

@@ -11,7 +11,7 @@ def execute():
frappe.db.set_single_value("Accounts Settings", "reconciliation_queue_size", 5)
# Create Scheduler Event record if it doesn't exist
if frappe.reload_doc("core", "doctype", "scheduler_event"):
if frappe.reload_doc("core", "doctype", "scheduler_event", force=True):
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
if not frappe.db.get_all(
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}

View File

@@ -26,7 +26,7 @@ erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager
currency: this.currency,
},
{
value: this.bank_statement_closing_balance - this.cleared_balance,
value: flt(this.bank_statement_closing_balance) - flt(this.cleared_balance),
label: __("Difference"),
datatype: "Currency",
currency: this.currency,

View File

@@ -159,8 +159,9 @@ erpnext.buying = {
});
}
company(){
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
company() {
super.company();
if (!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
frappe.call({
method: "erpnext.setup.doctype.company.company.get_billing_shipping_address",

View File

@@ -459,12 +459,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
const item_scanned = row.has_item_scanned;
let warehouse_match = true;
if (has_warehouse_field) {
if (warehouse) {
warehouse_match = row[warehouse_field] === warehouse;
} else {
warehouse_match = !row[warehouse_field];
}
if (has_warehouse_field && warehouse && row[warehouse_field]) {
warehouse_match = row[warehouse_field] === warehouse;
}
return (

View File

@@ -19,6 +19,9 @@ def create_transaction_log(doc, method):
Appends the transaction to a chain of hashed logs for legal resons.
Called on submit of Sales Invoice and Payment Entry.
"""
if frappe.conf.get("disable_transaction_log", False):
return
region = get_region()
if region not in ["Germany"]:
return

View File

@@ -365,6 +365,7 @@
"fieldname": "base_net_rate",
"fieldtype": "Currency",
"label": "Net Rate (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -698,7 +699,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-12 13:49:18.765883",
"modified": "2025-08-26 20:31:47.775890",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -766,6 +766,7 @@
},
{
"default": "0",
"description": "Upon enabling this, the JV will be submitted for a different exchange rate.",
"fieldname": "submit_err_jv",
"fieldtype": "Check",
"label": "Submit ERR Journals?"
@@ -857,7 +858,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-01-09 20:12:25.471544",
"modified": "2025-08-25 18:34:03.602046",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -919,6 +920,7 @@
"select": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@@ -4146,6 +4146,36 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertTrue(sles)
def test_validate_recreate_stock_ledgers_for_sn_item(self):
item_code = "Test SN Item for Recreate Stock Ledgers"
make_item(item_code, {"has_serial_no": 1, "serial_no_series": "SN-TRSLR-.#####"})
pr = make_purchase_receipt(item_code=item_code, qty=10, rate=100)
pr.submit()
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
pluck="name",
)
self.assertTrue(sles)
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Transaction",
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"posting_date": pr.posting_date,
"posting_time": pr.posting_time,
"company": pr.company,
"recreate_stock_ledgers": 1,
}
)
self.assertRaises(frappe.ValidationError, repost_doc.save)
def test_internal_pr_qty_change_only_single_batch(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note

View File

@@ -132,4 +132,18 @@ frappe.ui.form.on("Repost Item Valuation", {
},
});
},
voucher_type: function (frm) {
frm.trigger("set_company_on_transaction");
},
voucher_no: function (frm) {
frm.trigger("set_company_on_transaction");
},
set_company_on_transaction(frm) {
if (frm.doc.voucher_no && frm.doc.voucher_type) {
frm.call("set_company");
}
},
});

View File

@@ -70,12 +70,33 @@ class RepostItemValuation(Document):
)
def validate(self):
self.set_company()
self.validate_period_closing_voucher()
self.set_status(write=False)
self.reset_field_values()
self.set_company()
self.validate_accounts_freeze()
self.reset_recreate_stock_ledgers()
self.validate_recreate_stock_ledgers()
def validate_recreate_stock_ledgers(self):
if not self.recreate_stock_ledgers:
return
items = []
if self.based_on == "Item and Warehouse":
items.append(self.item_code)
else:
items = get_items_to_be_repost(self.voucher_type, self.voucher_no)
items = list(set([d.item_code for d in items]))
if serial_batch_items := frappe.get_all(
"Item", or_filters={"has_serial_no": 1, "has_batch_no": 1}, filters={"name": ("in", items)}
):
item_list = ", ".join([d.name for d in serial_batch_items])
msg = _(
"Since {0} are Serial No/Batch No items, you cannot enable 'Recreate Stock Ledgers' in Repost Item Valuation."
).format(item_list)
frappe.throw(msg)
def validate_period_closing_voucher(self):
# Period Closing Voucher
@@ -167,6 +188,7 @@ class RepostItemValuation(Document):
def on_trash(self):
self.clear_attachment()
@frappe.whitelist()
def set_company(self):
if self.based_on == "Transaction":
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")

View File

@@ -1006,7 +1006,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: this.frm,
warehouse_field: (doc) => {
return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
return doc.purpose === "Material Receipt" ? "t_warehouse" : "s_warehouse";
},
});

View File

@@ -2123,7 +2123,16 @@ class StockEntry(StockController):
"Work Order", self.work_order, "allow_alternative_item"
)
item.from_warehouse = self.from_warehouse or item.source_warehouse or item.default_warehouse
item.from_warehouse = (
frappe.get_value(
"Work Order Item",
{"parent": self.work_order, "item_code": item.item_code},
"source_warehouse",
)
if frappe.get_value("Work Order", self.work_order, "skip_transfer")
and not frappe.get_value("Work Order", self.work_order, "from_wip_warehouse")
else self.from_warehouse or item.source_warehouse or item.default_warehouse
)
if item.item_code in used_alternative_items:
alternative_item_data = used_alternative_items.get(item.item_code)
item.item_code = alternative_item_data.item_code

View File

@@ -770,12 +770,15 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.non_batchwise_valuation_batches = self.batches
return
batches = frappe.get_all(
"Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
)
if get_valuation_method(self.sle.item_code) == "FIFO":
self.batchwise_valuation_batches = self.batches
else:
batches = frappe.get_all(
"Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
)
for batch in batches:
self.batchwise_valuation_batches.append(batch.name)
for batch in batches:
self.batchwise_valuation_batches.append(batch.name)
self.non_batchwise_valuation_batches = list(set(self.batches) - set(self.batchwise_valuation_batches))