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

chore: release v15
This commit is contained in:
ruthra kumar
2025-12-16 21:01:44 +05:30
committed by GitHub
45 changed files with 562 additions and 193 deletions

View File

@@ -38,7 +38,10 @@
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
"bank_party_iban",
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
],
"fields": [
{
@@ -233,12 +236,32 @@
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-10-23 17:32:58.514807",
"modified": "2025-12-07 20:49:18.600757",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -32,6 +32,8 @@ class BankTransaction(Document):
date: DF.Date | None
deposit: DF.Currency
description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None
party_type: DF.Link | None
@@ -45,9 +47,11 @@ class BankTransaction(Document):
# end: auto-generated types
def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount()
def validate(self):
self.validate_included_fee()
self.validate_duplicate_references()
self.validate_currency()
@@ -307,6 +311,40 @@ class BankTransaction(Document):
self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():

View File

@@ -0,0 +1,133 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
class TestBankTransactionFees(FrappeTestCase):
def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 101
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
def test_included_fee_allows_equal(self):
"""A fee that's part of a withdrawal may be equal to the withdrawal
amount (only the fee was deducted from the account)."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 100
bt.validate_included_fee()
def test_included_fee_allows_for_deposit(self):
"""For deposits, a fee may be recorded separately without limiting the
received amount."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.included_fee = 999
bt.validate_included_fee()
def test_excluded_fee_noop_when_zero(self):
"""When there is no excluded fee to apply, the amounts should remain
unchanged."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 5
bt.excluded_fee = 0
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 100)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_throws_when_exceeds_deposit(self):
"""A fee deducted from an incoming payment must not exceed the incoming
amount (else it would be a withdrawal, a conversion we don't support)."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.excluded_fee = 11
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
"""A transaction must be either incoming or outgoing when applying a
fee, not both."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.withdrawal = 10
bt.excluded_fee = 1
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_deducts_from_deposit(self):
"""When a fee is deducted from an incoming payment, the net received
amount decreases and the fee is tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 95)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
"""A separately-deducted fee may reduce an incoming payment to zero,
while still tracking the fee."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 5
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_increases_outgoing_payment(self):
"""When a separately-deducted fee is provided for an outgoing payment,
the total money leaving increases and the fee is tracked."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 100
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 105)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
"""If only an excluded fee is provided, it should be treated as an
outgoing payment and the fee is then tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 5)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.dev") {
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.dev":
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
self.set("result_key", [])
self.set("req_params", [])
@@ -105,9 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"

View File

@@ -486,6 +486,9 @@ class ExchangeRateRevaluation(Document):
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
# Prevent JE from overriding user-entered exchange rates (e.g., rate of 1)
journal_entry.flags.ignore_exchange_rate = True
journal_entry_accounts = []
for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):

View File

@@ -1302,15 +1302,14 @@ frappe.ui.form.on("Payment Entry", {
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const response = await get_company_defaults(frm.doc.company);
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
const account =
response.message?.[account_fieldname] ||
company_defaults?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = response.message?.cost_center;
row.cost_center = company_defaults?.cost_center;
row.is_exchange_gain_loss = 1;
}

View File

@@ -545,6 +545,9 @@ def make_payment_request(**args):
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company

View File

@@ -575,17 +575,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.cost_center) {
var cl = doc.items || [];
for (var i = 0; i < cl.length; i++) {
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
}
}
refresh_field("items");
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],

View File

@@ -648,10 +648,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
};
cur_frm.set_query("debit_to", function (doc) {
return {
filters: {

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-10 18:32:36.201124",
"modified": "2025-12-10 08:06:40.611761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Int
amount: DF.Currency
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Int
rate: DF.Currency
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -199,19 +199,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
for d in gl_map:
cost_center = d.get("cost_center")
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
# Validate budget against main cost center
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
@@ -414,7 +415,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.notify_update = False
gle.submit()
if not from_repost and gle.voucher_type != "Period Closing Voucher":
if (
not from_repost
and gle.voucher_type != "Period Closing Voucher"
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
):
validate_expense_against_budget(args)

View File

@@ -482,7 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle):
def update_value_in_dict(data, key, gle, show_net_values=False):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -493,10 +493,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -526,11 +530,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -399,7 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
}
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
row[key] = flt(d.get(key, 0.0))
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values

View File

@@ -378,6 +378,9 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -587,7 +590,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
grand_total_diff = self.grand_total_diff
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -850,12 +853,11 @@ class calculate_taxes_and_totals:
)
)
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

View File

@@ -412,29 +412,29 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
],
# Daily but offset by 45 minutes
"45 0 * * *": [
"erpnext.stock.reorder_item.reorder_item",
],
"45 0 * * *": [],
},
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
"hourly_long": [],
"hourly_maintenance": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"daily": [
"daily": [],
"daily_long": [],
"daily_maintenance": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
@@ -458,17 +458,16 @@ scheduler_events = {
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
"erpnext.stock.reorder_item.reorder_item",
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",

View File

@@ -403,6 +403,10 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.then((data) => {
if (flt(data.qty) <= 0) {
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
return;
}
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: "Disassemble",

View File

@@ -979,14 +979,14 @@ class WorkOrder(Document):
for d in self.get("operations"):
precision = d.precision("completed_qty")
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision)
if not qty:
d.status = "Pending"
elif flt(qty) < flt(self.qty):
elif qty < flt(self.qty, precision):
d.status = "Work in Progress"
elif flt(qty) == flt(self.qty):
elif qty == flt(self.qty, precision):
d.status = "Completed"
elif flt(qty) <= max_allowed_qty_for_wo:
elif qty <= flt(max_allowed_qty_for_wo, precision):
d.status = "Completed"
else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
@@ -1509,7 +1509,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items(qty, work_order.production_item)
stock_entry.get_items()
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()

View File

@@ -426,4 +426,4 @@ erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11

View File

@@ -2,8 +2,13 @@ import frappe
def execute():
settings_meta = frappe.get_meta("Currency Exchange Settings")
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
if (
"frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n")
or settings.service_provider != "frankfurter.app"
):
return
settings.service_provider = "frankfurter.dev"

View File

@@ -75,13 +75,27 @@ def get_chart_data(data):
delay = delay + 1
else:
on_track = on_track + 1
labels = []
datasets = []
colors = []
if on_track:
labels.append(_("On Track"))
datasets.append(on_track)
colors.append("#84D5BA")
if delay:
labels.append(_("Delayed"))
datasets.append(delay)
colors.append("#CB4B5F")
charts = {
"data": {
"labels": [_("On Track"), _("Delayed")],
"datasets": [{"name": _("Delayed"), "values": [on_track, delay]}],
"labels": labels,
"datasets": [{"name": _("Delayed"), "values": datasets}],
},
"type": "percentage",
"colors": ["#84D5BA", "#CB4B5F"],
"colors": colors,
}
return charts

View File

@@ -343,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
// reset value from earlier calculations
this.grand_total_diff = 0;
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
@@ -578,6 +581,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.grand_total_diff = diff;
} else {
me.grand_total_diff = 0;
}
}
}
@@ -587,7 +592,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
const grand_total_diff = this.grand_total_diff;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff

View File

@@ -1093,12 +1093,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.refresh_field("payment_schedule");
}
cost_center(doc) {
this.frm.doc.items.forEach((item) => {
item.cost_center = doc.cost_center;
});
this.frm.refresh_field("items");
cost_center(doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
}
due_date(doc, cdt, cdn) {

View File

@@ -1408,7 +1408,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1417,6 +1416,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},
@@ -1549,7 +1549,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1558,6 +1557,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},

View File

@@ -11,7 +11,13 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
};
};
this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.employee_query" };
return {
query: "erpnext.controllers.queries.employee_query",
filters: [
["status", "=", "Active"],
["name", "!=", doc.name],
],
};
};
}

View File

@@ -347,8 +347,9 @@ class TransactionDeletionRecord(Document):
self.db_set("error_log", None)
def get_doctypes_to_be_ignored_list(self):
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
doctypes_to_be_ignored_list = singles
doctypes_to_be_ignored_list = frappe.get_all(
"DocType", or_filters=[["issingle", "=", 1], ["is_virtual", "=", 1]], pluck="name"
)
for doctype in self.doctypes_to_be_ignored:
doctypes_to_be_ignored_list.append(doctype.doctype_name)

View File

@@ -4,14 +4,13 @@
import click
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint
import erpnext
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from erpnext.setup.utils import identity as _
from .default_success_action import get_default_success_action
@@ -193,28 +192,27 @@ def add_company_to_session_defaults():
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
erpnext_navbar_items = [
{
"item_label": "Documentation",
"item_label": _("Documentation"),
"item_type": "Route",
"route": "https://docs.erpnext.com/",
"is_standard": 1,
},
{
"item_label": "User Forum",
"item_label": _("User Forum"),
"item_type": "Route",
"route": "https://discuss.frappe.io",
"is_standard": 1,
},
{
"item_label": "Frappe School",
"item_label": _("Frappe School"),
"item_type": "Route",
"route": "https://frappe.io/school?utm_source=in_app",
"is_standard": 1,
},
{
"item_label": "Report an Issue",
"item_label": _("Report an Issue"),
"item_type": "Route",
"route": "https://github.com/frappe/erpnext/issues",
"is_standard": 1,

View File

@@ -234,3 +234,15 @@ def welcome_email():
site_name = get_default_company() or "ERPNext"
title = _("Welcome to {0}").format(site_name)
return title
def identity(x, *args, **kwargs):
"""Used for redefining the translation function to return the string as is.
We want to create english records but still mark the strings as translatable.
E.g. when the respective DocTypes have 'Translate Link Fields' enabled or
we're creating custom fields.
Use like this: `from erpnext.setup.utils import identity as _`
"""
return x

View File

@@ -50,7 +50,7 @@ def boot_session(bootinfo):
bootinfo.docs += frappe.db.sql(
"""select name, default_currency, cost_center, default_selling_terms, default_buying_terms,
default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""",
default_letter_head, default_bank_account, enable_perpetual_inventory, country, exchange_gain_loss_account from `tabCompany`""",
as_dict=1,
update={"doctype": ":Company"},
)

