Merge pull request #53292 from frappe/version-16-hotfix

chore: release v16
This commit is contained in:
ruthra kumar
2026-03-10 20:19:47 +05:30
committed by GitHub
99 changed files with 3292 additions and 1991 deletions

View File

@@ -6,64 +6,83 @@
"Current Assets": {
"Accounts Receivable": {
"Debtors": {
"account_type": "Receivable"
"account_type": "Receivable",
"account_category": "Trade Receivables"
}
},
"Bank Accounts": {
"account_type": "Bank",
"is_group": 1
"is_group": 1,
"account_category": "Cash and Cash Equivalents"
},
"Cash In Hand": {
"Cash": {
"account_type": "Cash"
"account_type": "Cash",
"account_category": "Cash and Cash Equivalents"
},
"account_type": "Cash"
"account_type": "Cash",
"account_category": "Cash and Cash Equivalents"
},
"Loans and Advances (Assets)": {
"is_group": 1
"is_group": 1,
"account_category": "Other Receivables"
},
"Securities and Deposits": {
"Earnest Money": {}
"Earnest Money": {
"account_category": "Other Current Assets"
}
},
"Stock Assets": {
"Stock In Hand": {
"account_type": "Stock"
"account_type": "Stock",
"account_category": "Stock Assets"
},
"account_type": "Stock"
"account_type": "Stock",
"account_category": "Stock Assets"
},
"Tax Assets": {
"is_group": 1
"is_group": 1,
"account_category": "Other Current Assets"
}
},
"Fixed Assets": {
"Capital Equipment": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Electronic Equipment": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Furniture and Fixtures": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Office Equipment": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Plants and Machineries": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Buildings": {
"account_type": "Fixed Asset"
"account_type": "Fixed Asset",
"account_category": "Tangible Assets"
},
"Accumulated Depreciations": {
"account_type": "Accumulated Depreciation"
"account_type": "Accumulated Depreciation",
"account_category": "Tangible Assets"
}
},
"Investments": {
"is_group": 1
"is_group": 1,
"account_category": "Long-term Investments"
},
"Temporary Accounts": {
"Temporary Opening": {
"account_type": "Temporary"
"account_type": "Temporary",
"account_category": "Other Non-current Assets"
}
},
"root_type": "Asset"
@@ -72,55 +91,103 @@
"Direct Expenses": {
"Stock Expenses": {
"Cost of Goods Sold": {
"account_type": "Cost of Goods Sold"
"account_type": "Cost of Goods Sold",
"account_category": "Cost of Goods Sold"
},
"Expenses Included In Valuation": {
"account_type": "Expenses Included In Valuation"
"account_type": "Expenses Included In Valuation",
"account_category": "Other Direct Costs"
},
"Stock Adjustment": {
"account_type": "Stock Adjustment"
"account_type": "Stock Adjustment",
"account_category": "Other Direct Costs"
}
}
},
"Indirect Expenses": {
"Administrative Expenses": {},
"Commission on Sales": {},
"Administrative Expenses": {
"account_category": "Operating Expenses"
},
"Commission on Sales": {
"account_category": "Operating Expenses"
},
"Depreciation": {
"account_type": "Depreciation"
"account_type": "Depreciation",
"account_category": "Operating Expenses"
},
"Entertainment Expenses": {
"account_category": "Operating Expenses"
},
"Entertainment Expenses": {},
"Freight and Forwarding Charges": {
"account_type": "Chargeable"
"account_type": "Chargeable",
"account_category": "Operating Expenses"
},
"Legal Expenses": {
"account_category": "Operating Expenses"
},
"Marketing Expenses": {
"account_type": "Chargeable",
"account_category": "Operating Expenses"
},
"Miscellaneous Expenses": {
"account_type": "Chargeable",
"account_category": "Operating Expenses"
},
"Office Maintenance Expenses": {
"account_category": "Operating Expenses"
},
"Office Rent": {
"account_category": "Operating Expenses"
},
"Postal Expenses": {
"account_category": "Operating Expenses"
},
"Print and Stationery": {
"account_category": "Operating Expenses"
},
"Legal Expenses": {},
"Marketing Expenses": {},
"Miscellaneous Expenses": {},
"Office Maintenance Expenses": {},
"Office Rent": {},
"Postal Expenses": {},
"Print and Stationery": {},
"Rounded Off": {
"account_type": "Round Off"
"account_type": "Round Off",
"account_category": "Operating Expenses"
},
"Salary": {},
"Sales Expenses": {},
"Telephone Expenses": {},
"Travel Expenses": {},
"Utility Expenses": {},
"Write Off": {},
"Exchange Gain/Loss": {},
"Gain/Loss on Asset Disposal": {},
"Impairment": {}
"Salary": {
"account_category": "Operating Expenses"
},
"Sales Expenses": {
"account_category": "Operating Expenses"
},
"Telephone Expenses": {
"account_category": "Operating Expenses"
},
"Travel Expenses": {
"account_category": "Operating Expenses"
},
"Utility Expenses": {
"account_category": "Operating Expenses"
},
"Write Off": {
"account_category": "Operating Expenses"
},
"Exchange Gain/Loss": {
"account_category": "Operating Expenses"
},
"Gain/Loss on Asset Disposal": {
"account_category": "Other Operating Income"
},
"Impairment": {
"account_category": "Operating Expenses"
}
},
"root_type": "Expense"
},
"Income": {
"Direct Income": {
"Sales": {
"account_type": "Income Account"
"account_type": "Income Account",
"account_category": "Revenue from Operations"
},
"Service": {
"account_type": "Income Account"
"account_type": "Income Account",
"account_category": "Revenue from Operations"
},
"account_type": "Income Account"
},
@@ -132,31 +199,51 @@
},
"Source of Funds (Liabilities)": {
"Capital Account": {
"Reserves and Surplus": {},
"Shareholders Funds": {},
"Revaluation Surplus": {}
"Reserves and Surplus": {
"account_category": "Reserves and Surplus"
},
"Shareholders Funds": {
"account_category": "Share Capital"
},
"Revaluation Surplus": {
"account_category": "Reserves and Surplus"
}
},
"Current Liabilities": {
"Accounts Payable": {
"Creditors": {
"account_type": "Payable"
"account_type": "Payable",
"account_category": "Trade Payables"
},
"Payroll Payable": {}
"Payroll Payable": {
"account_category": "Other Payables"
}
},
"Stock Liabilities": {
"Stock Received But Not Billed": {
"account_type": "Stock Received But Not Billed"
"account_type": "Stock Received But Not Billed",
"account_category": "Trade Payables"
}
},
"Duties and Taxes": {
"TDS": {
"account_type": "Tax"
}
"account_type": "Tax",
"account_category": "Current Tax Liabilities"
},
"account_type": "Tax",
"is_group": 1,
"account_category": "Current Tax Liabilities"
},
"Loans (Liabilities)": {
"Secured Loans": {},
"Unsecured Loans": {},
"Bank Overdraft Account": {}
"Secured Loans": {
"account_category": "Long-term Borrowings"
},
"Unsecured Loans": {
"account_category": "Long-term Borrowings"
},
"Bank Overdraft Account": {
"account_category": "Short-term Borrowings"
}
}
},
"root_type": "Liability"

View File

@@ -20,7 +20,6 @@
{
"fieldname": "period_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Period Name",
"reqd": 1,
"unique": 1
@@ -79,7 +78,7 @@
}
],
"links": [],
"modified": "2025-12-01 16:53:44.631299",
"modified": "2026-03-09 17:15:33.577217",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",

View File

@@ -97,7 +97,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.is_existing_asset:
if doc.asset_type == "Existing Asset":
return
else:
date = doc.available_for_use_date

View File

@@ -205,7 +205,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order"
"label": "Automatically Fetch Payment Terms from Order/Quotation"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -697,7 +697,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-04 17:15:38.609327",
"modified": "2026-02-27 01:04:09.415288",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -140,6 +140,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
@@ -181,7 +182,6 @@
"fieldname": "cheque_no",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Reference Number",
"mandatory_depends_on": "eval:doc.voucher_type == \"Bank Entry\"",
"no_copy": 1,
@@ -665,7 +665,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-02-16 16:06:10.468482",
"modified": "2026-03-09 17:15:26.569327",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -138,6 +138,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Posting Date",
"reqd": 1
},
@@ -160,7 +161,6 @@
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Mode of Payment",
"options": "Mode of Payment"
},
@@ -228,6 +228,7 @@
"fieldname": "paid_from",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Account Paid From",
"options": "Account",
"print_hide": 1,
@@ -252,6 +253,7 @@
"fieldname": "paid_to",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Account Paid To",
"options": "Account",
"print_hide": 1,
@@ -414,6 +416,7 @@
"depends_on": "eval:(doc.paid_from && doc.paid_to)",
"fieldname": "reference_no",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Cheque/Reference No",
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
},
@@ -792,7 +795,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-02-03 16:08:49.800381",
"modified": "2026-03-09 17:15:30.453920",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -534,7 +534,7 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
cur_frm.set_query("wip_composite_asset", "items", function () {
return {
filters: { is_composite_asset: 1, docstatus: 0 },
filters: { asset_type: "Composite Asset", docstatus: 0 },
};
});

View File

