diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 874e239b9a7..9ee1c62b8c0 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -236,6 +236,10 @@ frappe.treeview_settings["Account"] = { root_company, ]); } else { + const node = treeview.tree.get_selected_node(); + if (node.is_root) { + frappe.throw(__("Cannot create root account.")); + } treeview.new_node(); } }, @@ -254,7 +258,8 @@ frappe.treeview_settings["Account"] = { ].treeview.page.fields_dict.root_company.get_value() || frappe.flags.ignore_root_company_validation) && node.expandable && - !node.hide_add + !node.hide_add && + !node.is_root ); }, click: function () { diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index b9c479b012c..a1282f07717 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -1133,13 +1133,6 @@ def create_pos_invoice(**args): return pos_inv -def make_batch_item(item_name): - from erpnext.stock.doctype.item.test_item import make_item - - if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) - - def set_allow_partial_payment(pos_profile, value): pos_profile.reload() pos_profile.allow_partial_payment = value diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index e46af2657b5..ac9d5bfbd01 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 17ee5e0b323..ae0bddaa766 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index e3f32ab974f..6f3f55b90a2 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -117,6 +117,11 @@ frappe.query_reports["Trial Balance"] = { fieldtype: "Check", default: 1, }, + { + fieldname: "show_account_name_and_number", + label: __("Show Account Name and Number"), + fieldtype: "Check", + }, ], formatter: erpnext.financial_statements.formatter, tree: true, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 587a8ea95cb..36209a94f92 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -32,7 +32,7 @@ value_fields = ( def execute(filters=None): validate_filters(filters) data = get_data(filters) - columns = get_columns() + columns = get_columns(filters) return columns, data @@ -407,6 +407,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): ), } + if filters.get("show_account_name_and_number"): + row["acc_name"] = d.account_name + row["acc_number"] = d.account_number + for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) @@ -427,7 +431,24 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): return data -def get_columns(): +def get_columns(filters): + account_name_number_cols = [] + if filters.get("show_account_name_and_number"): + account_name_number_cols = [ + { + "fieldname": "acc_name", + "label": _("Account Name"), + "fieldtype": "Data", + "width": 250, + }, + { + "fieldname": "acc_number", + "label": _("Account Number"), + "fieldtype": "Data", + "width": 120, + }, + ] + return [ { "fieldname": "account", @@ -436,6 +457,7 @@ def get_columns(): "options": "Account", "width": 300, }, + *account_name_number_cols, { "fieldname": "currency", "label": _("Currency"), diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 835352a7c68..90b7e4710c6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1608,6 +1608,7 @@ class WorkOrder(Document): "docstatus": 1, }, fields=[ + "item_code", "name", "stock_qty", "stock_reserved_qty", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cc92dc8dc33..2eab5d3fa65 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -424,4 +424,5 @@ execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resettin erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13 erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_pos_closing_entry -erpnext.patches.v15_0.set_company_on_pos_inv_merge_log \ No newline at end of file +erpnext.patches.v15_0.set_company_on_pos_inv_merge_log +erpnext.patches.v15_0.rename_price_list_to_buying_price_list \ No newline at end of file diff --git a/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py new file mode 100644 index 00000000000..2b6dbb77b9a --- /dev/null +++ b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Material Request", "price_list"): + rename_field( + "Material Request", + "price_list", + "buying_price_list", + ) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 2162c000221..8b5c8e0fe59 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -582,7 +582,6 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { ignore_pricing_rule: frm.doc.ignore_pricing_rule, doctype: frm.doc.doctype }, - price_list: frm.doc.price_list, }, freeze: true, callback: function(r) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d0a43ca9a97..3e7bc8933c1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -42,6 +42,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate); + cur_frm.cscript.set_gross_profit(item); + cur_frm.cscript.calculate_taxes_and_totals(); + cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); + if (item.item_code && item.rate) { frappe.call({ method: "erpnext.stock.get_item_details.get_item_tax_template", @@ -63,10 +67,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }); } - - cur_frm.cscript.set_gross_profit(item); - cur_frm.cscript.calculate_taxes_and_totals(); - cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); }); frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index d48ad47b100..e93f85383ac 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -35,9 +35,11 @@ class TestBatch(IntegrationTestCase): def make_batch_item(cls, item_name=None): from erpnext.stock.doctype.item.test_item import make_item - if not frappe.db.exists(item_name): + if not frappe.db.exists("Item", item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) + return frappe.get_doc("Item", item_name) + def test_purchase_receipt(self, batch_qty=100): """Test automated batch creation from Purchase Receipt""" self.make_batch_item("ITEM-BATCH-1") diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 10b91b4bf0f..0ba77ec0459 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -43,7 +43,7 @@ frappe.ui.form.on("Material Request", { }; }); - frm.set_query("price_list", () => { + frm.set_query("buying_price_list", () => { return { filters: { buying: 1, @@ -87,7 +87,7 @@ frappe.ui.form.on("Material Request", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.doc.price_list = frappe.defaults.get_default("buying_price_list"); + frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list"); }, company: function (frm) { @@ -269,8 +269,8 @@ frappe.ui.form.on("Material Request", { from_warehouse: item.from_warehouse, warehouse: item.warehouse, doctype: frm.doc.doctype, - buying_price_list: frm.doc.price_list - ? frm.doc.price_list + buying_price_list: frm.doc.buying_price_list + ? frm.doc.buying_price_list : frappe.defaults.get_default("buying_price_list"), currency: frappe.defaults.get_default("Currency"), name: frm.doc.name, diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 76dcd71ecdd..dce68d6ecc6 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -16,7 +16,7 @@ "column_break_2", "transaction_date", "schedule_date", - "price_list", + "buying_price_list", "amended_from", "warehouse_section", "scan_barcode", @@ -354,7 +354,7 @@ "fieldtype": "Column Break" }, { - "fieldname": "price_list", + "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" @@ -364,7 +364,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-07 13:15:28.615984", + "modified": "2025-07-11 21:03:26.588307", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index f78c53c0d1e..8c409fd7e7b 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -163,8 +163,8 @@ class MaterialRequest(BuyingController): self.validate_pp_qty() - if not self.price_list: - self.price_list = frappe.defaults.get_defaults().buying_price_list + if not self.buying_price_list: + self.buying_price_list = frappe.defaults.get_defaults().buying_price_list def validate_pp_qty(self): items_from_pp = [item for item in self.items if item.material_request_plan_item] diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index d5fe895089d..062718236d7 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -349,7 +349,7 @@ def on_doctype_update(): @frappe.whitelist() -def get_items_from_product_bundle(row, price_list): +def get_items_from_product_bundle(row): row, items = ItemDetailsCtx(json.loads(row)), [] bundled_items = get_product_bundle_items(row["item_code"]) @@ -359,7 +359,6 @@ def get_items_from_product_bundle(row, price_list): "item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty), "conversion_rate": 1, - "price_list": price_list, "currency": frappe.defaults.get_defaults().currency, } ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0096ab82580..db88d46806c 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2178,15 +2178,15 @@ def get_reserved_batches_for_sre(kwargs) -> dict: if kwargs.batch_no: if isinstance(kwargs.batch_no, list): - query = query.where(sb_entry.batch_no.isin(kwargs.batch_no)) + query = query.where(sb_entry.batch_no.notin(kwargs.batch_no)) else: - query = query.where(sb_entry.batch_no == kwargs.batch_no) + query = query.where(sb_entry.batch_no != kwargs.batch_no) if kwargs.warehouse: if isinstance(kwargs.warehouse, list): - query = query.where(sre.warehouse.isin(kwargs.warehouse)) + query = query.where(sre.warehouse.notin(kwargs.warehouse)) else: - query = query.where(sre.warehouse == kwargs.warehouse) + query = query.where(sre.warehouse != kwargs.warehouse) if kwargs.ignore_voucher_nos: query = query.where(sre.name.notin(kwargs.ignore_voucher_nos)) @@ -2203,12 +2203,25 @@ def get_reserved_batches_for_sre(kwargs) -> dict: def get_auto_batch_nos(kwargs): + if kwargs.against_sales_order and ( + only_consider_batches := get_batches_to_be_considered(kwargs.against_sales_order) + ): + batches, warehouses = [], [] + for item in only_consider_batches: + batches.append(item.batch_no) + warehouses.append(item.warehouse) + + if batches: + kwargs.batch_no = batches + kwargs.warehouse = warehouses available_batches = get_available_batches(kwargs) qty = flt(kwargs.qty) stock_ledgers_batches = get_stock_ledgers_batches(kwargs) pos_invoice_batches = get_reserved_batches_for_pos(kwargs) sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + kwargs.batch_no = kwargs.warehouse = None + picked_batches = frappe._dict() if kwargs.get("is_pick_list"): picked_batches = get_picked_batches(kwargs) @@ -2238,6 +2251,25 @@ def get_auto_batch_nos(kwargs): return get_qty_based_available_batches(available_batches, qty) +def get_batches_to_be_considered(sales_order_name): + parent = frappe.qb.DocType("Stock Reservation Entry") + child = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(parent) + .join(child) + .on(parent.name == child.parent) + .select(child.batch_no, child.warehouse) + .distinct() + .where( + (parent.docstatus == 1) + & (parent.voucher_no == sales_order_name) + & (child.delivered_qty < child.qty) + ) + ) + return query.run(as_dict=True) + + def filter_zero_near_batches(available_batches, kwargs): kwargs.batch_no = [d.batch_no for d in available_batches] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9072f7b25c2..ba86860277f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -451,7 +451,9 @@ class StockEntry(StockController): additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) + project = frappe.get_doc("Project", self.project) + project.total_consumed_material_cost = amount + project.save() def validate_item(self): stock_items = self.get_stock_items() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 37ff8bef313..71cc5e72481 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1084,17 +1084,14 @@ class StockReconciliation(StockController): } ) - if ( - add_new_sle - and not frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, - "name", - ) - and not row.current_serial_and_batch_bundle + if add_new_sle and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", ): - self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) - row.reload() + if not row.current_serial_and_batch_bundle: + self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) + row.reload() self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation)