diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0196b2b6189..80d75bfddc3 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -189,6 +189,9 @@ class POSInvoice(SalesInvoice): super().__init__(*args, **kwargs) def validate(self): + if not self.customer: + frappe.throw(_("Please select Customer first")) + if not cint(self.is_pos): frappe.throw( _("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment"))) @@ -345,14 +348,14 @@ class POSInvoice(SalesInvoice): ): return - from erpnext.stock.stock_ledger import is_negative_stock_allowed - for d in self.get("items"): if not d.serial_and_batch_bundle: - if is_negative_stock_allowed(item_code=d.item_code): - return + available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability( + d.item_code, d.warehouse + ) - available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) + if is_negative_stock_allowed: + continue item_code, warehouse, _qty = ( frappe.bold(d.item_code), @@ -760,20 +763,22 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): + from erpnext.stock.stock_ledger import is_negative_stock_allowed + if frappe.db.get_value("Item", item_code, "is_stock_item"): is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - return bin_qty - pos_sales_qty, is_stock_item + return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code) else: is_stock_item = True if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): - return get_bundle_availability(item_code, warehouse), is_stock_item + return get_bundle_availability(item_code, warehouse), is_stock_item, False else: is_stock_item = False # Is a service item or non_stock item - return 0, is_stock_item + return 0, is_stock_item, False def get_bundle_availability(bundle_item_code, warehouse): diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 7c3ced847ad..9af8c93a52e 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_account", 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 ac9d5bfbd01..18a85af95be 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index c5bfe4de0ff..b052d50838d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 1fb93846735..9ca930bd829 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) +from erpnext.accounts.report.financial_statements import get_cost_centers_with_children from erpnext.accounts.utils import ( build_qb_match_conditions, get_advance_payment_doctypes, @@ -994,11 +995,7 @@ class ReceivablePayableReport: self.add_accounting_dimensions_filters() def get_cost_center_conditions(self): - lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) - cost_center_list = [ - center.name - for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) - ] + cost_center_list = get_cost_centers_with_children(self.filters.cost_center) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) def add_common_filters(self): 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 ae0bddaa766..1ac2b27ca71 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py index 5084b6c1651..48047c81944 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.py +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -174,7 +174,7 @@ def add_solvency_ratios( return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")} for year in years: - profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year)) + profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year)) share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year)) debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision) @@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale avg_data = {} for d in ["Receivable", "Payable", "Stock"]: - avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters) + avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters) avg_debtors, avg_creditors, avg_stock = ( avg_data.get("receivable"), diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 7c6c809b939..100dcd46c85 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -566,6 +566,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot else: update_value_in_dict(consolidated_gle, key, gle) + if filters.get("include_dimensions"): + dimensions = [*accounting_dimensions, "cost_center", "project"] + + for dimension in dimensions: + if val := gle.get(dimension): + gle[dimension] = _(val) + for value in consolidated_gle.values(): update_value_in_dict(totals, "total", value) update_value_in_dict(totals, "closing", value) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 81dba55d609..78fc08614f2 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -4,7 +4,9 @@ import frappe from frappe import _ -from frappe.utils import getdate +from frappe.utils import flt, getdate + +from erpnext.accounts.utils import get_currency_precision def execute(filters=None): @@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ party_map = get_party_pan_map(filters.get("party_type")) tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) + precision = get_currency_precision() out = [] entries = {} @@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date) - if net_total_map.get((voucher_type, name)): + + values = net_total_map.get((voucher_type, name)) + + if values: if voucher_type == "Journal Entry" and tax_amount and rate: - # back calcalute total amount from rate and tax_amount - base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0]) + # back calculate total amount from rate and tax_amount + base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0]) total_amount = grand_total = base_total - elif voucher_type == "Purchase Invoice": - total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get( - (voucher_type, name) - ) + else: - total_amount, grand_total, base_total = net_total_map.get((voucher_type, name)) + if tax_amount and rate: + # back calculate total amount from rate and tax_amount + total_amount = flt((tax_amount * 100) / rate, precision=precision) + else: + total_amount = values[0] + + grand_total = values[1] + base_total = values[2] + + if voucher_type == "Purchase Invoice": + bill_no = values[3] + bill_date = values[4] else: total_amount += entry.credit diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 2f701900cf7..8c08c35b3e1 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Cost Center", - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "project", label: __("Project"), - fieldtype: "Link", + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Project", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, options: "Project", }, { diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 3f75f1e2b4c..2ea76c82975 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.report.financial_statements import ( filter_accounts, filter_out_zero_value_rows, + get_cost_centers_with_children, set_gl_entries_by_account, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency @@ -103,10 +104,6 @@ def get_data(filters): opening_balances = get_opening_balances(filters, ignore_is_opening) - # add filter inside list so that the query in financial_statements.py doesn't break - if filters.project: - filters.project = [filters.project] - set_gl_entries_by_account( filters.company, filters.from_date, @@ -270,18 +267,12 @@ def get_opening_balance( opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher") if filters.cost_center: - lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) - cost_center = frappe.qb.DocType("Cost Center") opening_balance = opening_balance.where( - closing_balance.cost_center.isin( - frappe.qb.from_(cost_center) - .select("name") - .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) - ) + closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center"))) ) if filters.project: - opening_balance = opening_balance.where(closing_balance.project == filters.project) + opening_balance = opening_balance.where(closing_balance.project.isin(filters.project)) if frappe.db.count("Finance Book"): if filters.get("include_default_book_entries"): diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 487f67669ff..2e15dd77b62 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -371,7 +371,6 @@ "label": "Other Details" }, { - "allow_on_submit": 1, "default": "Draft", "fieldname": "status", "fieldtype": "Select", @@ -379,7 +378,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress", + "options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress", "read_only": 1 }, { @@ -597,7 +596,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2025-10-23 22:43:33.634452", + "modified": "2025-11-17 18:01:51.417942", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9f8895b6e7e..6ef5a8643aa 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -103,6 +103,7 @@ class Asset(AccountsController): status: DF.Literal[ "Draft", "Submitted", + "Cancelled", "Partially Depreciated", "Fully Depreciated", "Sold", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 16f3da9b988..9b6d878378b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -139,7 +139,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Asset", - "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]", + "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]", "options": "Asset", "reqd": 1 }, @@ -250,7 +250,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-07-29 15:14:34.044564", + "modified": "2025-11-17 18:35:54.575265", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 358945edf87..77e191873a5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -60,6 +60,17 @@ class AssetRepair(AccountsController): if self.get("stock_items"): self.set_stock_items_cost() self.calculate_total_repair_cost() + self.validate_purchase_invoice_status() + + def validate_purchase_invoice_status(self): + if self.purchase_invoice: + docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus") + if docstatus == 0: + frappe.throw( + _("{0} is still in Draft. Please submit it before saving the Asset Repair.").format( + get_link_to_form("Purchase Invoice", self.purchase_invoice) + ) + ) def validate_asset(self): if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e4e2ee29d9b..d74ea55450a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -95,6 +95,7 @@ class SellingController(StockController): # set contact and address details for customer, if they are not mentioned self.set_missing_lead_customer_details(for_validate=for_validate) self.set_price_list_and_item_details(for_validate=for_validate) + self.set_company_contact_person() def set_missing_lead_customer_details(self, for_validate=False): customer, lead = None, None @@ -137,6 +138,7 @@ class SellingController(StockController): lead, posting_date=self.get("transaction_date") or self.get("posting_date"), company=self.company, + doctype=self.doctype, ) ) @@ -149,6 +151,13 @@ class SellingController(StockController): self.set_price_list_currency("Selling") self.set_missing_item_details(for_validate=for_validate) + def set_company_contact_person(self): + """Set the Company's Default Sales Contact as Company Contact Person.""" + if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person: + self.company_contact_person = frappe.get_cached_value( + "Company", self.company, "default_sales_contact" + ) + def remove_shipping_charge(self): if self.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 15c2760332e..0e94cfd95de 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -93,6 +93,7 @@ status_map = { ["Draft", None], ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], ["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"], + ["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], [ "Completed", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index cf9a7f02f9d..f0f492191fb 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -432,7 +432,7 @@ def _set_missing_values(source, target): @frappe.whitelist() -def get_lead_details(lead, posting_date=None, company=None): +def get_lead_details(lead, posting_date=None, company=None, doctype=None): if not lead: return {} @@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead", company=company) + set_address_details(out, lead, "Lead", doctype=doctype, company=company) taxes_and_charges = set_taxes( None, diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 175c5818c43..ab8586aaef1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -548,12 +548,14 @@ { "fieldname": "process_loss_percentage", "fieldtype": "Percent", - "label": "% Process Loss" + "label": "% Process Loss", + "non_negative": 1 }, { "fieldname": "process_loss_qty", "fieldtype": "Float", "label": "Process Loss Qty", + "non_negative": 1, "read_only": 1 }, { @@ -591,7 +593,6 @@ }, { "default": "0", - "depends_on": "eval:doc.track_semi_finished_goods === 0", "fieldname": "fg_based_operating_cost", "fieldtype": "Check", "label": "Finished Goods based Operating Cost" @@ -640,7 +641,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2025-10-29 17:43:12.966753", + "modified": "2025-11-19 16:17:15.925156", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 22264fb0e92..89f90e1658c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -465,7 +465,7 @@ class BOM(WebsiteGenerator): ) ) - def get_rm_rate(self, arg): + def get_rm_rate(self, arg, notify=True): """Get raw material rate as per selected method, if bom exists takes bom cost""" rate = 0 if not self.rm_cost_as_per: @@ -491,7 +491,7 @@ class BOM(WebsiteGenerator): ), alert=True, ) - else: + elif notify: frappe.msgprint( _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True, @@ -796,7 +796,8 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, "sourced_by_supplier": d.sourced_by_supplier, - } + }, + notify=False, ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 9d3c646598b..69d0cc8fbd8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", { return doc.status === "Complete" ? "green" : "orange"; } }); + + frm.set_query("employee", () => { + return { + filters: { + company: frm.doc.company, + status: "Active", + }, + }; + }); }, refresh: function (frm) { diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index f9eddcf2889..e6f257d39b8 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -79,18 +79,35 @@ erpnext.financial_statements = { }, open_general_ledger: function (data) { if (!data.account && !data.accounts) return; - let project = $.grep(frappe.query_report.filters, function (e) { + let filters = frappe.query_report.filters; + + let project = $.grep(filters, function (e) { return e.df.fieldname == "project"; }); + let cost_center = $.grep(filters, function (e) { + return e.df.fieldname == "cost_center"; + }); + frappe.route_options = { account: data.account || data.accounts, company: frappe.query_report.get_filter_value("company"), from_date: data.from_date || data.year_start_date, to_date: data.to_date || data.year_end_date, - project: project && project.length > 0 ? project[0].$input.val() : "", + project: project && project.length > 0 ? project[0].get_value() : "", + cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "", }; + filters.forEach((f) => { + if (f.df.fieldtype == "MultiSelectList") { + if (f.df.fieldname in frappe.route_options) return; + let value = f.get_value(); + if (value && value.length > 0) { + frappe.route_options[f.df.fieldname] = value; + } + } + }); + let report = "General Ledger"; if (["Payable", "Receivable"].includes(data.account_type)) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2b6d41c2d54..5942b34158d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -420,25 +420,36 @@ $.extend(erpnext.utils, { if (!frappe.boot.setup_complete) { return; } + const today = frappe.datetime.get_today(); if (!date) { - date = frappe.datetime.get_today(); + date = today; } let fiscal_year = ""; - frappe.call({ - method: "erpnext.accounts.utils.get_fiscal_year", - args: { - date: date, - boolean: boolean, - }, - async: false, - callback: function (r) { - if (r.message) { - if (with_dates) fiscal_year = r.message; - else fiscal_year = r.message[0]; - } - }, - }); + if ( + frappe.boot.current_fiscal_year && + date >= frappe.boot.current_fiscal_year[1] && + date <= frappe.boot.current_fiscal_year[2] + ) { + if (with_dates) fiscal_year = frappe.boot.current_fiscal_year; + else fiscal_year = frappe.boot.current_fiscal_year[0]; + } else if (today != date) { + frappe.call({ + method: "erpnext.accounts.utils.get_fiscal_year", + type: "GET", // make it cacheable + args: { + date: date, + boolean: boolean, + }, + async: false, + callback: function (r) { + if (r.message) { + if (with_dates) fiscal_year = r.message; + else fiscal_year = r.message[0]; + } + }, + }); + } return fiscal_year; }, diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index bf4ef8666cd..7e2271dc38f 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -115,6 +115,10 @@ erpnext.sales_common = { company() { super.company(); this.set_default_company_address(); + if (!this.is_onload) { + // we don't want to override the mapped contact from prevdoc + this.set_default_company_contact_person(); + } } set_default_company_address() { @@ -139,6 +143,24 @@ erpnext.sales_common = { } } + set_default_company_contact_person() { + if (!frappe.meta.has_field(this.frm.doc.doctype, "company_contact_person")) { + return; + } + + if (this.frm.doc.company) { + frappe.db + .get_value("Company", this.frm.doc.company, "default_sales_contact") + .then((r) => { + if (r.message?.default_sales_contact) { + this.frm.set_value("company_contact_person", r.message.default_sales_contact); + } else { + this.frm.set_value("company_contact_person", ""); + } + }); + } + } + customer() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function () { diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index da8476d6b1f..f0061c016bd 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -252,6 +252,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. lead: this.frm.doc.party_name, posting_date: this.frm.doc.transaction_date, company: this.frm.doc.company, + doctype: this.frm.doc.doctype, }, callback: function (r) { if (r.message) { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index d8de762dcd3..df488dafeb0 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -55,7 +55,7 @@ def search_by_term(search_term, warehouse, price_list): } ) - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item, is_negative_stock_allowed = get_stock_availability(item_code, warehouse) item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) item.update({"actual_qty": item_stock_qty}) @@ -198,7 +198,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te current_date = frappe.utils.today() for item in items_data: - item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) + item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse) item_prices = frappe.get_all( "Item Price", diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5e3218a67a2..6506ba047d0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -759,12 +759,16 @@ erpnext.PointOfSale.Controller = class { const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; const available_qty = resp[0]; const is_stock_item = resp[1]; + const is_negative_stock_allowed = resp[2]; frappe.dom.unfreeze(); const bold_uom = item_row.stock_uom.bold(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold(); + + if (is_negative_stock_allowed) return; + if (!(available_qty > 0)) { if (is_stock_item) { frappe.model.clear_doc(item_row.doctype, item_row.name); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index f736769b915..032ec707330 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -37,6 +37,13 @@ frappe.ui.form.on("Company", { return { filters: { selling: 1 } }; }); + frm.set_query("default_sales_contact", function (doc) { + return { + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { link_doctype: "Company", link_name: doc.name }, + }; + }); + frm.set_query("default_buying_terms", function () { return { filters: { buying: 1 } }; }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 8c4f85ff19f..fc6533a1e89 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -103,6 +103,7 @@ "total_monthly_sales", "column_break_goals", "default_selling_terms", + "default_sales_contact", "default_warehouse_for_sales_return", "credit_limit", "transactions_annual_history", @@ -851,6 +852,12 @@ "fieldtype": "Select", "label": "Reconciliation Takes Effect On", "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" + }, + { + "fieldname": "default_sales_contact", + "fieldtype": "Link", + "label": "Default Sales Contact", + "options": "Contact" } ], "icon": "fa fa-building", @@ -858,7 +865,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2025-08-25 18:34:03.602046", + "modified": "2025-11-16 16:51:27.624096", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index f9978099ed5..50fe87ef654 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -66,6 +66,7 @@ class Company(NestedSet): default_payable_account: DF.Link | None default_provisional_account: DF.Link | None default_receivable_account: DF.Link | None + default_sales_contact: DF.Link | None default_selling_terms: DF.Link | None default_warehouse_for_sales_return: DF.Link | None depreciation_cost_center: DF.Link | None diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 03cbd99f5c4..b17b6b49b6b 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -3,8 +3,11 @@ import frappe +from frappe.defaults import get_user_default from frappe.utils import cint +from erpnext.accounts.utils import get_fiscal_years + def boot_session(bootinfo): """boot session - send website info if guest""" @@ -53,6 +56,11 @@ def boot_session(bootinfo): ) party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") + fiscal_year = get_fiscal_years( + frappe.utils.nowdate(), company=get_user_default("company"), boolean=True + ) + if fiscal_year: + bootinfo.current_fiscal_year = fiscal_year[0] bootinfo.party_account_types = frappe._dict(party_account_types) bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company") diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 5445e1b88b0..dc693890cd7 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -55,6 +55,11 @@ class ItemPrice(Document): if not frappe.db.exists("Item", self.item_code): frappe.throw(_("Item {0} not found.").format(self.item_code)) + if self.uom and not frappe.db.exists( + "UOM Conversion Detail", {"parenttype": "Item", "parent": self.item_code, "uom": self.uom} + ): + frappe.throw(_("UOM {0} not found in Item {1}").format(self.uom, self.item_code)) + def update_price_list_details(self): if self.price_list: price_list_details = frappe.db.get_value( diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 243ba7adac8..f295f33853f 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -332,6 +332,9 @@ frappe.ui.form.on("Material Request", { label: __("For Warehouse"), options: "Warehouse", reqd: 1, + get_query: function () { + return { filters: { company: frm.doc.company } }; + }, }, { fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 }, { diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 5a4f3e7722d..65bec2d7ea3 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -108,7 +108,12 @@ def get_indexed_packed_items_table(doc): """ indexed_table = {} for packed_item in doc.get("packed_items"): - key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) + key = ( + packed_item.parent_item, + packed_item.item_code, + packed_item.idx if doc.is_new() else packed_item.parent_detail_docname, + ) + indexed_table[key] = packed_item return indexed_table @@ -169,7 +174,11 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re exists, pi_row = False, {} # check if row already exists in packed items table - key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) + key = ( + main_item_row.item_code, + packing_item.item_code, + main_item_row.idx if doc.is_new() else main_item_row.name, + ) if packed_items_table.get(key): pi_row, exists = packed_items_table.get(key), True diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 603f4a121d1..f220ed3736e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -893,7 +893,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1300,7 +1300,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2025-08-06 16:41:02.690658", + "modified": "2025-11-12 19:53:48.173096", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c4f878fe85d..53aad87e2da 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -117,7 +117,15 @@ class PurchaseReceipt(BuyingController): shipping_address_display: DF.SmallText | None shipping_rule: DF.Link | None status: DF.Literal[ - "", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed" + "", + "Draft", + "Partly Billed", + "To Bill", + "Completed", + "Return", + "Return Issued", + "Cancelled", + "Closed", ] subcontracting_receipt: DF.Link | None supplied_items: DF.Table[PurchaseReceiptItemSupplied] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index b1b0a962246..628b4628f79 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -18,6 +18,9 @@ def get_data(): "Purchase Order": ["items", "purchase_order"], "Project": ["items", "project"], }, + "internal_and_external_links": { + "Purchase Invoice": ["items", "purchase_invoice"], + }, "transactions": [ { "label": _("Related"), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index d70b357d731..30562e23de8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -11,7 +11,7 @@ frappe.listview_settings["Purchase Receipt"] = { "currency", ], get_indicator: function (doc) { - if (cint(doc.is_return) == 1) { + if (cint(doc.is_return) == 1 && doc.status == "Return") { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 383421f3c67..959c58b2380 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -455,6 +455,7 @@ class TestPurchaseReceipt(FrappeTestCase): # Check if Original PR updated self.assertEqual(pr.items[0].returned_qty, 2) self.assertEqual(pr.per_returned, 40) + self.assertEqual(returned.status, "Return") from erpnext.controllers.sales_and_purchase_return import make_return_doc @@ -2128,7 +2129,7 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.items[0].stock_qty = 0.0 return_pr.submit() - self.assertEqual(return_pr.status, "To Bill") + self.assertEqual(return_pr.status, "Return") pi = make_purchase_invoice(return_pr.name) pi.submit() 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 d9071c406a2..3e8df4dd8e5 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 @@ -1347,7 +1347,36 @@ class SerialandBatchBundle(Document): if self.voucher_type == "POS Invoice": return - if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1: + child_doctype = self.voucher_type + " Item" + mapper = { + "Asset Capitalization": "Asset Capitalization Stock Item", + "Asset Repair": "Asset Repair Consumed Item", + "Stock Entry": "Stock Entry Detail", + }.get(self.voucher_type) + + if mapper: + child_doctype = mapper + + if self.voucher_type == "Delivery Note" and not frappe.db.exists( + "Delivery Note Item", self.voucher_detail_no + ): + child_doctype = "Packed Item" + + elif self.voucher_type == "Sales Invoice" and not frappe.db.exists( + "Sales Invoice Item", self.voucher_detail_no + ): + child_doctype = "Packed Item" + + elif self.voucher_type == "Subcontracting Receipt" and not frappe.db.exists( + "Subcontracting Receipt Item", self.voucher_detail_no + ): + child_doctype = "Subcontracting Receipt Supplied Item" + + if ( + frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1 + and self.voucher_detail_no + and frappe.db.exists(child_doctype, self.voucher_detail_no) + ): msg = f"""The {self.voucher_type} {bold(self.voucher_no)} is in submitted state, please cancel it first""" frappe.throw(_(msg)) @@ -2297,7 +2326,11 @@ def get_auto_batch_nos(kwargs): 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) + + sre_reserved_batches = frappe._dict() + if not kwargs.ignore_reserved_stock: + sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + picked_batches = frappe._dict() if kwargs.get("is_pick_list"): picked_batches = get_picked_batches(kwargs) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a41a529ff2c..6a6bb4c95b4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -951,12 +951,10 @@ frappe.ui.form.on("Stock Entry Detail", { no_batch_serial_number_value = true; } - if ( - no_batch_serial_number_value && - !frappe.flags.hide_serial_batch_dialog && - !frappe.flags.dialog_set - ) { - frappe.flags.dialog_set = true; + if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) { + if (!frappe.flags.dialog_set) { + frappe.flags.dialog_set = true; + } erpnext.stock.select_batch_and_serial_no(frm, d); } else { frappe.flags.dialog_set = false; @@ -1338,8 +1336,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]); } - if (!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; - if (!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; + if (this.frm.doc.from_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; + if (this.frm.doc.to_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; if (cint(frappe.user_defaults?.use_serial_batch_fields)) { frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 76c57ae873d..9664ed67498 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1136,7 +1136,9 @@ class StockEntry(StockController): "qty": row.transfer_qty * -1, } ).update_serial_and_batch_entries() - elif not row.serial_and_batch_bundle: + elif not row.serial_and_batch_bundle and frappe.get_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" + ): bundle_doc = SerialBatchCreation( { "item_code": row.item_code, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 937b130cf7b..cc4b08b50f4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -979,6 +979,7 @@ class StockReconciliation(StockController): is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") if is_customer_item and d.valuation_rate: d.valuation_rate = 0.0 + d.allow_zero_valuation_rate = 1 changed_any_values = True if changed_any_values: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index afbfcdd6062..61c32d09467 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -45,6 +45,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): def test_reco_for_moving_average(self): self._test_reco_sle_gle("Moving Average") + @change_settings("Stock Settings", {"allow_negative_stock": 1}) def _test_reco_sle_gle(self, valuation_method): item_code = self.make_item(properties={"valuation_method": valuation_method}).name diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 4d3a87c70aa..e61591694e0 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -185,12 +185,10 @@ }, { "default": "0", - "depends_on": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate", "fieldtype": "Check", "label": "Allow Zero Valuation Rate", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "barcode", @@ -256,7 +254,7 @@ ], "istable": 1, "links": [], - "modified": "2025-03-12 16:34:51.326821", + "modified": "2025-11-20 15:27:13.868179", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -267,4 +265,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index ddbf0b2dc25..942c7f482ae 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -678,6 +678,36 @@ class TestStockReservationEntry(FrappeTestCase): # Test - 1: ValidationError should be thrown as the inwarded stock is reserved. self.assertRaises(frappe.ValidationError, se.cancel) + @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) + def test_reserved_stock_validation_for_batch_item(self): + item_properties = { + "is_stock_item": 1, + "valuation_rate": 100, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SRBV-.#####.", + } + sr_item = make_item(item_code="Test Reserve Item", properties=item_properties) + # inward 100 qty of stock + create_material_receipt(items={sr_item.name: sr_item}, warehouse=self.warehouse, qty=100) + + # reserve 80 qty from sales order + so = make_sales_order(item_code=sr_item.name, warehouse=self.warehouse, qty=80) + so.create_stock_reservation_entries() + + # create a material issue entry including the reserved qty 10 + se = make_stock_entry( + item_code=sr_item.name, + qty=30, + from_warehouse=self.warehouse, + rate=100, + purpose="Material Issue", + do_not_submit=True, + ) + + # validation for reserved stock should be thrown + self.assertRaises(frappe.ValidationError, se.submit) + def tearDown(self) -> None: cancel_all_stock_reservation_entries() return super().tearDown() diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 4600c2bbaae..a1991dd9c07 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -221,7 +221,9 @@ def get_child_warehouses(warehouse): def get_warehouses_based_on_account(account, company=None): warehouses = [] - for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): + for d in frappe.get_all( + "Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0} + ): if d.is_group: warehouses.extend(get_child_warehouses(d.name)) else: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index db9cf422b5f..2ee85fc2c24 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -259,7 +259,7 @@ class SerialBatchBundle: and not self.sle.serial_and_batch_bundle and self.item_details.has_batch_no == 1 and ( - self.item_details.create_new_batch + (self.item_details.create_new_batch and self.sle.actual_qty > 0) or ( frappe.db.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 47cb41852c2..8b61ca56983 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2240,9 +2240,11 @@ def validate_reserved_stock(kwargs): kwargs.ignore_voucher_nos = [kwargs.voucher_no] if kwargs.serial_no: + kwargs.serial_nos = kwargs.serial_no.split("\n") validate_reserved_serial_nos(kwargs) elif kwargs.batch_no: + kwargs.batch_nos = [kwargs.batch_no] validate_reserved_batch_nos(kwargs) elif kwargs.serial_and_batch_bundle: @@ -2311,6 +2313,7 @@ def validate_reserved_batch_nos(kwargs): "posting_date": kwargs.posting_date, "posting_time": kwargs.posting_time, "ignore_voucher_nos": kwargs.ignore_voucher_nos, + "ignore_reserved_stock": True, } ) )