@@ -266,6 +266,7 @@
{
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date",
"oldfieldname": "due_date",
"oldfieldtype": "Date"
@@ -319,6 +320,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
@@ -397,6 +399,8 @@
{
"fieldname": "bill_no",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Supplier Invoice No",
"oldfieldname": "bill_no",
"oldfieldtype": "Data",
@@ -1689,7 +1693,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-02-23 14:23:57.269770",
"modified": "2026-03-09 17:15:27.014131",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -733,9 +733,10 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if item.purchase_receipt:
frappe.throw(
_("Stock cannot be updated against Purchase Receipt {0}").format(
item.purchase_receipt
)
_(
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
).format(self.name, item.purchase_receipt),
title=_("Stock Update Not Allowed"),
)
def validate_for_repost(self):

View File

@@ -381,6 +381,8 @@
"fieldtype": "Date",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"no_copy": 1,
"oldfieldname": "posting_date",
@@ -415,6 +417,7 @@
"fieldtype": "Date",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Payment Due Date",
"no_copy": 1,
"oldfieldname": "due_date",
@@ -1639,6 +1642,7 @@
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"length": 30,
@@ -2330,7 +2334,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-02-28 17:58:56.453076",
"modified": "2026-03-09 17:15:30.931929",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1451,6 +1451,9 @@ class SalesInvoice(SellingController):
return asset_qty_map
def process_asset_depreciation(self):
if self.is_internal_transfer():
return
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:

View File

@@ -8,6 +8,8 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_default_address
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
from frappe.utils import cstr
from frappe.utils.nestedset import get_root_of
@@ -83,6 +85,8 @@ class TaxRule(Document):
frappe.throw(_("Tax Template is mandatory."))
def validate_filters(self):
TaxRule = DocType("Tax Rule")
filters = {
"tax_type": self.tax_type,
"customer": self.customer,
@@ -105,33 +109,34 @@ class TaxRule(Document):
"company": self.company,
}
conds = ""
for d in filters:
if conds:
conds += " and "
conds += f"""ifnull({d}, '') = {frappe.db.escape(cstr(filters[d]))}"""
if self.from_date and self.to_date:
conds += f""" and ((from_date > '{self.from_date}' and from_date < '{self.to_date}') or
(to_date > '{self.from_date}' and to_date < '{self.to_date}') or
('{self.from_date}' > from_date and '{self.from_date}' < to_date) or
('{self.from_date}' = from_date and '{self.to_date}' = to_date))"""
elif self.from_date and not self.to_date:
conds += f""" and to_date > '{self.from_date}'"""
elif self.to_date and not self.from_date:
conds += f""" and from_date < '{self.to_date}'"""
tax_rule = frappe.db.sql(
f"select name, priority \
from `tabTax Rule` where {conds} and name != '{self.name}'",
as_dict=1,
query = (
frappe.qb.from_(TaxRule).select(TaxRule.name, TaxRule.priority).where(TaxRule.name != self.name)
)
if tax_rule:
if tax_rule[0].priority == self.priority:
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
for field, value in filters.items():
query = query.where(IfNull(TaxRule[field], "") == cstr(value))
if self.from_date and self.to_date:
query = query.where(
((TaxRule.from_date > self.from_date) & (TaxRule.from_date < self.to_date))
| ((TaxRule.to_date > self.from_date) & (TaxRule.to_date < self.to_date))
| ((self.from_date > TaxRule.from_date) & (self.from_date < TaxRule.to_date))
| ((TaxRule.from_date == self.from_date) & (TaxRule.to_date == self.to_date))
)
elif self.from_date:
query = query.where(TaxRule.to_date > self.from_date)
elif self.to_date:
query = query.where(TaxRule.from_date < self.to_date)
tax_rule = query.run(as_dict=True)
if tax_rule and tax_rule[0].priority == self.priority:
frappe.throw(
_("Tax Rule Conflicts with {0}").format(tax_rule[0].name),
ConflictingTaxRule,
)
@frappe.whitelist()

View File

@@ -37,6 +37,20 @@ function get_filters() {
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Link",
options: "Party Type",
width: 100,
},
{
fieldname: "party",
label: __("Party"),
fieldtype: "Dynamic Link",
options: "party_type",
width: 100,
},
{
fieldname: "voucher_no",
label: __("Voucher No"),

View File

@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(gle.party.eq(self.filters.party))
if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else:
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(ple.party.eq(self.filters.party))
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(

View File

@@ -649,7 +649,7 @@ class GrossProfitGenerator:
new_row = row
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
else:
new_row.qty += flt(row.qty)
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
new_row = self.set_average_rate(new_row)
@@ -659,11 +659,17 @@ class GrossProfitGenerator:
if i == 0:
new_row = row
else:
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
new_row.buying_amount = flt(
(new_row.buying_amount + row.buying_amount), self.currency_precision
)
new_row.base_amount = flt(
(new_row.base_amount + row.base_amount), self.currency_precision
)
if self.filters.get("group_by") == "Sales Person":
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
new_row.allocated_amount = flt(
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)

View File

@@ -6,11 +6,11 @@
"docstatus": 0,
"doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"is_existing_asset\":0}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-10-28 23:16:16.939070",
"modified": "2026-02-03 15:48:13.407835",
"modified_by": "Administrator",
"module": "Assets",
"name": "Category-wise Asset Value",

View File

@@ -6,11 +6,11 @@
"docstatus": 0,
"doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"is_existing_asset\":0}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-10-28 23:16:07.883312",
"modified": "2026-02-03 15:48:13.407835",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location-wise Asset Value",

View File

@@ -100,7 +100,7 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
"company": company,
"status": "In Location",
"group_by": "Asset Category",
"is_existing_asset": 0,
"asset_type": ["!=", "Existing Asset"],
}
),
"type": "Donut",
@@ -126,7 +126,12 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
"x_field": "location",
"timeseries": 0,
"filters_json": json.dumps(
{"company": company, "status": "In Location", "group_by": "Location", "is_existing_asset": 0}
{
"company": company,
"status": "In Location",
"group_by": "Location",
"asset_type": ["!=", "Existing Asset"],
}
),
"type": "Donut",
"doctype": "Dashboard Chart",

View File

@@ -81,23 +81,79 @@ frappe.ui.form.on("Asset", {
},
before_submit: function (frm) {
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) {
if (frm.doc.asset_type == "Composite Asset" && !frm.has_active_capitalization) {
frappe.throw(__("Please capitalize this asset before submitting."));
}
},
refresh: function (frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
refresh: async function (frm) {
frappe.ui.form.trigger("Asset", "asset_type");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
let has_create_buttons = false;
if (frm.doc.docstatus == 1) {
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Asset Value Adjustment"),
function () {
frm.trigger("create_asset_value_adjustment");
},
__("Create")
);
frm.add_custom_button(
__("Asset Repair"),
function () {
frm.trigger("create_asset_repair");
},
__("Create")
);
has_create_buttons = true;
}
if (
!frm.doc.calculate_depreciation &&
["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)
) {
frm.add_custom_button(
__("Depreciation Entry"),
function () {
frm.trigger("make_journal_entry");
},
__("Create")
);
has_create_buttons = true;
}
if (has_create_buttons) {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(
__("Maintain Asset"),
function () {
frm.trigger("create_asset_maintenance");
},
__("Actions")
);
}
frm.add_custom_button(
__("Split Asset"),
function () {
frm.trigger("split_asset");
},
__("Actions")
);
frm.add_custom_button(
__("Transfer Asset"),
function () {
erpnext.asset.transfer_asset(frm);
},
__("Manage")
__("Actions")
);
frm.add_custom_button(
@@ -105,7 +161,7 @@ frappe.ui.form.on("Asset", {
function () {
erpnext.asset.scrap_asset(frm);
},
__("Manage")
__("Actions")
);
frm.add_custom_button(
@@ -113,15 +169,7 @@ frappe.ui.form.on("Asset", {
function () {
frm.trigger("sell_asset");
},
__("Manage")
);
frm.add_custom_button(
__("Split Asset"),
function () {
frm.trigger("split_asset");
},
__("Manage")
__("Actions")
);
} else if (frm.doc.status == "Scrapped") {
frm.add_custom_button(__("Restore Asset"), function () {
@@ -129,47 +177,9 @@ frappe.ui.form.on("Asset", {
}).addClass("btn-primary");
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
if (await frm.events.should_show_accounting_ledger(frm)) {
frm.add_custom_button(
__("Maintain Asset"),
function () {
frm.trigger("create_asset_maintenance");
},
__("Manage")
);
}
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Adjust Asset Value"),
function () {
frm.trigger("create_asset_value_adjustment");
},
__("Manage")
);
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(
__("Create Depreciation Entry"),
function () {
frm.trigger("make_journal_entry");
},
__("Manage")
);
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button(
__("View General Ledger"),
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
@@ -179,7 +189,7 @@ frappe.ui.form.on("Asset", {
};
frappe.set_route("query-report", "General Ledger");
},
__("Manage")
__("View")
);
}
@@ -195,7 +205,7 @@ frappe.ui.form.on("Asset", {
if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.is_composite_asset) {
if (frm.doc.asset_type == "Composite Asset") {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
args: {
@@ -217,6 +227,28 @@ frappe.ui.form.on("Asset", {
}
},
should_show_accounting_ledger: async function (frm) {
if (["Capitalized"].includes(frm.doc.status)) {
return false;
}
if (
!frm.doc.purchase_receipt &&
!frm.doc.purchase_invoice &&
["Existing Asset", "Composite Component"].includes(frm.doc.asset_type)
) {
return false;
}
const asset_category = await frappe.db.get_value(
"Asset Category",
frm.doc.asset_category,
"enable_cwip_accounting"
);
return !!asset_category.message?.enable_cwip_accounting;
},
set_depr_posting_failure_alert: function (frm) {
const alert = `
<div class="row">
@@ -232,7 +264,8 @@ frappe.ui.form.on("Asset", {
toggle_reference_doc: function (frm) {
const is_submitted = frm.doc.docstatus === 1;
const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset;
const is_special_asset =
frm.doc.asset_type == "Existing Asset" || frm.doc.asset_type == "Composite Asset";
const clear_field = (field) => {
if (frm.doc[field]) {
@@ -508,18 +541,13 @@ frappe.ui.form.on("Asset", {
});
},
is_existing_asset: function (frm) {
frm.trigger("toggle_reference_doc");
},
is_composite_asset: function (frm) {
asset_type: function (frm) {
if (frm.doc.docstatus == 0) {
if (frm.doc.is_composite_asset) {
if (frm.doc.asset_type == "Composite Asset") {
frm.set_value("net_purchase_amount", 0);
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
}
frm.trigger("toggle_reference_doc");
}
},

View File

@@ -9,20 +9,17 @@
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"item_code",
"item_name",
"asset_name",
"asset_category",
"location",
"image",
"column_break_3",
"status",
"company",
"asset_owner",
"asset_owner_company",
"is_existing_asset",
"is_composite_asset",
"is_composite_component",
"location",
"asset_category",
"asset_type",
"maintenance_required",
"calculate_depreciation",
"purchase_details_section",
"purchase_receipt",
"purchase_receipt_item",
@@ -30,31 +27,44 @@
"purchase_invoice_item",
"purchase_date",
"available_for_use_date",
"disposal_date",
"column_break_23",
"net_purchase_amount",
"purchase_amount",
"asset_quantity",
"additional_asset_cost",
"section_break_uiyd",
"column_break_bbwr",
"column_break_bfkm",
"total_asset_cost",
"disposal_date",
"depreciation_tab",
"calculate_depreciation",
"column_break_33",
"column_break_wqzi",
"opening_accumulated_depreciation",
"opening_number_of_booked_depreciations",
"is_fully_depreciated",
"column_break_33",
"opening_number_of_booked_depreciations",
"section_break_36",
"finance_books",
"section_break_33",
"depreciation_method",
"value_after_depreciation",
"total_number_of_depreciations",
"column_break_24",
"frequency_of_depreciation",
"column_break_24",
"next_depreciation_date",
"total_number_of_depreciations",
"depreciation_schedule_sb",
"depreciation_schedule_view",
"insurance_details_tab",
"other_info_tab",
"accounting_dimensions_section",
"cost_center",
"column_break_rjyw",
"asset_owner_section",
"asset_owner",
"column_break_yeds",
"asset_owner_company",
"customer",
"supplier",
"insurance_section",
"policy_number",
"insurer",
"insured_value",
@@ -62,22 +72,17 @@
"insurance_start_date",
"insurance_end_date",
"comprehensive_insurance",
"other_info_tab",
"accounting_dimensions_section",
"cost_center",
"section_break_jtou",
"status",
"custodian",
"department",
"default_finance_book",
"depr_entry_posting_status",
"booked_fixed_asset",
"customer",
"supplier",
"column_break_51",
"department",
"split_from",
"journal_entry_for_scrap",
"split_from",
"amended_from",
"maintenance_required",
"booked_fixed_asset",
"connections_tab"
],
"fields": [
@@ -106,13 +111,6 @@
"options": "Item",
"reqd": 1
},
{
"depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{
"depends_on": "item_code",
"fetch_from": "item_code.asset_category",
@@ -171,6 +169,8 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@@ -207,7 +207,7 @@
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Purchase Date",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"read_only_depends_on": "eval:doc.asset_type != \"Existing Asset\" && doc.asset_type != \"Composite Asset\"",
"reqd": 1
},
{
@@ -229,25 +229,18 @@
{
"fieldname": "available_for_use_date",
"fieldtype": "Date",
"label": "Available-for-use Date",
"mandatory_depends_on": "eval:(!(doc.is_composite_component || doc.is_composite_asset) || doc.docstatus==1)"
"label": "Available for Use Date",
"mandatory_depends_on": "eval:(!(doc.asset_type == \"Composite Component\" || doc.asset_type == \"Composite Asset\") || doc.docstatus==1)"
},
{
"default": "0",
"fieldname": "calculate_depreciation",
"fieldtype": "Check",
"label": "Calculate Depreciation",
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component"
"read_only_depends_on": "eval:(doc.asset_type == \"Composite Asset\" && !doc.net_purchase_amount) || doc.asset_type == \"Composite Component\""
},
{
"default": "0",
"depends_on": "eval:(!doc.is_composite_asset && !doc.is_composite_component)",
"fieldname": "is_existing_asset",
"fieldtype": "Check",
"label": "Is Existing Asset"
},
{
"depends_on": "eval:(doc.is_existing_asset)",
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"label": "Opening Accumulated Depreciation",
@@ -257,18 +250,20 @@
"columns": 10,
"fieldname": "finance_books",
"fieldtype": "Table",
"label": "Finance Books",
"options": "Asset Finance Book"
},
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
"hidden": 1
"hidden": 1,
"label": "Depreciation Details"
},
{
"fieldname": "depreciation_method",
"fieldtype": "Select",
"label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nManual"
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual"
},
{
"fieldname": "value_after_depreciation",
@@ -295,6 +290,7 @@
{
"fieldname": "next_depreciation_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Next Depreciation Date",
"no_copy": 1
},
@@ -364,7 +360,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
"fieldname": "purchase_receipt",
"fieldtype": "Link",
"label": "Purchase Receipt",
@@ -373,7 +369,7 @@
"print_hide": 1
},
{
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
@@ -399,7 +395,7 @@
"read_only": 1
},
{
"collapsible_depends_on": "is_existing_asset",
"collapsible_depends_on": "eval:doc.asset_type == \"Existing Asset\"",
"fieldname": "purchase_details_section",
"fieldtype": "Section Break",
"label": "Purchase Details"
@@ -413,10 +409,9 @@
"fieldtype": "Column Break"
},
{
"depends_on": "calculate_depreciation",
"depends_on": "eval: doc.calculate_depreciation",
"fieldname": "section_break_36",
"fieldtype": "Section Break",
"label": "Finance Books"
"fieldtype": "Section Break"
},
{
"fieldname": "split_from",
@@ -455,18 +450,11 @@
},
{
"default": "0",
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "is_fully_depreciated",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Fully Depreciated"
},
{
"default": "0",
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_component)",
"fieldname": "is_composite_asset",
"fieldtype": "Check",
"label": "Is Composite Asset"
},
{
"depends_on": "eval:doc.docstatus > 0",
"fieldname": "total_asset_cost",
@@ -496,7 +484,7 @@
"read_only": 1
},
{
"depends_on": "eval:(doc.is_existing_asset)",
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
"fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Opening Number of Booked Depreciations"
@@ -513,15 +501,10 @@
"hidden": 1,
"label": "Purchase Invoice Item"
},
{
"fieldname": "insurance_details_tab",
"fieldtype": "Tab Break",
"label": "Insurance"
},
{
"fieldname": "other_info_tab",
"fieldtype": "Tab Break",
"label": "Other Info"
"label": "More Info"
},
{
"fieldname": "connections_tab",
@@ -530,6 +513,7 @@
"show_dashboard": 1
},
{
"depends_on": "eval: doc.calculate_depreciation || doc.asset_type == \"Existing Asset\"",
"fieldname": "depreciation_tab",
"fieldtype": "Tab Break",
"label": "Depreciation"
@@ -544,20 +528,61 @@
"fieldtype": "Section Break",
"label": "Additional Info"
},
{
"default": "0",
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_asset)",
"fieldname": "is_composite_component",
"fieldtype": "Check",
"label": "Is Composite Component"
},
{
"fieldname": "net_purchase_amount",
"fieldtype": "Currency",
"label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"mandatory_depends_on": "eval:(doc.asset_type != \"Composite Asset\" || doc.docstatus==1)",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.is_composite_asset"
"read_only_depends_on": "eval: doc.asset_type == \"Composite Asset\""
},
{
"fieldname": "asset_type",
"fieldtype": "Select",
"label": "Asset Type",
"options": "\nExisting Asset\nComposite Asset\nComposite Component"
},
{
"fieldname": "column_break_wqzi",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_rjyw",
"fieldtype": "Column Break"
},
{
"fieldname": "insurance_section",
"fieldtype": "Section Break",
"label": "Insurance"
},
{
"fieldname": "section_break_uiyd",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_bbwr",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_bfkm",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "Item Name"
},
{
"fieldname": "asset_owner_section",
"fieldtype": "Section Break",
"label": "Ownership"
},
{
"fieldname": "column_break_yeds",
"fieldtype": "Column Break"
}
],
"idx": 72,
@@ -601,7 +626,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-12-18 16:36:40.904246",
"modified": "2026-03-09 17:15:32.819896",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -56,6 +56,7 @@ class Asset(AccountsController):
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None
asset_quantity: DF.Int
asset_type: DF.Literal["", "Existing Asset", "Composite Asset", "Composite Component"]
available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check
@@ -67,7 +68,9 @@ class Asset(AccountsController):
default_finance_book: DF.Link | None
department: DF.Link | None
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"]
depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
]
disposal_date: DF.Date | None
finance_books: DF.Table[AssetFinanceBook]
frequency_of_depreciation: DF.Int
@@ -76,9 +79,6 @@ class Asset(AccountsController):
insurance_start_date: DF.Date | None
insured_value: DF.Data | None
insurer: DF.Data | None
is_composite_asset: DF.Check
is_composite_component: DF.Check
is_existing_asset: DF.Check
is_fully_depreciated: DF.Check
item_code: DF.Link
item_name: DF.ReadOnly | None
@@ -243,7 +243,7 @@ class Asset(AccountsController):
self.set_total_booked_depreciations()
def before_submit(self):
if self.is_composite_asset and not has_active_capitalization(self.name):
if self.asset_type == "Composite Asset" and not has_active_capitalization(self.name):
if self.split_from and has_active_capitalization(self.split_from):
return
frappe.throw(_("Please capitalize this asset before submitting."))
@@ -252,7 +252,11 @@ class Asset(AccountsController):
self.validate_in_use_date()
self.make_asset_movement()
self.reload()
if not self.booked_fixed_asset and not self.is_composite_component and self.validate_make_gl_entry():
if (
not self.booked_fixed_asset
and self.asset_type != "Composite Component"
and self.validate_make_gl_entry()
):
self.make_gl_entries()
if self.calculate_depreciation and not self.split_from:
convert_draft_asset_depr_schedules_into_active(self)
@@ -267,7 +271,7 @@ class Asset(AccountsController):
cancel_asset_depr_schedules(self)
self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
if not self.is_composite_component:
if self.asset_type != "Composite Component":
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
self.db_set("booked_fixed_asset", 0)
add_asset_activity(self.name, _("Asset cancelled"))
@@ -285,7 +289,7 @@ class Asset(AccountsController):
add_asset_activity(self.name, _("Asset deleted"))
def set_purchase_doc_row_item(self):
if self.is_existing_asset or self.is_composite_asset:
if self.asset_type == "Existing Asset" or self.asset_type == "Composite Asset":
return
self.purchase_amount = self.net_purchase_amount
@@ -328,7 +332,7 @@ class Asset(AccountsController):
)
)
if self.is_existing_asset and self.purchase_invoice:
if self.asset_type == "Existing Asset" and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def validate_item(self):
@@ -374,7 +378,7 @@ class Asset(AccountsController):
)
def validate_in_use_date(self):
if not self.available_for_use_date and not self.is_composite_component:
if not self.available_for_use_date and self.asset_type != "Composite Component":
frappe.throw(_("Available for use date is required"))
for d in self.finance_books:
@@ -442,13 +446,13 @@ class Asset(AccountsController):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if not flt(self.net_purchase_amount) and not self.is_composite_asset:
if not flt(self.net_purchase_amount) and self.asset_type != "Composite Asset":
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
if (
not self.is_existing_asset
and not self.is_composite_asset
not self.asset_type == "Existing Asset"
and not self.asset_type == "Composite Asset"
and not self.purchase_receipt
and not self.purchase_invoice
):
@@ -477,7 +481,7 @@ class Asset(AccountsController):
if self.is_fully_depreciated:
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
if self.is_existing_asset:
if self.asset_type == "Existing Asset":
return
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
@@ -549,7 +553,7 @@ class Asset(AccountsController):
)
def validate_gross_and_purchase_amount(self):
if self.is_existing_asset:
if self.asset_type == "Existing Asset":
return
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
@@ -617,7 +621,7 @@ class Asset(AccountsController):
self.validate_depreciation_start_date(row)
self.validate_total_number_of_depreciations_and_frequency(row)
if not self.is_existing_asset:
if self.asset_type != "Existing Asset":
self.opening_accumulated_depreciation = 0
self.opening_number_of_booked_depreciations = 0
else:
@@ -770,7 +774,7 @@ class Asset(AccountsController):
def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0:
if self.is_composite_asset:
if self.asset_type == "Composite Asset":
status = "Work In Progress"
else:
status = "Draft"
@@ -843,7 +847,7 @@ class Asset(AccountsController):
return records
def validate_make_gl_entry(self):
if self.is_composite_asset:
if self.asset_type == "Composite Asset":
return True
purchase_document = self.get_purchase_document()
@@ -924,7 +928,7 @@ class Asset(AccountsController):
purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
if (self.asset_type == "Composite Asset" or (purchase_document and self.purchase_amount)) and getdate(
self.available_for_use_date
) <= getdate():
gl_entries.append(
@@ -964,7 +968,7 @@ class Asset(AccountsController):
self.db_set("booked_fixed_asset", 1)
def check_asset_capitalization_gl_entries(self):
if self.is_composite_asset:
if self.asset_type == "Composite Asset":
result = frappe.db.get_value(
"Asset Capitalization",
{"target_asset": self.name, "docstatus": 1},
@@ -1395,7 +1399,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
asset_doc.purchase_amount = existing_asset.net_purchase_amount
asset_doc.purchase_amount = existing_asset.net_purchase_amount * scaling_factor
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
asset_doc.opening_accumulated_depreciation = (

View File

@@ -786,10 +786,14 @@ def get_disposal_account_and_cost_center(company):
@frappe.whitelist()
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
def get_value_after_depreciation_on_disposal_date(
asset: str,
disposal_date: str,
finance_book: str | None = None,
) -> float:
asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.is_composite_component:
if asset_doc.asset_type == "Composite Component":
validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase")
return flt(asset_doc.value_after_depreciation)

View File

@@ -71,16 +71,16 @@ class TestAsset(AssetSetup):
self.assertRaises(frappe.MandatoryError, asset.save)
def test_pr_or_pi_mandatory_if_not_existing_asset(self):
"""Tests if either PI or PR is present if CWIP is enabled and is_existing_asset=0."""
"""Tests if either PI or PR is present if CWIP is enabled and asset_type == Existing Asset."""
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.is_existing_asset = 0
asset.asset_type = ""
self.assertRaises(frappe.ValidationError, asset.save)
def test_available_for_use_date_is_after_purchase_date(self):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1)
asset.is_existing_asset = 0
asset.asset_type = ""
asset.purchase_date = getdate("2021-10-10")
asset.available_for_use_date = getdate("2021-10-1")
@@ -183,7 +183,7 @@ class TestAsset(AssetSetup):
asset.submit()
def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset=1)
asset = create_asset(asset_type="Existing Asset")
doc = frappe.new_doc("Purchase Invoice")
doc.company = "_Test Company"
doc.supplier = "_Test Supplier"
@@ -710,7 +710,7 @@ class TestAsset(AssetSetup):
# create an asset
asset = create_asset(
item_code="Macbook Pro",
is_existing_asset=1,
asset_type="Existing Asset",
calculate_depreciation=1,
available_for_use_date=purchase_date,
purchase_date=purchase_date,
@@ -890,7 +890,7 @@ class TestDepreciationMethods(AssetSetup):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2030-06-06",
is_existing_asset=1,
asset_type="Existing Asset",
opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=47178.08,
expected_value_after_useful_life=10000,
@@ -939,7 +939,7 @@ class TestDepreciationMethods(AssetSetup):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2030-01-01",
is_existing_asset=1,
asset_type="Existing Asset",
depreciation_method="Double Declining Balance",
opening_number_of_booked_depreciations=1,
opening_accumulated_depreciation=50000,
@@ -1680,7 +1680,7 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
def test_asset_cost_center(self):
asset = create_asset(is_existing_asset=1, do_not_save=1)
asset = create_asset(asset_type="Existing Asset", do_not_save=1)
asset.cost_center = "Main - WP"
self.assertRaises(frappe.ValidationError, asset.submit)
@@ -1717,7 +1717,7 @@ class TestDepreciationBasics(AssetSetup):
def test_manual_depreciation_for_existing_asset(self):
asset = create_asset(
item_code="Macbook Pro",
is_existing_asset=1,
asset_type="Existing Asset",
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
submit=1,
@@ -1843,7 +1843,7 @@ class TestDepreciationBasics(AssetSetup):
# Create composite asset
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset for Split",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
asset_quantity=2, # Set quantity > 1 to allow splitting
@@ -1937,9 +1937,7 @@ def create_asset(**args):
"available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
"is_composite_asset": args.is_composite_asset or 0,
"is_composite_component": args.is_composite_component or 0,
"asset_type": args.asset_type or "Existing Asset",
"asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
}
@@ -1961,7 +1959,7 @@ def create_asset(**args):
},
)
if asset.is_composite_asset:
if asset.asset_type == "Composite Asset":
asset.net_purchase_amount = 0
asset.purchase_amount = 0

View File

@@ -16,11 +16,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
refresh() {
this.show_general_ledger();
erpnext.toggle_serial_batch_fields(this.frm);
if (
(this.frm.doc.stock_items && this.frm.doc.stock_items.length) ||
!this.frm.doc.target_is_fixed_asset
) {
if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
this.show_stock_ledger();
}
@@ -41,7 +39,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.set_query("target_asset", function () {
return {
filters: { is_composite_asset: 1, docstatus: 0 },
filters: { asset_type: "Composite Asset", docstatus: 0 },
};
});
@@ -240,10 +238,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
this.calculate_totals();
}
target_qty() {
this.calculate_totals();
}
rate() {
this.calculate_totals();
}
@@ -485,10 +479,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value"));
me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision("target_qty"));
me.frm.doc.target_incoming_rate = me.frm.doc.target_qty
? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
: me.frm.doc.total_value;
me.frm.doc.target_incoming_rate = me.frm.doc.total_value;
me.frm.refresh_fields();
}

View File

@@ -9,30 +9,33 @@
"field_order": [
"title",
"naming_series",
"company",
"target_asset",
"target_asset_name",
"target_item_code",
"finance_book",
"target_qty",
"column_break_9",
"company",
"finance_book",
"posting_date",
"posting_time",
"set_posting_time",
"target_batch_no",
"target_serial_no",
"target_item_code",
"amended_from",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"section_break_16",
"stock_items",
"section_break_urtz",
"column_break_gqep",
"column_break_yvlx",
"stock_items_total",
"section_break_26",
"asset_items",
"section_break_arbh",
"column_break_boeu",
"column_break_qecy",
"asset_items_total",
"service_expenses_section",
"service_items",
"section_break_ptna",
"column_break_szvh",
"column_break_katv",
"service_items_total",
"totals_section",
"total_value",
@@ -55,20 +58,12 @@
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
"fieldname": "target_item_code",
"fieldtype": "Link",
"hidden": 1,
"in_standard_filter": 1,
"label": "Target Item Code",
"options": "Item",
"read_only": 1
},
{
"default": "0",
"fetch_from": "target_item_code.is_fixed_asset",
"fieldname": "target_is_fixed_asset",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Is Fixed Asset",
"read_only": 1
},
{
"fieldname": "target_asset",
"fieldtype": "Link",
@@ -143,6 +138,7 @@
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Consumed Stock Items"
},
{
@@ -151,49 +147,11 @@
"label": "Stock Items",
"options": "Asset Capitalization Stock Item"
},
{
"depends_on": "target_has_batch_no",
"fieldname": "target_batch_no",
"fieldtype": "Link",
"label": "Target Batch No",
"options": "Batch"
},
{
"default": "1",
"fieldname": "target_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Target Qty",
"read_only": 1
},
{
"default": "0",
"fetch_from": "target_item_code.has_batch_no",
"fieldname": "target_has_batch_no",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Has Batch No",
"read_only": 1
},
{
"default": "0",
"fetch_from": "target_item_code.has_serial_no",
"fieldname": "target_has_serial_no",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Has Serial No",
"read_only": 1
},
{
"depends_on": "target_has_serial_no",
"fieldname": "target_serial_no",
"fieldtype": "Small Text",
"label": "Target Serial No"
},
{
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
"fieldname": "section_break_26",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Consumed Assets"
},
{
@@ -203,6 +161,7 @@
"options": "Asset Capitalization Asset Item"
},
{
"depends_on": "eval: doc.stock_items_total",
"fieldname": "stock_items_total",
"fieldtype": "Currency",
"label": "Consumed Stock Total Value",
@@ -210,6 +169,7 @@
"read_only": 1
},
{
"depends_on": "eval: doc.asset_items_total",
"fieldname": "asset_items_total",
"fieldtype": "Currency",
"label": "Consumed Asset Total Value",
@@ -226,6 +186,7 @@
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
"fieldname": "service_expenses_section",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Service Expenses"
},
{
@@ -235,6 +196,7 @@
"options": "Asset Capitalization Service Item"
},
{
"depends_on": "eval: doc.service_items_total",
"fieldname": "service_items_total",
"fieldtype": "Currency",
"label": "Service Expense Total Amount",
@@ -277,10 +239,10 @@
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
@@ -292,12 +254,48 @@
"label": "Target Fixed Asset Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "section_break_urtz",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_gqep",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_yvlx",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_arbh",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_boeu",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_qecy",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ptna",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_szvh",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_katv",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-05-20 15:15:12.110035",
"modified": "2026-02-06 01:52:41.890713",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",

View File

@@ -39,9 +39,6 @@ force_fields = [
"target_asset_name",
"item_name",
"asset_name",
"target_is_fixed_asset",
"target_has_serial_no",
"target_has_batch_no",
"stock_uom",
"fixed_asset_account",
"valuation_rate",
@@ -76,6 +73,7 @@ class AssetCapitalization(StockController):
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
posting_date: DF.Date
posting_time: DF.Time
project: DF.Link | None
service_items: DF.Table[AssetCapitalizationServiceItem]
service_items_total: DF.Currency
set_posting_time: DF.Check
@@ -83,15 +81,9 @@ class AssetCapitalization(StockController):
stock_items_total: DF.Currency
target_asset: DF.Link | None
target_asset_name: DF.Data | None
target_batch_no: DF.Link | None
target_fixed_asset_account: DF.Link | None
target_has_batch_no: DF.Check
target_has_serial_no: DF.Check
target_incoming_rate: DF.Currency
target_is_fixed_asset: DF.Check
target_item_code: DF.Link | None
target_qty: DF.Float
target_serial_no: DF.SmallText | None
title: DF.Data | None
total_value: DF.Currency
# end: auto-generated types
@@ -190,22 +182,13 @@ class AssetCapitalization(StockController):
if not target_item.is_fixed_asset:
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
if target_item.is_fixed_asset:
self.target_qty = 1
if flt(self.target_qty) <= 0:
frappe.throw(_("Target Qty must be a positive number"))
if not target_item.has_batch_no:
self.target_batch_no = None
if not target_item.has_serial_no:
self.target_serial_no = ""
self.validate_item(target_item)
def validate_target_asset(self):
if self.target_asset:
target_asset = self.get_asset_for_validation(self.target_asset)
if not target_asset.is_composite_asset:
if not target_asset.asset_type == "Composite Asset":
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
if target_asset.item_code != self.target_item_code:
@@ -314,7 +297,7 @@ class AssetCapitalization(StockController):
return frappe.db.get_value(
"Asset",
asset,
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
["name", "item_code", "company", "status", "docstatus", "asset_type"],
as_dict=1,
)
@@ -380,8 +363,7 @@ class AssetCapitalization(StockController):
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
self.total_value = flt(self.total_value, self.precision("total_value"))
self.target_qty = flt(self.target_qty, self.precision("target_qty"))
self.target_incoming_rate = self.total_value / self.target_qty
self.target_incoming_rate = self.total_value
def update_stock_ledger(self):
sl_entries = []
@@ -489,7 +471,7 @@ class AssetCapitalization(StockController):
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
if not asset.is_composite_component:
if asset.asset_type != "Composite Component":
if asset.calculate_depreciation:
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
@@ -542,30 +524,29 @@ class AssetCapitalization(StockController):
def get_composite_component_value(self):
composite_component_value = 0
for item in self.asset_items:
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True)
if asset and asset.is_composite_component:
asset = frappe.db.get_value("Asset", item.asset, ["asset_type"], as_dict=True)
if asset and asset.asset_type == "Composite Component":
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
return composite_component_value
def get_gl_entries_for_target_item(
self, gl_entries, target_account, target_against, precision, composite_component_value
):
if self.target_is_fixed_asset:
total_value = flt(self.total_value - composite_component_value, precision)
if total_value:
# Capitalization
gl_entries.append(
self.get_gl_dict(
{
"account": target_account,
"against": ", ".join(target_against),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": total_value,
"cost_center": self.get("cost_center"),
},
item=self,
)
total_value = flt(self.total_value - composite_component_value, precision)
if total_value:
# Capitalization
gl_entries.append(
self.get_gl_dict(
{
"account": target_account,
"against": ", ".join(target_against),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": total_value,
"cost_center": self.get("cost_center"),
},
item=self,
)
)
def update_target_asset(self):
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
@@ -611,14 +592,13 @@ class AssetCapitalization(StockController):
def set_consumed_asset_status(self, asset):
if self.docstatus == 1:
if self.target_is_fixed_asset:
asset.set_status("Capitalized")
add_asset_activity(
asset.name,
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
get_link_to_form("Asset Capitalization", self.name)
),
)
asset.set_status("Capitalized")
add_asset_activity(
asset.name,
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
get_link_to_form("Asset Capitalization", self.name)
),
)
else:
asset.set_status()
add_asset_activity(
@@ -630,7 +610,7 @@ class AssetCapitalization(StockController):
@frappe.whitelist()
def get_target_item_details(item_code=None, company=None):
def get_target_item_details(item_code: str | None = None, company: str | None = None) -> frappe._dict:
out = frappe._dict()
# Get Item Details
@@ -640,17 +620,6 @@ def get_target_item_details(item_code=None, company=None):
# Set Item Details
out.target_item_name = item.item_name
out.target_is_fixed_asset = cint(item.is_fixed_asset)
out.target_has_batch_no = cint(item.has_batch_no)
out.target_has_serial_no = cint(item.has_serial_no)
if out.target_is_fixed_asset:
out.target_qty = 1
if not out.target_has_batch_no:
out.target_batch_no = None
if not out.target_has_serial_no:
out.target_serial_no = ""
# Cost Center
item_defaults = get_item_defaults(item.name, company)
@@ -667,7 +636,7 @@ def get_target_item_details(item_code=None, company=None):
@frappe.whitelist()
def get_target_asset_details(asset=None, company=None):
def get_target_asset_details(asset: str | None = None, company: str | None = None) -> frappe._dict:
out = frappe._dict()
# Get Asset Details

View File

@@ -10,12 +10,14 @@ from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import (
create_asset,
create_asset_data,
create_fixed_asset_item,
set_depreciation_settings_in_company,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
make_serial_batch_bundle,
)
@@ -61,7 +63,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
)
@@ -81,7 +83,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
@@ -156,7 +157,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
)
@@ -176,8 +177,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
@@ -245,7 +244,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
)
@@ -262,8 +261,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
@@ -313,7 +310,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
)
@@ -361,33 +358,45 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
)
consumed_asset_value = 100000
consumed_asset = create_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_value,
submit=1,
warehouse="Stores - _TC",
is_composite_component=1,
item = create_fixed_asset_item("Asset Capitalization Consumable Asset")
pr = make_purchase_receipt(
item_code=item.item_code,
qty=1,
rate=consumed_asset_value,
company=company,
warehouse="Stores - TCP1",
)
consumed_asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
consumed_asset_doc = frappe.get_doc("Asset", consumed_asset_name)
consumed_asset_doc.update(
{
"asset_type": "Composite Component",
"purchase_date": pr.posting_date,
"available_for_use_date": pr.posting_date,
}
)
consumed_asset_doc.save()
consumed_asset_doc.submit()
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
consumed_asset=consumed_asset.name,
consumed_asset=consumed_asset_doc.name,
company=company,
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
actual_gle = get_actual_gle_dict(asset_capitalization.name)
@@ -421,9 +430,6 @@ def create_asset_capitalization(**args):
"target_item_code": target_item_code,
"target_asset": target_asset.name,
"target_asset_location": "Test Location",
"target_qty": flt(args.target_qty) or 1,
"target_batch_no": args.target_batch_no,
"target_serial_no": args.target_serial_no,
"finance_book": args.finance_book,
}
)
@@ -516,7 +522,7 @@ def create_depreciation_asset(**args):
args = frappe._dict(args)
asset = frappe.new_doc("Asset")
asset.is_existing_asset = 1
asset.asset_type = args.asset_type or "Existing Asset"
asset.calculate_depreciation = 1
asset.asset_owner = "Company"

View File

@@ -87,7 +87,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-10-10",
is_existing_asset=1,
asset_type="Existing Asset",
opening_number_of_booked_depreciations=9,
opening_accumulated_depreciation=265,
depreciation_start_date="2024-07-31",
@@ -127,7 +127,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-10-10",
is_existing_asset=1,
asset_type="Existing Asset",
opening_number_of_booked_depreciations=9,
opening_accumulated_depreciation=265.30,
depreciation_start_date="2024-07-31",
@@ -165,7 +165,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-11-01",
is_existing_asset=1,
asset_type="Existing Asset",
opening_number_of_booked_depreciations=4,
opening_accumulated_depreciation=223.15,
depreciation_start_date="2024-12-31",
@@ -529,7 +529,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2023-03-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
is_existing_asset=1,
asset_type="Existing Asset",
opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2,
submit=1,
@@ -851,7 +851,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2023-03-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
is_existing_asset=1,
asset_type="Existing Asset",
opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2,
submit=1,
@@ -925,7 +925,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2021-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
asset_type="Existing Asset",
submit=1,
)
post_depreciation_entries(date="2021-12-31")
@@ -1014,7 +1014,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2021-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
asset_type="Existing Asset",
submit=1,
)
post_depreciation_entries(date="2021-12-31")
@@ -1093,7 +1093,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
rate_of_depreciation=50,
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
asset_type="Existing Asset",
submit=1,
)
post_depreciation_entries(date="2021-12-31")

View File

@@ -32,6 +32,7 @@
{
"fieldname": "purpose",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Purpose",
"options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
"reqd": 1
@@ -97,7 +98,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-05-30 17:01:55.864353",
"modified": "2026-03-09 17:19:02.087333",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement",

View File

@@ -9,9 +9,9 @@
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"asset",
"asset_name",
"company",
"column_break_2",
"repair_status",
"failure_date",
@@ -28,10 +28,6 @@
"column_break_ajbh",
"column_break_hkem",
"repair_cost",
"accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"stock_consumption_details_section",
"stock_items",
"section_break_ltbb",
@@ -43,7 +39,12 @@
"capitalize_repair_cost",
"increase_in_asset_life",
"column_break_xebe",
"total_repair_cost"
"total_repair_cost",
"accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"connection_tab"
],
"fields": [
{
@@ -149,8 +150,7 @@
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Repair Purchase Invoices"
"hide_border": 1
},
{
"fieldname": "stock_items",
@@ -206,6 +206,7 @@
{
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Repair Purchase Invoices",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
"no_copy": 1,
"options": "Asset Repair Purchase Invoice"
@@ -244,6 +245,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.consumed_items_cost",
"fieldname": "consumed_items_cost",
"fieldtype": "Currency",
"label": "Consumed Items Cost"
@@ -256,7 +258,13 @@
"depends_on": "capitalize_repair_cost",
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
"label": "Accounting Dimension"
},
{
"fieldname": "connection_tab",
"fieldtype": "Tab Break",
"label": "Connection",
"show_dashboard": 1
}
],
"index_web_pages_for_search": 1,
@@ -267,7 +275,7 @@
"link_fieldname": "asset_repair"
}
],
"modified": "2026-01-06 15:48:13.862505",
"modified": "2026-02-06 14:57:54.257572",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -360,7 +360,7 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset = create_asset(asset_type="Existing Asset", calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
@@ -400,7 +400,7 @@ def create_asset_repair(**args):
if args.asset:
asset = args.asset
else:
asset = create_asset(is_existing_asset=1, submit=1, company=args.company)
asset = create_asset(asset_type=args.asset_type or "Existing Asset", submit=1, company=args.company)
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update(
{

View File

@@ -144,7 +144,7 @@ def get_conditions(filters):
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
conditions["asset_type"] = "Existing Asset"
if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"):
@@ -274,7 +274,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
)
if filters.only_existing_assets:
query = query.where(asset.is_existing_asset == 1)
query = query.where(asset.asset_type == "Existing Asset")
if filters.asset_category:
query = query.where(asset.asset_category == filters.asset_category)
if filters.cost_center:
@@ -325,7 +325,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
)
if filters.only_existing_assets:
query = query.where(asset.is_existing_asset == 1)
query = query.where(asset.asset_type == "Existing Asset")
if filters.asset_category:
query = query.where(asset.asset_category == filters.asset_category)
if filters.cost_center:

View File

@@ -252,6 +252,7 @@
"allow_on_submit": 1,
"fieldname": "schedule_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Required By"
},
{
@@ -1327,7 +1328,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-02 00:40:47.119584",
"modified": "2026-03-09 17:15:29.184682",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -49,7 +49,6 @@
{
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Series",
"no_copy": 1,
"oldfieldname": "naming_series",
@@ -77,6 +76,7 @@
"fieldname": "vendor",
"fieldtype": "Link",
"hidden": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Supplier",
"no_copy": 1,
@@ -95,6 +95,7 @@
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
@@ -147,7 +148,6 @@
"depends_on": "eval:doc.use_html == 0",
"fieldname": "message_for_supplier",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Message for Supplier",
"mandatory_depends_on": "eval:doc.use_html == 0",
"print_hide": 1
@@ -225,6 +225,8 @@
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"oldfieldname": "status",
@@ -263,6 +265,7 @@
{
"fieldname": "schedule_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Required Date"
},
{
@@ -350,7 +353,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-03-01 23:38:48.079274",
"modified": "2026-03-09 17:15:29.774614",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -178,6 +178,7 @@
"default": "Company",
"fieldname": "supplier_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Supplier Type",
"options": "Company\nIndividual\nPartnership",
"reqd": 1
@@ -238,6 +239,7 @@
"fieldname": "default_currency",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Billing Currency",
"no_copy": 1,
"options": "Currency"
@@ -515,7 +517,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-02-10 21:28:01.101808",
"modified": "2026-03-09 17:15:25.465759",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -34,7 +34,7 @@ class TestPurchaseOrder(IntegrationTestCase):
self.assertEqual(sq.get("items")[1].rate, 300)
self.assertEqual(sq.get("items")[1].description, "test")
def test_update_supplier_quotation_child_rate_disallow(self):
def test_update_supplier_quotation_child_rate(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
trans_item = json.dumps(
@@ -47,6 +47,22 @@ class TestPurchaseOrder(IntegrationTestCase):
},
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(sq.get("items")[0].rate, 300)
po = make_purchase_order(sq.name)
po.schedule_date = add_days(today(), 1)
po.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": 20,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
]
)
self.assertRaises(
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)

View File

@@ -2525,13 +2525,14 @@ class AccountsController(TransactionBase):
grand_total = flt(self.get("rounded_total") or self.grand_total)
automatically_fetch_payment_terms = 0
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
po_or_so, doctype, fieldname = self.get_order_details()
automatically_fetch_payment_terms = cint(
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
if self.doctype != "Sales Order":
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"):
if party_account_currency == self.company_currency:
@@ -2547,7 +2548,7 @@ class AccountsController(TransactionBase):
if not self.get("payment_schedule"):
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"]
self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
and automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
@@ -2605,16 +2606,18 @@ class AccountsController(TransactionBase):
if not self.get("items"):
return None, None, None
if self.doctype == "Sales Invoice":
po_or_so = self.get("items")[0].get("sales_order")
po_or_so_doctype = "Sales Order"
po_or_so_doctype_name = "sales_order"
prev_doc = self.get("items")[0].get("sales_order")
prev_doctype = "Sales Order"
prev_doctype_name = "sales_order"
elif self.doctype == "Purchase Invoice":
prev_doc = self.get("items")[0].get("purchase_order")
prev_doctype = "Purchase Order"
prev_doctype_name = "purchase_order"
else:
po_or_so = self.get("items")[0].get("purchase_order")
po_or_so_doctype = "Purchase Order"
po_or_so_doctype_name = "purchase_order"
return po_or_so, po_or_so_doctype, po_or_so_doctype_name
prev_doc = self.get("items")[0].get("prevdoc_docname")
prev_doctype = "Quotation"
prev_doctype_name = "prevdoc_docname"
return prev_doc, prev_doctype, prev_doctype_name
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
@@ -3872,20 +3875,28 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
return False
def validate_quantity(child_item, new_data):
def validate_quantity_and_rate(child_item, new_data):
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
frappe.throw(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
)
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
qty_limits = {
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
}
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
frappe.throw(_("Cannot set quantity less than received quantity"))
if parent_doctype in qty_limits:
qty_field, error_message = qty_limits[parent_doctype]
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
frappe.throw(
_("Row #{0}:").format(new_data.get("idx"))
+ error_message.format(frappe.bold(new_data.get("item_code"))),
title=_("Invalid Qty"),
)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if (parent_doctype == "Quotation" and not ordered_items) or (
@@ -3898,7 +3909,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if parent_doctype == "Quotation"
else purchased_items.get(child_item.name)
)
if qty_to_check:
if not rate_unchanged:
frappe.throw(
_(
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
).format(frappe.bold(new_data.get("item_code")))
)
if flt(new_data.get("qty")) < qty_to_check:
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
@@ -4017,10 +4036,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
):
continue
validate_quantity(child_item, d)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if not rate_unchanged:
frappe.throw(_("Rates cannot be modified for quoted items"))
validate_quantity_and_rate(child_item, d)
if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True

View File

@@ -1439,6 +1439,16 @@ class StockController(AccountsController):
continue
if qi_required: # validate row only if inspection is required on item level
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
self.validate_qi_presence(row)
if self.docstatus == 1:
self.validate_qi_submission(row)
@@ -1446,16 +1456,6 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)

View File

@@ -0,0 +1,21 @@
{
"app": "erpnext",
"bg_color": "blue",
"creation": "2026-02-24 17:43:08.379896",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon_type": "Link",
"idx": 0,
"label": "Organization",
"link_to": "Organization",
"link_type": "Workspace Sidebar",
"modified": "2026-02-24 17:59:39.885360",
"modified_by": "Administrator",
"name": "Organization",
"owner": "Administrator",
"parent_icon": "",
"restrict_removal": 0,
"roles": [],
"standard": 1
}

View File

@@ -219,6 +219,16 @@ website_route_rules = [
{"from_route": "/tasks", "to_route": "Task"},
]
standard_navbar_items = [
{
"item_label": "Clear Demo Data",
"item_type": "Action",
"action": "erpnext.demo.clear_demo();",
"is_standard": 1,
"condition": "eval: frappe.boot.sysdefaults.demo_company",
},
]
standard_portal_menu_items = [
{"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"},
{

File diff suppressed because it is too large Load Diff

View File

@@ -637,11 +637,18 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
}
buying_price_list(doc) {
this.apply_price_list();
if (doc.rm_cost_as_per !== "Price List" && doc.buying_price_list) {
this.frm.set_value("buying_price_list", "");
return;
}
if (doc.buying_price_list) {
this.apply_price_list();
}
}
plc_conversion_rate(doc) {
if (!this.in_apply_price_list) {
if (!this.in_apply_price_list && doc.rm_cost_as_per === "Price List") {
this.apply_price_list(null, true);
}
}

View File

@@ -15,18 +15,18 @@ frappe.treeview_settings["BOM"] = {
get_tree_root: false,
show_expand_all: false,
get_label: function (node) {
if (node.data.qty) {
const escape = frappe.utils.escape_html;
let label = escape(node.data.item_code);
if (node.data.item_name && node.data.item_code !== node.data.item_name) {
label += `: ${escape(node.data.item_name)}`;
}
return `${label} <span class="badge badge-pill badge-light">${node.data.qty} ${escape(
__(node.data.stock_uom)
)}</span>`;
} else {
return node.data.item_code || node.data.value;
if (node.is_root && node.data.value != "BOM") {
frappe.model.with_doc("BOM", node.data.value, function () {
var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.item_name = bom.item_name || "";
node.data.item_code = bom.item || "";
node.data.qty = bom.quantity || "";
node.data.stock_uom = bom.uom || "";
return get_bom_node(node);
});
}
return get_bom_node(node);
},
onload: function (me) {
var label = frappe.get_route()[0] + "/" + frappe.get_route()[1];
@@ -78,3 +78,22 @@ frappe.treeview_settings["BOM"] = {
},
view_template: "bom_item_preview",
};
function get_bom_node(node) {
if (node.data.qty) {
const escape = frappe.utils.escape_html;
let label = escape(node.data.item_code);
if (node.is_root && node.data.value != "BOM") {
label = escape(node.data.value);
}
if (node.data.item_name && node.data.item_code !== node.data.item_name) {
label += `: ${escape(node.data.item_name)}`;
}
return `${label} <span class="badge badge-pill badge-light">${node.data.qty} ${escape(
__(node.data.stock_uom)
)}</span>`;
} else {
return node.data.item_code || node.data.value;
}
}

View File

@@ -1321,9 +1321,9 @@ class JobCard(Document):
def is_work_order_closed(self):
if self.work_order:
status = frappe.get_value("Work Order", self.work_order)
status = frappe.get_value("Work Order", self.work_order, "status")
if status == "Closed":
if status in ["Closed", "Stopped"]:
return True
return False

View File

@@ -461,10 +461,11 @@ frappe.ui.form.on("Work Order", {
var added_min = false;
// produced qty
var title = __("{0} items produced", [frm.doc.produced_qty]);
let produced_qty = frm.doc.produced_qty - frm.doc.disassembled_qty;
var title = __("{0} items produced", [produced_qty]);
bars.push({
title: title,
width: (frm.doc.produced_qty / frm.doc.qty) * 100 + "%",
width: (flt(produced_qty) / frm.doc.qty) * 100 + "%",
progress_class: "progress-bar-success",
});
if (bars[0].width == "0%") {
@@ -481,14 +482,27 @@ frappe.ui.form.on("Work Order", {
if (pending_complete > 0) {
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
title = __("{0} items in progress", [pending_complete]);
let progress_class = "progress-bar-warning";
if (frm.doc.status == "Closed") {
if (frm.doc.required_items.find((d) => d.returned_qty > 0)) {
title = __("{0} items returned", [pending_complete]);
progress_class = "progress-bar-warning";
} else {
title = __("{0} items to return", [pending_complete]);
progress_class = "progress-bar-info";
}
}
bars.push({
title: title,
width: (width > 100 ? "99.5" : width) + "%",
progress_class: "progress-bar-warning",
progress_class: progress_class,
});
message = message + ". " + title;
}
}
//process loss qty
if (frm.doc.process_loss_qty) {
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
@@ -499,6 +513,19 @@ frappe.ui.form.on("Work Order", {
});
message = message + ". " + title;
}
// disassembled qty
if (frm.doc.disassembled_qty) {
var disassembled_width = (frm.doc.disassembled_qty / frm.doc.qty) * 100;
title = __("{0} items disassembled", [frm.doc.disassembled_qty]);
bars.push({
title: title,
width: disassembled_width + "%",
progress_class: "progress-bar-secondary",
});
message = message + ". " + title;
}
frm.dashboard.add_progress(__("Status"), bars, message);
},

View File

@@ -713,19 +713,25 @@ class WorkOrder(Document):
self.db_set("disassembled_qty", self.disassembled_qty)
def get_transferred_or_manufactured_qty(self, purpose, fieldname):
table = frappe.qb.DocType("Stock Entry")
query = frappe.qb.from_(table).where(
(table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose)
parent = frappe.qb.DocType("Stock Entry")
query = frappe.qb.from_(parent).where(
(parent.work_order == self.name)
& (parent.docstatus == 1)
& (parent.purpose == purpose)
& (parent.is_additional_transfer_entry == cint(fieldname == "additional_transferred_qty"))
)
if purpose == "Manufacture":
query = query.select(Sum(table.fg_completed_qty) - Sum(table.process_loss_qty))
child = frappe.qb.DocType("Stock Entry Detail")
query = (
query.join(child)
.on(parent.name == child.parent)
.select(Sum(child.transfer_qty))
.where(child.is_finished_item == 1)
)
else:
query = query.select(Sum(table.fg_completed_qty))
query = query.where(
table.is_additional_transfer_entry == cint(fieldname == "additional_transferred_qty")
)
query = query.select(Sum(parent.fg_completed_qty))
return flt(query.run()[0][0])

View File

@@ -74,7 +74,6 @@ erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.move_item_tax_to_item_tax_template
erpnext.patches.v11_1.set_variant_based_on
erpnext.patches.v11_1.woocommerce_set_creation_user
erpnext.patches.v11_1.rename_depends_on_lwp
execute:frappe.delete_doc("Report", "Inactive Items")
erpnext.patches.v11_1.delete_scheduling_tool
erpnext.patches.v12_0.rename_tolerance_fields
@@ -468,3 +467,6 @@ erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v15_0.delete_quotation_lost_record_detail
erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
erpnext.patches.v16_0.migrate_asset_type_checkboxes_to_select
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.enable_serial_batch_setting

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
if not frappe.get_all("Serial No", limit=1) and not frappe.get_all("Batch", limit=1):
return
frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1)
frappe.db.set_default("enable_serial_and_batch_no_for_item", 1)

View File

@@ -0,0 +1,25 @@
import frappe
from frappe.query_builder import Case
def execute():
required_columns = [
"is_existing_asset",
"is_composite_asset",
"is_composite_component",
]
# Skip patch if any required column is missing
if not all(frappe.db.has_column("Asset", col) for col in required_columns):
return
Asset = frappe.qb.DocType("Asset")
frappe.qb.update(Asset).set(
Asset.asset_type,
Case()
.when(Asset.is_existing_asset == 1, "Existing Asset")
.when(Asset.is_composite_asset == 1, "Composite Asset")
.when(Asset.is_composite_component == 1, "Composite Component")
.else_(""),
).run()

View File

@@ -0,0 +1,33 @@
import frappe
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
def execute():
PurchaseOrderItem = DocType("Purchase Order Item")
MaterialRequestItem = DocType("Material Request Item")
poi_query = (
frappe.qb.from_(PurchaseOrderItem)
.select(PurchaseOrderItem.sales_order_item, Sum(PurchaseOrderItem.stock_qty))
.where(PurchaseOrderItem.sales_order_item.isnotnull() & PurchaseOrderItem.docstatus == 1)
.groupby(PurchaseOrderItem.sales_order_item)
)
mri_query = (
frappe.qb.from_(MaterialRequestItem)
.select(MaterialRequestItem.sales_order_item, Sum(MaterialRequestItem.stock_qty))
.where(MaterialRequestItem.sales_order_item.isnotnull() & MaterialRequestItem.docstatus == 1)
.groupby(MaterialRequestItem.sales_order_item)
)
poi_data = poi_query.run()
mri_data = mri_query.run()
updates_against_poi = {data[0]: {"ordered_qty": data[1]} for data in poi_data}
updates_against_mri = {data[0]: {"requested_qty": data[1], "ordered_qty": 0} for data in mri_data}
frappe.db.auto_commit_on_many_writes = 1
frappe.db.bulk_update("Sales Order Item", updates_against_mri)
frappe.db.bulk_update("Sales Order Item", updates_against_poi)
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -205,7 +205,7 @@ frappe.ui.form.on("Project", {
collect_progress: function (frm) {
if (frm.doc.collect_progress && !frm.doc.subject) {
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
frm.set_value("subject", __("For project - {0}, update your status", [frm.doc.project_name]));
}
},
});

View File

@@ -12,29 +12,21 @@
"project_name",
"status",
"project_type",
"is_active",
"percent_complete_method",
"percent_complete",
"column_break_5",
"project_template",
"expected_start_date",
"expected_end_date",
"priority",
"department",
"customer_details",
"customer",
"column_break_14",
"sales_order",
"users_section",
"users",
"copied_from",
"section_break0",
"notes",
"is_active",
"percent_complete",
"section_break_18",
"expected_start_date",
"actual_start_date",
"actual_time",
"column_break_20",
"expected_end_date",
"actual_end_date",
"costing_tab",
"project_details",
"estimated_costing",
"total_costing_amount",
@@ -50,7 +42,7 @@
"gross_margin",
"column_break_37",
"per_gross_margin",
"monitor_progress",
"monitor_progress_tab",
"collect_progress",
"holiday_list",
"frequency",
@@ -63,7 +55,18 @@
"weekly_time_to_send",
"column_break_45",
"subject",
"message"
"message",
"more_info_tab",
"customer_details",
"customer",
"column_break_14",
"sales_order",
"users_section",
"users",
"copied_from",
"section_break0",
"notes",
"connections_tab"
],
"fields": [
{
@@ -115,6 +118,7 @@
"bold": 1,
"fieldname": "percent_complete",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Completed",
"no_copy": 1,
"read_only": 1
@@ -135,6 +139,7 @@
"bold": 1,
"fieldname": "expected_start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Expected Start Date",
"oldfieldname": "project_start_date",
"oldfieldtype": "Date"
@@ -231,7 +236,7 @@
"collapsible": 1,
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Start and End Dates"
"label": "Timeline"
},
{
"fieldname": "actual_start_date",
@@ -258,7 +263,6 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "project_details",
"fieldtype": "Section Break",
"label": "Costing and Billing",
@@ -329,7 +333,6 @@
"options": "Cost Center"
},
{
"collapsible": 1,
"fieldname": "margin",
"fieldtype": "Section Break",
"label": "Margin",
@@ -357,12 +360,6 @@
"oldfieldtype": "Currency",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "monitor_progress",
"fieldtype": "Section Break",
"label": "Monitor Progress"
},
{
"default": "0",
"fieldname": "collect_progress",
@@ -455,6 +452,27 @@
"fieldtype": "Data",
"label": "Subject",
"mandatory_depends_on": "collect_progress"
},
{
"fieldname": "costing_tab",
"fieldtype": "Tab Break",
"label": "Costing"
},
{
"fieldname": "monitor_progress_tab",
"fieldtype": "Tab Break",
"label": "Progress"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
}
],
"icon": "fa fa-puzzle-piece",
@@ -462,7 +480,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2025-08-21 17:57:58.314809",
"modified": "2026-03-09 17:15:24.426294",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",

View File

@@ -19,6 +19,13 @@ frappe.ui.form.on("Project Template", {
frappe.ui.form.on("Project Template Task", {
task: function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (!row.task) {
row.subject = null;
refresh_field("tasks");
return;
}
frappe.db.get_value("Task", row.task, "subject", (value) => {
row.subject = value.subject;
refresh_field("tasks");

View File

@@ -13,7 +13,6 @@
"type",
"color",
"is_group",
"is_template",
"column_break0",
"status",
"priority",
@@ -21,17 +20,21 @@
"parent_task",
"completed_by",
"completed_on",
"section_break_dafi",
"is_template",
"column_break_vvfp",
"start",
"duration",
"sb_timeline",
"exp_start_date",
"expected_time",
"start",
"column_break_11",
"exp_end_date",
"progress",
"duration",
"is_milestone",
"sb_details",
"description",
"dependencies_tab",
"sb_depends_on",
"depends_on",
"depends_on_tasks",
@@ -44,12 +47,13 @@
"total_costing_amount",
"column_break_20",
"total_billing_amount",
"more_info_tab",
"sb_more_info",
"company",
"review_date",
"closing_date",
"column_break_22",
"department",
"company",
"lft",
"rgt",
"old_parent",
@@ -78,7 +82,6 @@
"oldfieldname": "project",
"oldfieldtype": "Link",
"options": "Project",
"remember_last_selected_value": 1,
"search_index": 1
},
{
@@ -218,7 +221,6 @@
{
"fieldname": "sb_depends_on",
"fieldtype": "Section Break",
"label": "Dependencies",
"oldfieldtype": "Section Break"
},
{
@@ -298,10 +300,9 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "sb_more_info",
"fieldtype": "Section Break",
"label": "More Info"
"label": "Additional Info"
},
{
"depends_on": "eval:doc.status == \"Closed\" || doc.status == \"Pending Review\"",
@@ -334,8 +335,7 @@
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1
"options": "Company"
},
{
"fieldname": "lft",
@@ -368,6 +368,7 @@
"options": "User"
},
{
"allow_in_quick_entry": 1,
"default": "0",
"fieldname": "is_template",
"fieldtype": "Check",
@@ -397,6 +398,24 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Template Task"
},
{
"fieldname": "dependencies_tab",
"fieldtype": "Tab Break",
"label": "Dependencies"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "section_break_dafi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vvfp",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-check",
@@ -404,11 +423,11 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2025-10-16 08:39:12.214577",
"modified": "2026-03-04 11:47:10.454548",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
"naming_rule": "Expression (old style)",
"naming_rule": "Expression",
"nsm_parent_field": "parent_task",
"owner": "Administrator",
"permissions": [
@@ -425,6 +444,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "subject",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
@@ -434,4 +454,4 @@
"timeline_field": "project",
"title_field": "subject",
"track_seen": 1
}
}

View File

@@ -138,6 +138,8 @@ class Task(NestedSet):
def validate_status(self):
if self.is_template and self.status != "Template":
self.status = "Template"
if self.status == "Template" and not self.is_template:
self.status = "Open"
if self.status != self.get_db_value("status") and self.status == "Completed":
for d in self.depends_on:
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):

View File

@@ -260,6 +260,33 @@ frappe.ui.form.on("Timesheet", {
parent_project: function (frm) {
set_project_in_timelog(frm);
},
employee: function (frm) {
if (frm.doc.employee && frm.doc.time_logs) {
const selected_employee = frm.doc.employee;
frm.doc.time_logs.forEach((row) => {
if (row.activity_type) {
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
employee: frm.doc.employee,
activity_type: row.activity_type,
currency: frm.doc.currency,
},
callback: function (r) {
if (r.message) {
if (selected_employee !== frm.doc.employee) return;
row.billing_rate = r.message["billing_rate"];
row.costing_rate = r.message["costing_rate"];
frm.refresh_fields("time_logs");
calculate_billing_costing_amount(frm, row.doctype, row.name);
}
},
});
}
});
}
},
});
frappe.ui.form.on("Timesheet Detail", {

View File

@@ -18,28 +18,29 @@
"column_break_3",
"status",
"parent_project",
"employee_detail",
"employee",
"employee_name",
"department",
"column_break_9",
"user",
"start_date",
"end_date",
"employee_detail",
"employee",
"department",
"column_break_9",
"employee_name",
"section_break_5",
"time_logs",
"working_hours",
"total_hours",
"billing_tab",
"billing_details",
"total_billable_hours",
"total_billable_amount",
"total_costing_amount",
"base_total_billable_amount",
"base_total_billed_amount",
"base_total_costing_amount",
"column_break_10",
"total_billed_hours",
"total_billable_amount",
"total_billed_amount",
"total_costing_amount",
"base_total_billed_amount",
"per_billed",
"section_break_18",
"note",
@@ -176,7 +177,6 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "billing_details",
"fieldtype": "Section Break",
"label": "Billing Details",
@@ -304,13 +304,18 @@
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate"
},
{
"fieldname": "billing_tab",
"fieldtype": "Tab Break",
"label": "Billing"
}
],
"icon": "fa fa-clock-o",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-12-19 13:48:23.453636",
"modified": "2026-03-04 11:56:51.438298",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

View File

@@ -0,0 +1,4 @@
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.5714 0H15.4286C6.90761 0 0 6.90761 0 15.4286V38.5714C0 47.0924 6.90761 54 15.4286 54H38.5714C47.0924 54 54 47.0924 54 38.5714V15.4286C54 6.90761 47.0924 0 38.5714 0Z" fill="#0289F7"/>
<path d="M19.2857 15.4286H22.1786C23.7763 15.4286 25.0714 16.7237 25.0714 18.3214V24.1071C25.0714 25.7048 23.7763 27 22.1786 27H19.2857V38.5714H15.4286V27H11.5714V23.1428H21.2143V19.2857H11.5714V15.4286H15.4286V11.5714H19.2857V15.4286ZM38.5714 38.5714H34.7143V34.7143H31.8214C30.2238 34.7143 28.9286 33.4191 28.9286 31.8214V26.0357C28.9286 24.438 30.2238 23.1428 31.8214 23.1428H34.7143V11.5714H38.5714V23.1428H42.4286V27H32.7857V30.8571H42.4286V34.7143H38.5714V38.5714Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 0H8C3.58172 0 0 3.58172 0 8V20C0 24.4183 3.58172 28 8 28H20C24.4183 28 28 24.4183 28 20V8C28 3.58172 24.4183 0 20 0Z" fill="#0289F7"/>
<path d="M20.5 13.25C20.5 13.0926 20.5 13 20.5 13L18.5 11.499V19.5H20C20.2761 19.5 20 19.5 20.5 19.5V13.25ZM14.5 14V16H10.5V14H14.5ZM22.5 19C22.5 20.3807 21.3807 21.5 20 21.5H16.5V19.5V7.5C16.5 7.5 16.2761 7.5 16 7.5H9C8.72386 7.5 9 7.5 8.5 7.5V19.5C9 19.5 8.72386 19.5 9 19.5H12.5V21.5H9C7.61929 21.5 6.5 20.3807 6.5 19V8C6.5 6.61929 7.61929 5.5 9 5.5H16C17.3807 5.5 18.5 6.61929 18.5 8V9L21.5 11.25C22.1295 11.7221 22.5 12.4631 22.5 13.25V19Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@@ -0,0 +1,4 @@
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.5714 0H15.4286C6.90761 0 0 6.90761 0 15.4286V38.5714C0 47.0924 6.90761 54 15.4286 54H38.5714C47.0924 54 54 47.0924 54 38.5714V15.4286C54 6.90761 47.0924 0 38.5714 0Z" fill="#0289F7" fill-opacity="0.1"/>
<path d="M19.2857 15.4286H22.1786C23.7762 15.4286 25.0714 16.7238 25.0714 18.3215V24.1072C25.0714 25.7049 23.7762 27 22.1786 27H19.2857V38.5715H15.4286V27H11.5714V23.1429H21.2143V19.2858H11.5714V15.4286H15.4286V11.5715H19.2857V15.4286ZM38.5714 38.5715H34.7143V34.7143H31.8214C30.2237 34.7143 28.9286 33.4192 28.9286 31.8215V26.0358C28.9286 24.4381 30.2237 23.1429 31.8214 23.1429H34.7143V11.5715H38.5714V23.1429H42.4286V27H32.7857V30.8572H42.4286V34.7143H38.5714V38.5715Z" fill="#0981E3"/>
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 0H8C3.58172 0 0 3.58172 0 8V20C0 24.4183 3.58172 28 8 28H20C24.4183 28 28 24.4183 28 20V8C28 3.58172 24.4183 0 20 0Z" fill="#0289F7" fill-opacity="0.1"/>
<path d="M20.5 13.25C20.5 13.0926 20.5 13 20.5 13L18.5 11.499V19.5H20C20.2761 19.5 20 19.5 20.5 19.5V13.25ZM14.5 14V16H10.5V14H14.5ZM22.5 19C22.5 20.3807 21.3807 21.5 20 21.5H16.5V19.5V7.5C16.5 7.5 16.2761 7.5 16 7.5H9C8.72386 7.5 9 7.5 8.5 7.5V19.5C9 19.5 8.72386 19.5 9 19.5H12.5V21.5H9C7.61929 21.5 6.5 20.3807 6.5 19V8C6.5 6.61929 7.61929 5.5 9 5.5H16C17.3807 5.5 18.5 6.61929 18.5 8V9L21.5 11.25C22.1295 11.7221 22.5 12.4631 22.5 13.25V19Z" fill="#0981E3"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -543,7 +543,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
schedules: selected,
},
});
frappe.model.sync(pr_name);
frappe.set_route("Form", "Payment Request", pr_name.name);
},
});
@@ -580,6 +580,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.validate_has_items();
erpnext.utils.view_serial_batch_nos(this.frm);
this.set_route_options_for_new_doc();
erpnext.toggle_serial_batch_fields(this.frm);
}
set_route_options_for_new_doc() {
@@ -1307,6 +1308,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (this.frm.doc.transaction_date) {
this.frm.transaction_date = this.frm.doc.transaction_date;
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
this.recalculate_terms();
}
}

View File

@@ -19,6 +19,77 @@ $.extend(erpnext, {
return currency_list;
},
toggle_serial_batch_fields(frm) {
let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0;
let fields = ["serial_and_batch_bundle", "use_serial_batch_fields", "serial_no", "batch_no"];
if (
[
"Stock Entry",
"Purchase Receipt",
"Purchase Invoice",
"Stock Reconciliation",
"Subcontracting Receipt",
].includes(frm.doc.doctype)
) {
fields.push("add_serial_batch_bundle");
}
if (["Stock Reconciliation"].includes(frm.doc.doctype)) {
fields.push("reconcile_all_serial_batch");
}
if (["Sales Invoice", "Delivery Note", "Pick List"].includes(frm.doc.doctype)) {
fields.push("pick_serial_and_batch");
}
if (["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(frm.doc.doctype)) {
fields.push("add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle");
}
let child_name = "items";
if (frm.doc.doctype === "Pick List") {
child_name = "locations";
}
if (frm.doc.doctype === "Asset Capitalization") {
child_name = "stock_items";
}
fields.forEach((field) => {
if (frm.fields_dict[child_name].get_field(field)) {
frm.fields_dict[child_name].grid.update_docfield_property(field, "hidden", hide_fields);
frm.fields_dict[child_name].grid.update_docfield_property(
field,
"in_list_view",
hide_fields ? 0 : 1
);
if (
frm.doc.doctype === "Subcontracting Receipt" &&
!["add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"].includes(field)
) {
frm.fields_dict["supplied_items"].grid.update_docfield_property(
field,
"hidden",
hide_fields
);
frm.fields_dict["supplied_items"].grid.update_docfield_property(
field,
"in_list_view",
hide_fields ? 0 : 1
);
frm.fields_dict["supplied_items"].grid.reset_grid();
}
}
});
frm.fields_dict[child_name].grid.reset_grid();
},
toggle_naming_series: function () {
if (
cur_frm &&

View File

@@ -240,6 +240,7 @@
"fieldname": "default_currency",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Billing Currency",
"no_copy": 1,
"options": "Currency"
@@ -639,7 +640,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-02-02 15:39:55.920831",
"modified": "2026-03-09 17:15:26.040050",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -213,6 +213,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
@@ -1127,7 +1128,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2026-02-06 17:34:22.170032",
"modified": "2026-03-09 17:15:31.941114",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt, getdate, nowdate
from frappe.utils import cint, flt, getdate, nowdate
from erpnext.controllers.selling_controller import SellingController
@@ -446,6 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
automatically_fetch_payment_terms = cint(
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -453,6 +457,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
"Quotation": {
"doctype": "Sales Order",
"validation": {"docstatus": ["=", 1]},
"field_no_map": ["payment_terms_template"],
},
"Quotation Item": {
"doctype": "Sales Order Item",
@@ -462,13 +467,15 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
"Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
},
target_doc,
set_missing_values,
ignore_permissions=ignore_permissions,
)
if automatically_fetch_payment_terms:
doclist.set_payment_schedule()
return doclist

View File

@@ -59,8 +59,22 @@ class TestQuotation(IntegrationTestCase):
qo.payment_schedule[0].due_date = add_days(qo.transaction_date, -2)
self.assertRaises(frappe.ValidationError, qo.save)
def test_update_child_disallow_rate_change(self):
qo = make_quotation(qty=4)
def test_update_child_rate_change(self):
from erpnext.stock.doctype.item.test_item import make_item
item_1 = make_item("_Test Item")
item_2 = make_item("_Test Item 1")
item_list = [
{"item_code": item_1.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 300},
{"item_code": item_2.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 5, "rate": 400},
]
qo = make_quotation(item_list=item_list)
so = make_sales_order(qo.name, args={"filtered_children": [qo.items[0].name]})
so.delivery_date = nowdate()
so.submit()
qo.reload()
trans_item = json.dumps(
[
{
@@ -68,10 +82,35 @@ class TestQuotation(IntegrationTestCase):
"rate": 5000,
"qty": qo.items[0].qty,
"docname": qo.items[0].name,
}
},
{
"item_code": qo.items[1].item_code,
"rate": qo.items[1].rate,
"qty": qo.items[1].qty,
"docname": qo.items[1].name,
},
]
)
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
trans_item = json.dumps(
[
{
"item_code": qo.items[0].item_code,
"rate": qo.items[0].rate,
"qty": qo.items[0].qty,
"docname": qo.items[0].name,
},
{
"item_code": qo.items[1].item_code,
"rate": 50,
"qty": qo.items[1].qty,
"docname": qo.items[1].name,
},
]
)
update_child_qty_rate("Quotation", trans_item, qo.name)
qo.reload()
self.assertEqual(qo.items[1].rate, 50)
def test_update_child_removing_item(self):
qo = make_quotation(qty=10)
@@ -143,6 +182,10 @@ class TestQuotation(IntegrationTestCase):
self.assertTrue(quotation.payment_schedule)
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
)
def test_make_sales_order_terms_copied(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@@ -285,7 +328,11 @@ class TestQuotation(IntegrationTestCase):
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
{
"add_taxes_from_item_tax_template": 0,
"add_taxes_from_taxes_and_charges_template": 0,
"automatically_fetch_payment_terms": 1,
},
)
def test_make_sales_order_with_terms(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@@ -323,10 +370,13 @@ class TestQuotation(IntegrationTestCase):
sales_order.save()
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
self.assertEqual(
getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date)
)
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
self.assertEqual(
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
getdate(sales_order.payment_schedule[1].due_date),
getdate(add_days(quotation.transaction_date, 30)),
)
def test_valid_till_before_transaction_date(self):
@@ -1026,6 +1076,56 @@ class TestQuotation(IntegrationTestCase):
quotation.reload()
self.assertEqual(quotation.status, "Open")
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
)
def test_make_sales_order_with_payment_terms(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test Payment Terms Template for Quotation",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 0,
},
{
"doctype": "Payment Terms Template Detail",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 10,
},
],
}
).save()
quotation = make_quotation(qty=10, rate=1000, do_not_submit=1)
quotation.transaction_date = add_days(nowdate(), -2)
quotation.valid_till = add_days(nowdate(), 10)
quotation.update({"payment_terms_template": template.name, "payment_schedule": []})
quotation.save()
quotation.submit()
self.assertEqual(quotation.payment_schedule[0].payment_amount, 5000)
self.assertEqual(quotation.payment_schedule[1].payment_amount, 5000)
self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 10))
sales_order = make_sales_order(quotation.name)
sales_order.transaction_date = nowdate()
sales_order.delivery_date = nowdate()
sales_order.save()
self.assertEqual(sales_order.payment_schedule[0].due_date, sales_order.transaction_date)
self.assertEqual(sales_order.payment_schedule[1].due_date, add_days(sales_order.transaction_date, 10))
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 5000)
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 5000)
def enable_calculate_bundle_price(enable=1):
selling_settings = frappe.get_doc("Selling Settings")

View File

@@ -56,6 +56,13 @@ frappe.ui.form.on("Sales Order", {
frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true);
},
delivery_date(frm) {
if (frm.doc.delivery_date) {
frm.doc.items.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "delivery_date", frm.doc.delivery_date);
});
}
},
refresh: function (frm) {
frm.fields_dict["items"].grid.update_docfield_property(
@@ -158,7 +165,7 @@ frappe.ui.form.on("Sales Order", {
});
}
}
prevent_past_delivery_dates(frm);
// Hide `Reserve Stock` field description in submitted or cancelled Sales Order.
if (frm.doc.docstatus > 0) {
frm.set_df_property("reserve_stock", "description", null);
@@ -238,13 +245,6 @@ frappe.ui.form.on("Sales Order", {
];
},
delivery_date: function (frm) {
$.each(frm.doc.items || [], function (i, d) {
if (!d.delivery_date) d.delivery_date = frm.doc.delivery_date;
});
refresh_field("items");
},
create_stock_reservation_entries(frm) {
const dialog = new frappe.ui.Dialog({
title: __("Stock Reservation"),
@@ -1816,3 +1816,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
};
extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm }));
function prevent_past_delivery_dates(frm) {
if (frm.doc.transaction_date) {
frm.fields_dict["delivery_date"].datepicker?.update({
minDate: new Date(frm.doc.transaction_date),
});
}
}

View File

@@ -123,6 +123,7 @@
"company_contact_person",
"payment_schedule_section",
"payment_terms_section",
"ignore_default_payment_terms_template",
"payment_terms_template",
"payment_schedule",
"terms_section_break",
@@ -1733,6 +1734,14 @@
"fieldtype": "Time",
"label": "Time",
"mandatory_depends_on": "is_internal_customer"
},
{
"default": "0",
"fieldname": "ignore_default_payment_terms_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -1740,7 +1749,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-02 00:42:18.834823",
"modified": "2026-03-04 18:04:05.873483",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -116,6 +116,7 @@ class SalesOrder(SellingController):
grand_total: DF.Currency
group_same_items: DF.Check
has_unit_price_items: DF.Check
ignore_default_payment_terms_template: DF.Check
ignore_pricing_rule: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None

View File

@@ -95,6 +95,7 @@
"ordered_qty",
"planned_qty",
"production_plan_qty",
"requested_qty",
"column_break_69",
"work_order_qty",
"delivered_qty",
@@ -1010,13 +1011,21 @@
"fieldtype": "Float",
"label": "Finished Good Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted"
},
{
"fieldname": "requested_qty",
"fieldtype": "Float",
"label": "Requested Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-20 16:39:00.200328",
"modified": "2026-02-21 16:39:00.200328",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -80,6 +80,7 @@ class SalesOrderItem(Document):
quotation_item: DF.Data | None
rate: DF.Currency
rate_with_margin: DF.Currency
requested_qty: DF.Float
reserve_stock: DF.Check
returned_qty: DF.Float
stock_qty: DF.Float

View File

@@ -329,7 +329,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-12 10:38:34.605126",
"modified": "2026-02-27 00:47:46.003305",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -166,6 +166,7 @@
"default": "0",
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Group"
},
{
@@ -187,7 +188,6 @@
"fieldname": "parent_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Parent Company",
"options": "Company"
},
@@ -245,6 +245,7 @@
"fieldname": "default_currency",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Default Currency",
"options": "Currency",
"reqd": 1
@@ -468,6 +469,7 @@
"default": "1",
"fieldname": "enable_perpetual_inventory",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enable Perpetual Inventory"
},
{
@@ -940,7 +942,6 @@
"description": "Accounting entries are frozen up to this date. Only users with the specified role can create or modify entries before this date.",
"fieldname": "accounts_frozen_till_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Accounts Frozen Till Date"
},
{
@@ -955,7 +956,6 @@
{
"fieldname": "role_allowed_for_frozen_entries",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Roles Allowed to Set and Edit Frozen Account Entries",
"options": "Role"
},
@@ -970,7 +970,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-11-16 16:51:27.624096",
"modified": "2026-03-09 17:15:33.819426",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -183,13 +183,11 @@ class Employee(NestedSet):
throw(_("Please enter relieving date."))
def validate_for_enabled_user_id(self, enabled):
if self.status != "Active":
return
if enabled is None:
frappe.throw(_("User {0} does not exist").format(self.user_id))
if enabled == 0:
frappe.throw(_("User {0} is disabled").format(self.user_id), EmployeeUserDisabledError)
if self.status != "Active" and enabled or self.status == "Active" and enabled == 0:
frappe.set_value("User", self.user_id, "enabled", not enabled)
def validate_duplicate_user_id(self):
Employee = frappe.qb.DocType("Employee")

View File

@@ -0,0 +1,47 @@
{
"allow_roles": [
{
"role": "System Manager"
},
{
"role": "Sales Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Manufacturing Manager"
},
{
"role": "Stock Manager"
}
],
"creation": "2026-02-24 18:03:53.158438",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 0,
"is_complete": 0,
"modified": "2026-02-24 18:07:36.808560",
"modified_by": "Administrator",
"module": "Setup",
"name": "Organization Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Setup Company"
},
{
"step": "Invite Users"
},
{
"step": "Setup Email Account"
},
{
"step": "Setup Role Permissions"
},
{
"step": "Review System Settings"
}
],
"title": "Setup Organization"
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Invite Users",
"creation": "2026-02-24 18:04:21.585575",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-24 18:04:21.585575",
"modified_by": "Administrator",
"name": "Invite Users",
"owner": "Administrator",
"reference_document": "User",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Invite Users",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Update Settings",
"action_label": "Review System Settings",
"creation": "2026-02-24 18:06:56.781335",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2026-02-24 18:06:56.781335",
"modified_by": "Administrator",
"name": "Review System Settings",
"owner": "Administrator",
"reference_document": "System Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review System Settings",
"validate_action": 0,
"value_to_validate": ""
}

View File

@@ -0,0 +1,21 @@
{
"action": "Go to Page",
"action_label": "Setup Company",
"creation": "2026-02-20 11:12:50.373049",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 21:10:17.680053",
"modified_by": "Administrator",
"name": "Setup Company",
"owner": "Administrator",
"path": "company",
"reference_document": "Company",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Company",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Setup Email Account",
"creation": "2026-02-24 18:04:39.983155",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-24 18:04:39.983155",
"modified_by": "Administrator",
"name": "Setup Email Account",
"owner": "Administrator",
"reference_document": "Email Account",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Email Account",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Go to Page",
"action_label": "Setup Role Permissions",
"creation": "2026-02-24 18:05:10.485778",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-24 18:05:10.485778",
"modified_by": "Administrator",
"name": "Setup Role Permissions",
"owner": "Administrator",
"path": "permission-manager",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Role Permissions",
"validate_action": 1
}

View File

@@ -221,6 +221,8 @@ def set_defaults_for_tests():
frappe.db.set_default(key, value)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1)
def insert_record(records):
from frappe.desk.page.setup_wizard.setup_wizard import make_records

View File

@@ -246,6 +246,7 @@
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"no_copy": 1,
"oldfieldname": "posting_date",
@@ -971,6 +972,7 @@
{
"fieldname": "per_billed",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Amount Billed",
"no_copy": 1,
"print_hide": 1,
@@ -1061,6 +1063,7 @@
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
@@ -1078,7 +1081,6 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "per_installed",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Installed",
"no_copy": 1,
"oldfieldname": "per_installed",
@@ -1212,6 +1214,7 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "per_returned",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Returned",
"no_copy": 1,
"print_hide": 1,
@@ -1449,7 +1452,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2026-02-10 14:35:08.523130",
"modified": "2026-03-09 17:15:27.932956",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -84,7 +84,25 @@ frappe.ui.form.on("Item", {
}
},
toggle_has_serial_batch_fields(frm) {
let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0;
frm.toggle_display(["serial_no_series", "batch_number_series", "create_new_batch"], !hide_fields);
frm.toggle_enable(["has_serial_no", "has_batch_no"], !hide_fields);
if (hide_fields) {
let description = __(
"To enable the Serial No and Batch No feature, please check the 'Enable Serial / Batch No for Item' checkbox in Stock Settings."
);
frm.set_df_property("has_serial_no", "description", description);
frm.set_df_property("has_batch_no", "description", description);
}
},
refresh: function (frm) {
frm.trigger("toggle_has_serial_batch_fields");
if (frm.doc.is_stock_item) {
frm.add_custom_button(
__("Stock Balance"),

View File

@@ -452,6 +452,7 @@
"fieldname": "batch_number_series",
"fieldtype": "Data",
"label": "Batch Number Series",
"show_description_on_click": 1,
"translatable": 1
},
{
@@ -493,7 +494,8 @@
"description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.",
"fieldname": "serial_no_series",
"fieldtype": "Data",
"label": "Serial Number Series"
"label": "Serial Number Series",
"show_description_on_click": 1
},
{
"collapsible": 1,
@@ -985,7 +987,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2026-02-05 17:20:35.605734",
"modified": "2026-03-05 16:29:31.653447",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -159,6 +159,7 @@
"default": "Today",
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Transaction Date",
"no_copy": 1,
"oldfieldname": "transaction_date",
@@ -282,7 +283,6 @@
{
"fieldname": "set_warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Set Target Warehouse",
"options": "Warehouse"
},
@@ -377,7 +377,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2026-01-21 12:48:40.792323",
"modified": "2026-03-09 17:15:30.124509",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -90,7 +90,7 @@ class MaterialRequest(BuyingController):
{
"source_dt": "Material Request Item",
"target_dt": "Sales Order Item",
"target_field": "ordered_qty",
"target_field": "requested_qty",
"target_parent_dt": "Sales Order",
"target_parent_field": "",
"join_field": "sales_order_item",
@@ -280,6 +280,8 @@ class MaterialRequest(BuyingController):
def on_cancel(self):
self.update_requested_qty_in_production_plan(cancel=True)
self.update_requested_qty()
if self.material_request_type == "Purchase":
self.update_prevdoc_status()
def get_mr_items_ordered_qty(self, mr_items):
mr_items_ordered_qty = {}

View File

@@ -119,6 +119,8 @@ frappe.ui.form.on("Pick List", {
refresh: (frm) => {
frm.trigger("add_get_items_button");
frm.trigger("update_warehouse_property");
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus === 1) {
const status_completed = frm.doc.status === "Completed";

View File

@@ -938,6 +938,7 @@
{
"fieldname": "per_billed",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Amount Billed",
"no_copy": 1,
"print_hide": 1,
@@ -1302,7 +1303,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2026-02-23 16:56:41.075091",
"modified": "2026-03-09 17:15:28.602690",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -107,6 +107,7 @@ class SerialandBatchBundle(Document):
self.autoname()
def validate(self):
self.validate_allow_to_set_serial_batch()
if self.docstatus == 1 and self.voucher_detail_no:
self.validate_voucher_detail_no()
@@ -143,6 +144,15 @@ class SerialandBatchBundle(Document):
self.calculate_qty_and_amount()
self.set_child_details()
def validate_allow_to_set_serial_batch(self):
if not frappe.db.get_single_value("Stock Settings", "enable_serial_and_batch_no_for_item"):
frappe.throw(
_(
"Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item."
).format(get_link_to_form("Stock Settings", "Stock Settings")),
title=_("Serial and Batch No for Item Disabled"),
)
def validate_serial_no_status(self):
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
invalid_serial_nos = frappe.get_all(

View File

@@ -245,6 +245,7 @@ frappe.ui.form.on("Stock Entry", {
refresh: function (frm) {
frm.trigger("get_items_from_transit_entry");
frm.trigger("toggle_warehouse_fields");
erpnext.toggle_serial_batch_fields(frm);
if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) {
frm.trigger("validate_purpose_consumption");
@@ -930,10 +931,6 @@ frappe.ui.form.on("Stock Entry Detail", {
);
},
qty(frm, cdt, cdn) {
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},
conversion_factor(frm, cdt, cdn) {
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},

View File

@@ -76,6 +76,8 @@ frappe.ui.form.on("Stock Reconciliation", {
},
refresh: function (frm) {
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus < 1) {
frm.add_custom_button(__("Fetch Items from Warehouse"), function () {
frm.events.get_items(frm);

View File

@@ -38,6 +38,7 @@
"allow_internal_transfer_at_arms_length_price",
"validate_material_transfer_warehouses",
"serial_and_batch_item_settings_tab",
"enable_serial_and_batch_no_for_item",
"section_break_7",
"allow_existing_serial_no",
"do_not_use_batchwise_valuation",
@@ -48,9 +49,8 @@
"use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_negative_stock_for_batch",
"serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"section_break_gnhq",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"use_naming_series",
"column_break_wslv",
"naming_series_prefix",
@@ -158,6 +158,7 @@
"label": "Convert Item Description to Clean HTML in Transactions"
},
{
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Serial & Batch Item Settings"
@@ -487,11 +488,6 @@
"fieldtype": "Check",
"label": "Auto Reserve Stock"
},
{
"fieldname": "serial_and_batch_bundle_section",
"fieldtype": "Section Break",
"label": "Serial and Batch Bundle"
},
{
"default": "0",
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
@@ -499,6 +495,7 @@
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
},
{
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_gnhq",
"fieldtype": "Section Break"
},
@@ -554,6 +551,11 @@
"fieldname": "allow_negative_stock_for_batch",
"fieldtype": "Check",
"label": "Allow Negative Stock for Batch"
},
{
"fieldname": "enable_serial_and_batch_no_for_item",
"fieldtype": "Check",
"label": "Enable Serial / Batch No for Item"
}
],
"hide_toolbar": 1,
@@ -562,7 +564,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-25 09:56:34.105949",
"modified": "2026-02-25 10:56:34.105949",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -47,6 +47,7 @@ class StockSettings(Document):
disable_serial_no_and_batch_selector: DF.Check
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
do_not_use_batchwise_valuation: DF.Check
enable_serial_and_batch_no_for_item: DF.Check
enable_stock_reservation: DF.Check
item_group: DF.Link | None
item_naming_by: DF.Literal["Item Code", "Naming Series"]
@@ -82,6 +83,7 @@ class StockSettings(Document):
"default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input",
"use_serial_batch_fields",
"enable_serial_and_batch_no_for_item",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
]:
frappe.db.set_default(key, self.get(key, ""))
@@ -104,6 +106,7 @@ class StockSettings(Document):
)
self.validate_warehouses()
self.validate_serial_and_batch_no_settings()
self.cant_change_valuation_method()
self.validate_clean_description_html()
self.validate_pending_reposts()
@@ -112,6 +115,25 @@ class StockSettings(Document):
self.change_precision_for_for_sales()
self.change_precision_for_purchase()
def validate_serial_and_batch_no_settings(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save:
return
if doc_before_save.enable_serial_and_batch_no_for_item == self.enable_serial_and_batch_no_for_item:
return
if (
doc_before_save.enable_serial_and_batch_no_for_item
and not self.enable_serial_and_batch_no_for_item
):
if frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"):
frappe.throw(
_(
"Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch."
)
)
def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
for field in warehouse_fields:

View File

@@ -352,7 +352,7 @@ class SerialBatchBundle:
"Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0}
)
> 0
):
) and not self.sle.is_cancelled:
frappe.throw(
_("Serial and Batch Bundle {0} is not submitted").format(
bold(self.sle.serial_and_batch_bundle)

View File

@@ -30,6 +30,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
refresh: (frm) => {
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus === 1) {
frm.add_custom_button(
__("Stock Ledger"),

View File

@@ -8,7 +8,7 @@
<form action="/search_help" style="display: flex;">
<input name='q' class='form-control' type='text'
style='max-width: 400px; display: inline-block; margin-right: 10px;'
value='{{ frappe.form_dict.q or ''}}'
value='{{ (frappe.form_dict.q or '') | e }}'
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
<input type='submit'
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">

View File

@@ -0,0 +1,102 @@
{
"app": "erpnext",
"creation": "2026-02-24 17:39:43.793115",
"docstatus": 0,
"doctype": "Workspace Sidebar",
"header_icon": "organization",
"idx": 1,
"items": [
{
"child": 0,
"collapsible": 1,
"icon": "organization",
"indent": 0,
"keep_closed": 0,
"label": "Company",
"link_to": "Company",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "book-text",
"indent": 0,
"keep_closed": 0,
"label": "Letter Head",
"link_to": "Letter Head",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "file-user",
"indent": 0,
"keep_closed": 0,
"label": "Department",
"link_to": "Department",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "book-user",
"indent": 0,
"keep_closed": 0,
"label": "Branch",
"link_to": "Branch",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "users",
"indent": 0,
"keep_closed": 0,
"label": "User",
"link_to": "User",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "user-round-check",
"indent": 0,
"keep_closed": 0,
"label": "Role Permissions",
"link_to": "permission-manager",
"link_type": "Page",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "mail",
"indent": 0,
"keep_closed": 0,
"label": "Email Account",
"link_to": "Email Account",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2026-02-24 18:08:00.796746",
"modified_by": "Administrator",
"module": "Setup",
"module_onboarding": "Organization Onboarding",
"name": "Organization",
"owner": "Administrator",
"standard": 1,
"title": "Organization"
}