View File

@@ -130,10 +130,6 @@ frappe.ui.form.on("Delivery Note Item", {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "expense_account", d.expense_account);
},
cost_center: function (frm, dt, dn) {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "cost_center", d.cost_center);
},
});
erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (

View File

@@ -504,6 +504,61 @@ class TestInventoryDimension(FrappeTestCase):
self.assertEqual(site_name, "Site 1")
def test_validate_negative_stock_with_multiple_dimension(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
item_code = "Test Negative Multi Inventory Dimension Item"
create_item(item_code)
inv_dimension_1 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Inv Site",
reference_document="Inv Site",
document_type="Inv Site",
validate_negative_stock=1,
)
inv_dimension_1.db_set("validate_negative_stock", 1)
inv_dimension_2 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Rack",
reference_document="Rack",
document_type="Rack",
validate_negative_stock=1,
)
inv_dimension_2.db_set("validate_negative_stock", 1)
frappe.local.inventory_dimensions = {}
frappe.local.document_wise_inventory_dimensions = {}
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True)
dn_doc.items[0].inv_site = "Site 2"
dn_doc.items[0].rack = "Rack 1"
dn_doc.save()
self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit)
def get_voucher_sl_entries(voucher_no, fields):
return frappe.get_all(
@@ -593,7 +648,7 @@ def prepare_test_data():
}
).insert(ignore_permissions=True)
for rack in ["Rack 1"]:
for rack in ["Rack 1", "Rack 2"]:
if not frappe.db.exists("Rack", rack):
frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True)

