diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index ad68352c2a4..c3531420ce1 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", { to: "{to_currency}", }; add_param(frm, r.message, params, result); - } else if (frm.doc.service_provider == "frankfurter.app") { + } else if (frm.doc.service_provider == "frankfurter.dev") { let result = ["rates", "{to_currency}"]; let params = { base: "{from_currency}", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index bd90b8add80..614f4e6d3e5 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -78,7 +78,7 @@ "fieldname": "service_provider", "fieldtype": "Select", "label": "Service Provider", - "options": "frankfurter.app\nexchangerate.host\nCustom", + "options": "frankfurter.dev\nexchangerate.host\nCustom", "reqd": 1 }, { @@ -104,7 +104,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-03-18 08:32:26.895076", + "modified": "2025-11-25 13:03:41.896424", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", @@ -141,8 +141,9 @@ "write": 1 } ], - "sort_field": "modified", + "row_format": "Dynamic", + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 160e791978e..3a12ab30403 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document): disabled: DF.Check req_params: DF.Table[CurrencyExchangeSettingsDetails] result_key: DF.Table[CurrencyExchangeSettingsResult] - service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"] + service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"] url: DF.Data | None use_http: DF.Check # end: auto-generated types @@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document): self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"}) - elif self.service_provider == "frankfurter.app": + elif self.service_provider == "frankfurter.dev": self.set("result_key", []) self.set("req_params", []) @@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document): @frappe.whitelist() def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): - if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]: + if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]: if service_provider == "exchangerate.host": api = "api.exchangerate.host/convert" - elif service_provider == "frankfurter.app": - api = "api.frankfurter.app/{transaction_date}" + elif service_provider == "frankfurter.dev": + api = "api.frankfurter.dev/v1/{transaction_date}" protocol = "https://" if use_http: diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index fde3b9e48dd..c6875b53ed8 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args): if group_condition: conditions += " and " + group_condition - if args.get("transaction_date"): + date = args.get("transaction_date") or frappe.get_value( + args.get("doctype"), args.get("name"), "posting_date", ignore=True + ) + if date: conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" - values["transaction_date"] = args.get("transaction_date") + values["transaction_date"] = date if args.get("doctype") in [ "Quotation", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ad367f3c9cb..9a831598328 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller(); erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 193014d340d..abb5d76b9d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( erpnext.selling.SellingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); this.frm.make_methods = { 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/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 6f5fe349dd2..e56081280ef 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -69,12 +69,18 @@ class PartyLedgerSummaryReport: party_type = self.filters.party_type doctype = qb.DocType(party_type) + + party_details_fields = [ + doctype.name.as_("party"), + f"{scrub(party_type)}_name", + f"{scrub(party_type)}_group", + ] + + if party_type == "Customer": + party_details_fields.append(doctype.territory) + conditions = self.get_party_conditions(doctype) - query = ( - qb.from_(doctype) - .select(doctype.name.as_("party"), f"{scrub(party_type)}_name") - .where(Criterion.all(conditions)) - ) + query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions)) from frappe.desk.reportview import build_match_conditions @@ -153,6 +159,31 @@ class PartyLedgerSummaryReport: credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + }, + ] + else: + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + } + ] + columns += [ { "label": _("Opening Balance"), @@ -213,35 +244,6 @@ class PartyLedgerSummaryReport: }, ] - # Hidden columns for handling 'User Permissions' - if self.filters.party_type == "Customer": - columns += [ - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Link", - "options": "Territory", - "hidden": 1, - }, - { - "label": _("Customer Group"), - "fieldname": "customer_group", - "fieldtype": "Link", - "options": "Customer Group", - "hidden": 1, - }, - ] - else: - columns += [ - { - "label": _("Supplier Group"), - "fieldname": "supplier_group", - "fieldtype": "Link", - "options": "Supplier Group", - "hidden": 1, - } - ] - return columns def get_data(self): 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.js b/erpnext/assets/doctype/asset/asset.js index f28860d325b..d2a86acc837 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", { callback: function (r) { if (!r.message) { $(".primary-action").prop("hidden", true); - $(".form-message").text("Capitalize this asset to confirm"); + $(".form-message").text(__("Capitalize this asset to confirm")); frm.add_custom_button(__("Capitalize Asset"), function () { frm.trigger("create_asset_capitalization"); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 77e191873a5..093af05fd2f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -316,7 +316,6 @@ class AssetRepair(AccountsController): "cost_center": self.cost_center, "posting_date": self.completion_date, "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, "company": self.company, }, item=self, diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 170a68fb394..f9c3da285e1 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -36,6 +36,7 @@ "backflush_raw_materials_of_subcontract_based_on", "column_break_11", "over_transfer_allowance", + "validate_consumed_qty", "section_break_xcug", "auto_create_subcontracting_order", "column_break_izrr", @@ -270,6 +271,14 @@ "label": "Fixed Outgoing Email Account", "link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]", "options": "Email Account" + }, + { + "default": "0", + "depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"", + "description": "Raw materials consumed qty will be validated based on FG BOM required qty", + "fieldname": "validate_consumed_qty", + "fieldtype": "Check", + "label": "Validate Consumed Qty (as per BOM)" } ], "grid_page_length": 50, @@ -278,7 +287,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-08-20 22:13:38.506889", + "modified": "2025-11-20 12:59:09.925862", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 8b83418f6f8..3634f8a9069 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -44,6 +44,7 @@ class BuyingSettings(Document): supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supplier_group: DF.Link | None use_transaction_date_exchange_rate: DF.Check + validate_consumed_qty: DF.Check # end: auto-generated types def validate(self): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index d82a5e18c23..85095e66a57 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( erpnext.buying.BuyingController ) { setup() { + this.setup_accounting_dimension_triggers(); this.frm.custom_make_buttons = { "Purchase Receipt": "Purchase Receipt", "Purchase Invoice": "Purchase Invoice", diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 93fa566dc74..a27244d1528 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", { frm.set_query("supplier_primary_contact", function (doc) { return { - query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary", filters: { supplier: doc.name, + type: "Contact", }, }; }); frm.set_query("supplier_primary_address", function (doc) { return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary", filters: { - link_doctype: "Supplier", - link_name: doc.name, + supplier: doc.name, + type: "Address", }, }; }); diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 3b72953c563..07a2d31166b 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -215,19 +215,25 @@ class Supplier(TransactionBase): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): +def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters): supplier = filters.get("supplier") - contact = frappe.qb.DocType("Contact") + type = filters.get("type") + type_doctype = frappe.qb.DocType(type) dynamic_link = frappe.qb.DocType("Dynamic Link") - return ( - frappe.qb.from_(contact) + query = ( + frappe.qb.from_(type_doctype) .join(dynamic_link) - .on(contact.name == dynamic_link.parent) - .select(contact.name, contact.email_id) + .on(type_doctype.name == dynamic_link.parent) + .select(type_doctype.name) .where( (dynamic_link.link_name == supplier) & (dynamic_link.link_doctype == "Supplier") - & (contact.name.like(f"%{txt}%")) + & (type_doctype.name.like(f"%{txt}%")) ) - ).run(as_dict=False) + ) + + if type == "Contact": + query = query.select(type_doctype.email_id) + + return query.run() 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/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 36341a090dc..bbb5429303b 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -505,7 +505,7 @@ class SubcontractingController(StockController): if item.get("serial_and_batch_bundle"): frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) - def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0): doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] @@ -849,7 +849,7 @@ class SubcontractingController(StockController): if self.doctype == self.subcontract_data.order_doctype or ( self.backflush_based_on == "BOM" or self.is_return ): - for bom_item in self.__get_materials_from_bom( + for bom_item in self._get_materials_from_bom( row.item_code, row.bom, row.get("include_exploded_items") ): qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 04774d4f2a8..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 }, { @@ -639,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..a6fad4ee1aa 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,11 +796,14 @@ 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) - d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) + d.amount = flt( + flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount") + ) d.base_amount = d.amount * flt(self.conversion_rate) d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt( self.quantity, self.precision("quantity") @@ -823,7 +826,10 @@ class BOM(WebsiteGenerator): d.base_rate = flt(d.rate, d.precision("rate")) * flt( self.conversion_rate, self.precision("conversion_rate") ) - d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")) + d.amount = flt( + flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")), + d.precision("amount"), + ) d.base_amount = flt(d.amount, d.precision("amount")) * flt( self.conversion_rate, self.precision("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/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 320db68e037..602dfcc04b0 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -318,7 +318,7 @@ "type": "Link" } ], - "modified": "2024-10-21 14:13:38.777556", + "modified": "2025-11-24 11:11:28.343568", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,7 +336,7 @@ "doc_view": "List", "label": "Learn Manufacturing", "type": "URL", - "url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app" + "url": "https://school.frappe.io/lms/courses/production-planning-and-execution" }, { "color": "Grey", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index be07f9c3120..7a79332e316 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -426,3 +426,4 @@ erpnext.patches.v15_0.set_asset_status_if_not_already_set erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) +erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter diff --git a/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py b/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py new file mode 100644 index 00000000000..68157b1a4ad --- /dev/null +++ b/erpnext/patches/v16_0/update_currency_exchange_settings_for_frankfurter.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + settings = frappe.get_doc("Currency Exchange Settings") + if settings.service_provider != "frankfurter.app": + return + + settings.service_provider = "frankfurter.dev" + settings.set_parameters_and_result() + settings.flags.ignore_validate = True + settings.save() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7b63d78c09b..3abcde36072 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2749,6 +2749,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe ]); } } + + setup_accounting_dimension_triggers() { + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + callback: function (r) { + if (r.message && r.message[0]) { + let dimensions = r.message[0].map((d) => d.fieldname); + dimensions.forEach((dim) => { + // nosemgrep: frappe-semgrep-rules.rules.frappe-cur-frm-usage + cur_frm.cscript[dim] = function (doc, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", dim); + }; + }); + } + }, + }); + } }; erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) { 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/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 598452276cc..f5a2009e551 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", { frm.set_query("customer_primary_contact", function (doc) { return { - query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact", + query: "erpnext.selling.doctype.customer.customer.get_customer_primary", filters: { customer: doc.name, + type: "Contact", }, }; }); + frm.set_query("customer_primary_address", function (doc) { return { + query: "erpnext.selling.doctype.customer.customer.get_customer_primary", filters: { - link_doctype: "Customer", - link_name: doc.name, + customer: doc.name, + type: "Address", }, }; }); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index a04b9c414cf..7c0676c672f 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -610,7 +610,7 @@ "link_fieldname": "party" } ], - "modified": "2025-03-05 10:01:47.885574", + "modified": "2025-11-25 09:35:56.772949", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -696,4 +696,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 9bebfa1e086..449b56de3b4 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -232,7 +232,7 @@ class Customer(TransactionBase): self.update_lead_status() if self.flags.is_new_doc: - self.link_lead_address_and_contact() + self.link_address_and_contact() self.copy_communication() self.update_customer_groups() @@ -272,15 +272,23 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") - def link_lead_address_and_contact(self): - if self.lead_name: - # assign lead address and contact to customer (if already not set) + def link_address_and_contact(self): + linked_documents = { + "Lead": self.lead_name, + "Opportunity": self.opportunity_name, + "Prospect": self.prospect_name, + } + for doctype, docname in linked_documents.items(): + # assign lead, opportunity and prospect address and contact to customer (if already not set) + if not docname: + continue + linked_contacts_and_addresses = frappe.get_all( "Dynamic Link", filters=[ ["parenttype", "in", ["Contact", "Address"]], - ["link_doctype", "=", "Lead"], - ["link_name", "=", self.lead_name], + ["link_doctype", "=", doctype], + ["link_name", "=", docname], ], fields=["parent as name", "parenttype as doctype"], ) @@ -792,21 +800,29 @@ def make_address(args, is_primary_address=1, is_shipping_address=1): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): +def get_customer_primary(doctype, txt, searchfield, start, page_len, filters): customer = filters.get("customer") - - con = qb.DocType("Contact") + type = filters.get("type") + type_doctype = qb.DocType(type) dlink = qb.DocType("Dynamic Link") - return ( - qb.from_(con) + query = ( + qb.from_(type_doctype) .join(dlink) - .on(con.name == dlink.parent) - .select(con.name, con.email_id) - .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) - .run() + .on(type_doctype.name == dlink.parent) + .select(type_doctype.name) + .where( + (dlink.link_name == customer) + & (type_doctype.name.like(f"%{txt}%")) + & (dlink.link_doctype == "Customer") + ) ) + if type == "Contact": + query = query.select(type_doctype.email_id) + + return query.run() + def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]: """Parse full name into first name, middle name and last name""" diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3e0425c1201..ce20966a25e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -574,6 +574,9 @@ frappe.ui.form.on("Sales Order Item", { }); erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController { + setup() { + this.setup_accounting_dimension_triggers(); + } onload(doc, dt, dn) { super.onload(doc, dt, dn); } diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index d28b1e65b7b..86fae223b45 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if test_exchange_values.get(kwargs["params"]["date"]): return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) - elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"): + elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"): - date = args[0].replace("https://api.frankfurter.app/", "") + date = args[0].replace("https://api.frankfurter.dev/v1/", "") if test_exchange_values.get(date): return PatchResponse( {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 @@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase): self.assertEqual(flt(exchange_rate, 3), 65.1) settings = frappe.get_single("Currency Exchange Settings") - settings.service_provider = "frankfurter.app" + settings.service_provider = "frankfurter.dev" settings.save() def test_exchange_rate_strict(self, mock_get): diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b826c52f20e..0bcab1ccb59 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -93,7 +93,7 @@ def setup_currency_exchange(): ces.set("result_key", []) ces.set("req_params", []) - ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}" + ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}" ces.append("result_key", {"key": "rates"}) ces.append("result_key", {"key": "{to_currency}"}) ces.append("req_params", {"key": "base", "value": "{from_currency}"}) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 527d672cc6a..b9c98bbb8ba 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( erpnext.selling.SellingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); this.frm.make_methods = { 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/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 330d01edb3d..e2cfb8eeca8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -7,7 +7,7 @@ from itertools import groupby import frappe from frappe import _, bold -from frappe.model.mapper import get_mapped_doc, map_child_doc +from frappe.model.mapper import map_child_doc from frappe.query_builder import Case from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum @@ -646,8 +646,8 @@ class PickList(TransactionBase): product_bundles = self._get_product_bundles() product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) - for so_row, item_code in product_bundles.items(): - picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + for so_row, value in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code]) item_table = "Sales Order Item" already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) frappe.db.set_value( @@ -770,19 +770,23 @@ class PickList(TransactionBase): if not item.product_bundle_item: continue - product_bundles[item.sales_order_item] = frappe.db.get_value( - "Sales Order Item", - item.sales_order_item, - "item_code", + product_bundles[item.sales_order_item] = frappe._dict( + { + "item_code": frappe.db.get_value( + "Sales Order Item", + item.sales_order_item, + "item_code", + ), + "pick_list_item": item.name, + } ) return product_bundles - def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: - # bundle_item_code: Dict[component, qty] + def _get_product_bundle_qty_map(self, bundles) -> dict[str, dict[str, float]]: product_bundle_qty_map = {} - for bundle_item_code in bundles: - bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) - product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + for data in bundles: + bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": data.item_code, "disabled": 0}) + product_bundle_qty_map[data.item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: @@ -1388,15 +1392,16 @@ def add_product_bundles_to_delivery_note( product_bundles = pick_list._get_product_bundles() product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) - for so_row, item_code in product_bundles.items(): + for so_row, value in product_bundles.items(): sales_order_item = frappe.get_doc("Sales Order Item", so_row) if sales_order and sales_order_item.parent != sales_order: continue dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( - so_row, product_bundle_qty_map[item_code] + so_row, product_bundle_qty_map[value.item_code] ) + dn_bundle_item.pick_list_item = value.pick_list_item dn_bundle_item.against_pick_list = pick_list.name update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index db065a80c92..29026969404 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -195,6 +195,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend erpnext.buying.BuyingController ) { setup(doc) { + this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); } 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 690e3c81c92..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)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 61c270bd170..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; 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_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/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/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 2f9a04e7e93..4f87e695dfc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -187,7 +187,7 @@ class SubcontractingOrder(SubcontractingController): for item in self.get("items"): bom = frappe.get_doc("BOM", item.bom) rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) - item.rm_cost_per_qty = rm_cost / flt(bom.quantity) + item.rm_cost_per_qty = flt(rm_cost / flt(bom.quantity), item.precision("rm_cost_per_qty")) def calculate_items_qty_and_amount(self): total_qty = total = 0 diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 520d7bb0e16..15e0259722f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -1,6 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc @@ -17,6 +19,10 @@ from erpnext.stock.get_item_details import get_default_cost_center, get_default_ from erpnext.stock.stock_ledger import get_valuation_rate +class BOMQuantityError(frappe.ValidationError): + pass + + class SubcontractingReceipt(SubcontractingController): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController): def on_submit(self): self.validate_closed_subcontracting_order() self.validate_available_qty_for_consumption() + self.validate_bom_required_qty() self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status(update_bin=False) @@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController): item.available_qty_for_consumption and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 ): - msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} - must be less than or equal to Available Qty For Consumption - {flt(item.available_qty_for_consumption, precision)} - in Consumed Items Table.""" + msg = _( + """Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption + {3} {4} in Consumed Items Table.""" + ).format( + item.idx, + flt(item.consumed_qty, precision), + item.stock_uom, + flt(item.available_qty_for_consumption, precision), + item.stock_uom, + ) - frappe.throw(_(msg)) + frappe.throw(msg) + + def validate_bom_required_qty(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "Material Transferred for Subcontract" + ) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")): + return + + rm_consumed_dict = self.get_rm_wise_consumed_qty() + + for row in self.items: + precision = row.precision("qty") + for bom_item in self._get_materials_from_bom( + row.item_code, row.bom, row.get("include_exploded_items") + ): + required_qty = flt( + bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision + ) + consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0) + diff = flt(consumed_qty, precision) - flt(required_qty, precision) + + if diff < 0: + msg = _( + """Additional {0} {1} of item {2} required as per BOM to complete this transaction""" + ).format( + frappe.bold(abs(diff)), + frappe.bold(bom_item.stock_uom), + frappe.bold(bom_item.rm_item_code), + ) + + frappe.throw( + msg, + exc=BOMQuantityError, + ) + + def get_rm_wise_consumed_qty(self): + rm_dict = defaultdict(float) + + for row in self.supplied_items: + rm_dict[row.rm_item_code] += row.consumed_qty + + return rm_dict def update_status_updater_args(self): if cint(self.is_return): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index b9d062af5b2..443ca8cb569 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) +from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import ( + BOMQuantityError, +) class TestSubcontractingReceipt(FrappeTestCase): @@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase): def test_subcontracting_over_receipt(self): """ Behaviour: Raise multiple SCRs against one SCO that in total - receive more than the required qty in the SCO. + receive more than the required qty in the SCO. Expected Result: Error Raised for Over Receipt against SCO. """ from erpnext.controllers.subcontracting_controller import ( @@ -1785,6 +1788,109 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.items[0].rm_cost_per_qty, 300) self.assertEqual(scr.items[0].service_cost_per_qty, 100) + def test_bom_required_qty_validation_based_on_bom(self): + set_backflush_based_on("BOM") + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BRQV-.####", + } + ).name + + make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2) + se = make_stock_entry( + item_code=rm_item1, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.reload() + + self.assertEqual(scr.supplied_items[0].batch_no, batch_no) + self.assertEqual(scr.supplied_items[0].consumed_qty, 1) + self.assertEqual(scr.supplied_items[0].required_qty, 2) + + self.assertRaises(BOMQuantityError, scr.submit) + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + + def test_bom_required_qty_validation_based_on_transfer(self): + from erpnext.controllers.subcontracting_controller import ( + make_rm_stock_entry as make_subcontract_transfer_entry, + ) + + set_backflush_based_on("Material Transferred for Subcontract") + frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1) + + item_code = "_Test Subcontracted Validation FG Item 1" + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + } + ).name + + make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1]) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, + include_exploded_items=0, + ) + + # inward raw material stock + make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": sco.supplied_items[0].rm_item_code, + "qty": sco.supplied_items[0].required_qty - 5, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + ] + + # transfer partial raw materials + ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + ste.submit() + + scr = make_subcontracting_receipt(sco.name) + scr.save() + + self.assertRaises(BOMQuantityError, scr.submit) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args)