View File

@@ -167,6 +167,7 @@
"in_preview": 1,
"in_standard_filter": 1,
"label": "Item Group",
"link_filters": "[[\"Item Group\",\"is_group\",\"=\",0]]",
"oldfieldname": "item_group",
"oldfieldtype": "Link",
"options": "Item Group",
@@ -894,7 +895,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-12-04 09:11:56.029567",
"modified": "2025-12-15 20:08:35.634046",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -19,6 +19,7 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -926,6 +927,49 @@ class TestMaterialRequest(FrappeTestCase):
pl_for_pending = create_pick_list(mr.name)
self.assertEqual(pl_for_pending.locations[0].qty, 5)
def test_mr_status_with_partial_and_excess_end_transit(self):
material_request = make_material_request(
material_request_type="Material Transfer",
item_code="_Test Item Home Desktop 100",
)
in_transit_wh = get_in_transit_warehouse(material_request.company)
# Make sure stock is available in source warehouse
self._insert_stock_entry(20.0, 20.0)
# Stock Entry (Transfer to In-Transit)
stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_1.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_1.save().submit()
stock_entry_2 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_2.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_2.save().submit()
end_transit_1 = make_stock_in_entry(stock_entry_1.name)
end_transit_1.save().submit()
# Material Request Transfer Status should still be In Transit
material_request.load_from_db()
self.assertEqual(material_request.transfer_status, "In Transit")
end_transit_2 = make_stock_in_entry(stock_entry_2.name)
end_transit_2.items[0].update({"qty": 6}) # More than transferred
end_transit_2.save()
self.assertRaises(frappe.ValidationError, end_transit_2.submit)
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import CombineDatetime
from frappe.query_builder.functions import Abs, CombineDatetime, Sum
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
from pypika import functions as fn
@@ -1372,21 +1372,26 @@ def get_invoiced_qty_map(purchase_receipt):
def get_returned_qty_map(purchase_receipt):
"""returns a map: {so_detail: returned_qty}"""
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent
and pr.docstatus = 1
and pr.is_return = 1
and pr.return_against = %s
""",
purchase_receipt,
)
)
"""returns a map: {pr_detail: returned_qty}"""
return returned_qty_map
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr.name == pr_item.parent)
.select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty"))
.where(
(pr.docstatus == 1)
& (pr.is_return == 1)
& (pr.return_against == purchase_receipt)
& (pr_item.purchase_receipt_item.isnotnull())
)
.groupby(pr_item.purchase_receipt_item)
).run(as_list=1)
return frappe._dict(query) if query else frappe._dict()
@frappe.whitelist()

View File

@@ -2008,6 +2008,8 @@ def get_available_serial_nos(kwargs):
filters["name"] = ("in", time_based_serial_nos)
elif ignore_serial_nos:
filters["name"] = ("not in", ignore_serial_nos)
elif kwargs.get("serial_nos"):
filters["name"] = ("in", kwargs.get("serial_nos"))
if kwargs.get("batches"):
batches = get_non_expired_batches(kwargs.get("batches"))

View File

@@ -8,6 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
@@ -1905,7 +1906,7 @@ class StockEntry(StockController):
},
)
def get_items_for_disassembly(self, disassemble_qty, production_item):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
@@ -1918,7 +1919,7 @@ class StockEntry(StockController):
items_dict = get_bom_items_as_dict(
self.bom_no,
self.company,
disassemble_qty,
self.fg_completed_qty,
fetch_exploded=self.use_multi_level_bom,
fetch_qty_in_stock_uom=False,
)
@@ -1935,8 +1936,8 @@ class StockEntry(StockController):
child_row.qty = bom_items.get("qty", child_row.qty)
child_row.amount = bom_items.get("amount", child_row.amount)
if row.item_code == production_item:
child_row.qty = disassemble_qty
if row.is_finished_item:
child_row.qty = self.fg_completed_qty
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = row.s_warehouse
@@ -1972,12 +1973,12 @@ class StockEntry(StockController):
)
@frappe.whitelist()
def get_items(self, qty=None, production_item=None):
def get_items(self):
self.set("items", [])
self.validate_work_order()
if self.purpose == "Disassemble" and qty is not None:
return self.get_items_for_disassembly(qty, production_item)
if self.purpose == "Disassemble":
return self.get_items_for_disassembly()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))
@@ -2815,6 +2816,17 @@ class StockEntry(StockController):
},
)
if d.docstatus == 1:
transfer_qty = frappe.get_value("Stock Entry Detail", d.ste_detail, "transfer_qty")
if transferred_qty and transferred_qty[0]:
if transferred_qty[0].qty > transfer_qty:
frappe.throw(
_(
"Row {0}: Transferred quantity cannot be greater than the requested quantity."
).format(d.idx)
)
stock_entries[(d.against_stock_entry, d.ste_detail)] = (
transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0
) or 0.0
@@ -2878,7 +2890,7 @@ class StockEntry(StockController):
parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit")
for item in self.items:
material_request = item.material_request or None
material_request = item.get("material_request")
if self.purpose == "Material Transfer" and material_request not in material_requests:
if self.outgoing_stock_entry and parent_se:
material_request = frappe.get_value(
@@ -2887,6 +2899,11 @@ class StockEntry(StockController):
if material_request and material_request not in material_requests:
material_requests.append(material_request)
if status == "Completed":
qty = get_transferred_qty(material_request)
if qty.get("transfer_qty") > qty.get("transferred_qty"):
status = "In Transit"
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
def set_serial_no_batch_for_finished_good(self):
@@ -3545,3 +3562,18 @@ def get_batchwise_serial_nos(item_code, row):
batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos])
return batchwise_serial_nos
def get_transferred_qty(material_request):
sed = DocType("Stock Entry Detail")
query = (
frappe.qb.from_(sed)
.select(
Sum(sed.transfer_qty).as_("transfer_qty"),
Sum(sed.transferred_qty).as_("transferred_qty"),
)
.where((sed.material_request == material_request) & (sed.docstatus == 1))
).run(as_dict=True)
return query[0]

View File

@@ -113,17 +113,15 @@ class StockLedgerEntry(Document):
return
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value)
available_qty = self.get_available_qty_after_prev_transaction(dimensions)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimension, dimension_value)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
def get_available_qty_after_prev_transaction(self, dimension, dimension_value):
def get_available_qty_after_prev_transaction(self, dimensions):
sle = frappe.qb.DocType("Stock Ledger Entry")
available_qty = (
available_qty_query = (
frappe.qb.from_(sle)
.select(Sum(sle.actual_qty))
.where(
@@ -132,21 +130,27 @@ class StockLedgerEntry(Document):
& (sle.posting_datetime < self.posting_datetime)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle[dimension] == dimension_value)
)
).run()
)
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty_query = available_qty_query.where(sle[dimension] == dimension_value)
available_qty = available_qty_query.run()
return available_qty[0][0] or 0
def throw_validation_error(self, diff, dimension, dimension_value):
def throw_validation_error(self, diff, dimensions):
msg = _(
"{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction."
"{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction."
).format(
abs(diff),
frappe.get_desk_link("Item", self.item_code),
frappe.get_desk_link("Warehouse", self.warehouse),
frappe.bold(dimension),
frappe.bold(dimension_value),
frappe.bold(
", ".join([f"{dimension}: {values.get('value')}" for dimension, values in dimensions.items()])
),
self.posting_date,
self.posting_time,
frappe.get_desk_link(self.voucher_type, self.voucher_no),

View File

@@ -20,6 +20,9 @@ def execute(filters=None):
def get_chart_data(data, filters):
def wrap_in_quotes(label):
return f"'{label}'"
if not data:
return []
@@ -36,6 +39,9 @@ def get_chart_data(data, filters):
data = data[:10]
for row in data:
if row[0] == wrap_in_quotes(_("Total")):
continue
labels.append(row[0])
datapoints.append(row[-1])

View File

@@ -273,6 +273,7 @@ class FIFOSlots:
else:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or []
serial_nos = self.uppercase_serial_nos(serial_nos)
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
@@ -289,6 +290,10 @@ class FIFOSlots:
return self.item_details
def uppercase_serial_nos(self, serial_nos):
"Convert serial nos to uppercase for uniformity."
return [sn.upper() for sn in serial_nos]
def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."

View File

@@ -543,6 +543,12 @@ class SubcontractingReceipt(SubcontractingController):
for row in self.items:
precision = row.precision("qty")
# if allow alternative item, ignore the validation as per BOM required qty
is_allow_alternative_item = frappe.db.get_value("BOM", row.bom, "allow_alternative_item")
if is_allow_alternative_item:
continue
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):

View File

@@ -199,7 +199,7 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
customer = contact_doc.get_link_for("Customer")
ignore_permissions = False
if is_website_user():
if is_website_user() and user != "Guest":
if not filters:
filters = {}

View File

@@ -2,14 +2,6 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Rename Tool", {
onload: function (frm) {
return frappe.call({
method: "erpnext.utilities.doctype.rename_tool.rename_tool.get_doctypes",
callback: function (r) {
frm.set_df_property("select_doctype", "options", r.message);
},
});
},
refresh: function (frm) {
frm.disable_save();

View File

@@ -8,27 +8,13 @@
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Select DocType",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Link",
"label": "Select DocType",
"link_filters": "[[\"DocType\",\"allow_rename\",\"=\",1],[\"DocType\",\"module\",\"!=\",\"Core\"]]",
"options": "DocType"
},
{
"allow_on_submit": 0,
"bold": 0,
@@ -72,22 +58,18 @@
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 1,
"modified": "2015-10-19 03:04:49.097140",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"issingle": 1,
"links": [],
"max_attachments": 1,
"modified": "2025-12-09 14:18:33.838623",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
@@ -112,4 +94,4 @@
],
"read_only": 0,
"read_only_onload": 0
}
}

View File

@@ -7,6 +7,7 @@
import frappe
from frappe.model.document import Document
from frappe.model.rename_doc import bulk_rename
from frappe.utils.deprecations import deprecated
class RenameTool(Document):
@@ -19,13 +20,14 @@ class RenameTool(Document):
from frappe.types import DF
file_to_rename: DF.Attach | None
select_doctype: DF.Literal
select_doctype: DF.Link | None
# end: auto-generated types
pass
@frappe.whitelist()
@deprecated
def get_doctypes():
return frappe.db.sql_list(
"""select name from tabDocType