diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1cf9a5bd9b5..9f040b4e62e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want. -The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com). +The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6). ### Reply and Closing Policy diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 4d61f1fb943..8b13b00c042 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -9,7 +9,7 @@ body: Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext - - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com) + - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6) - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly. 2. When making a bug report, make sure you provide all required information. The easier it is for maintainers to reproduce, the faster it'll be fixed. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 26bb7ab280c..7b58c0d3a3a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Community Forum - url: https://discuss.erpnext.com/ + url: https://discuss.frappe.io/c/erpnext/6 about: For general QnA, discussions and community help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 607e42d1b49..38d881e24ae 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -11,7 +11,7 @@ assignees: '' Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext - - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com + - For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6 2. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. @@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on If you're in urgent need to a feature, please try the following channels to get paid developments done quickly: 1. Certified ERPNext partners: https://erpnext.com/partners -2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5 +2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5 3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps --> diff --git a/README.md b/README.md index 3b703d97831..021b1a3ab5c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB 1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. -3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. +3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers. 4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index a88764cf1b2..748a9a14397 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -46,7 +46,8 @@ def validate_service_stop_date(doc): if ( old_stop_dates and old_stop_dates.get(item.name) - and item.service_stop_date != old_stop_dates.get(item.name) + and item.service_stop_date + and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name)) ): frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx)) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 5ad2479e858..35a3196c140 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -82,7 +82,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-07-29 11:37:42.678556", + "modified": "2025-10-13 15:11:58.300836", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py index fa863741d51..599bd2c5e4c 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py @@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document): and not frappe.flags.is_reverse_depr_entry ): update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None) + + +def on_doctype_update(): + frappe.db.add_index( + "Advance Payment Ledger Entry", + ["against_voucher_type", "against_voucher_no"], + ) + + frappe.db.add_index( + "Advance Payment Ledger Entry", + ["voucher_type", "voucher_no"], + ) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 510e6b1dcbc..0f3216ae772 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -131,8 +131,8 @@ class GLEntry(Document): if not self.is_cancelled and not (self.party_type and self.party): account_type = frappe.get_cached_value("Account", self.account, "account_type") - # skipping validation for payroll entry creation in case party is not required - if not frappe.flags.party_not_required_for_receivable_payable: + + if not frappe.flags.party_not_required: # skipping validation if party is not required if account_type == "Receivable": frappe.throw( _("{0} {1}: Customer is required against Receivable account {2}").format( diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 4184bdaabb9..4f904d87998 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -59,6 +59,7 @@ "addtional_info", "mode_of_payment", "payment_order", + "party_not_required", "column_break3", "is_opening", "stock_entry", @@ -543,6 +544,14 @@ "label": "Is System Generated", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "party_not_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Party Not Required", + "no_copy": 1 } ], "icon": "fa fa-file-text", @@ -557,7 +566,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-07-18 15:32:29.413598", + "modified": "2025-09-29 13:05:46.982277", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 82132ce70be..9d3c9eb501e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -72,6 +72,7 @@ class JournalEntry(AccountsController): multi_currency: DF.Check naming_series: DF.Literal["ACC-JV-.YYYY.-"] paid_loan: DF.Data | None + party_not_required: DF.Check pay_to_recd_from: DF.Data | None payment_order: DF.Link | None posting_date: DF.Date @@ -543,10 +544,10 @@ class JournalEntry(AccountsController): for d in self.get("accounts"): account_type = frappe.get_cached_value("Account", d.account, "account_type") - # skipping validation for payroll entry creation - skip_validation = frappe.flags.party_not_required_for_receivable_payable if account_type in ["Receivable", "Payable"]: - if not (d.party_type and d.party) and not skip_validation: + if ( + not (d.party_type and d.party) and not self.party_not_required + ): # skipping validation if party_not_required is passed via payroll entry frappe.throw( _( "Row {0}: Party Type and Party is required for Receivable / Payable account {1}" @@ -1139,6 +1140,11 @@ class JournalEntry(AccountsController): } ) + # set flag to skip party validation + account_type = frappe.get_cached_value("Account", d.account, "account_type") + if account_type in ["Receivable", "Payable"] and self.party_not_required: + frappe.flags.party_not_required = True + gl_map.append( self.get_gl_dict( row, @@ -1166,6 +1172,7 @@ class JournalEntry(AccountsController): merge_entries=merge_entries, update_outstanding=update_outstanding, ) + frappe.flags.party_not_required = False if cancel: cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 45c2b4ce764..5d31c627381 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -286,7 +286,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-07-25 04:45:28.117715", + "modified": "2025-09-29 13:01:48.916517", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 90654b42d02..f8e12eda182 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2640,6 +2640,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + @change_settings( + "Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1} + ) + def test_pr_status_rate_adjusted_from_pi(self): + pr = make_purchase_receipt(qty=5, rate=100) + pi = create_purchase_invoice_from_receipt(pr.name) + pi.submit() + pr.reload() + + # Inital check + self.assertEqual(pr.status, "Completed") + + pi.reload() + pi.cancel() + pi = create_purchase_invoice_from_receipt(pr.name) + pi.items[0].rate = 80 + pi.submit() + pr.reload() + + # Test 1 : Adjustment amount is negative + self.assertEqual(pr.status, "Completed") + + pi.reload() + pi.cancel() + pi = create_purchase_invoice_from_receipt(pr.name) + pi.items[0].rate = 120 + pi.submit() + pr.reload() + + # Test 2 : Adjustment amount is positive + self.assertEqual(pr.status, "Completed") + def test_opening_invoice_rounding_adjustment_validation(self): pi = make_purchase_invoice(do_not_save=1) pi.items[0].rate = 99.98 diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 69d5da7b9a3..7acb36eca93 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -912,7 +912,8 @@ "label": "Rejected Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "fieldname": "wip_composite_asset", @@ -983,7 +984,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 16:33:13.453290", + "modified": "2025-10-14 13:01:54.441511", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -993,4 +994,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a19850af8f1..193014d340d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -798,6 +798,15 @@ frappe.ui.form.on("Sales Invoice", { }, }; }; + + frm.set_query("sales_person", "sales_team", function () { + return { + filters: { + is_group: 0, + enabled: 1, + }, + }; + }); }, onload: function (frm) { frm.redemption_conversion_factor = null; diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d7884b3e973..c196d52d744 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -354,7 +354,7 @@ def get_asset_details_for_grouped_by_category(filters): # nosemgrep return frappe.db.sql( f""" - SELECT a.name, + SELECT a.name, a.asset_name, ifnull(sum(case when a.purchase_date < %(from_date)s then case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then a.gross_purchase_amount @@ -583,6 +583,14 @@ def get_columns(filters): "width": 120, } ) + columns.append( + { + "label": _("Asset Name"), + "fieldname": "asset_name", + "fieldtype": "Data", + "width": 140, + } + ) columns += [ { diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index b06244fd344..c00fc86d4b2 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -507,7 +507,8 @@ def depreciate_asset(asset_doc, date, notes): make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) asset_doc.reload() - cancel_depreciation_entries(asset_doc, date) + if not frappe.flags.is_composite_component: + cancel_depreciation_entries(asset_doc, date) @erpnext.allow_regional diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index fc0625edf9b..ab3bb9ab406 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -492,14 +492,18 @@ class AssetCapitalization(StockController): asset = frappe.get_doc("Asset", item.asset) if asset.calculate_depreciation: - notes = _( - "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.doctype, self.get("name")), - ) - depreciate_asset(asset, self.posting_date, notes) - asset.reload() + frappe.flags.is_composite_component = True + try: + notes = _( + "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + depreciate_asset(asset, self.posting_date, notes) + asset.reload() + finally: + frappe.flags.is_composite_component = False fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index cfe9768ef4b..44aa271846c 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_link_to_form +from frappe.utils import cstr, get_link_to_form from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity @@ -142,18 +142,10 @@ class AssetMovement(Document): def update_asset_location_and_custodian(self, asset_id, location, employee): asset = frappe.get_doc("Asset", asset_id) - updates = {} - if employee and employee != asset.custodian: - updates["custodian"] = employee - - elif not employee and asset.custodian: - updates["custodian"] = "" - + if cstr(employee) != asset.custodian: + frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee)) if location and location != asset.location: - updates["location"] = location - - if updates: - frappe.db.set_value("Asset", asset_id, updates) + frappe.db.set_value("Asset", asset_id, "location", location) def log_asset_activity(self, asset_id, location, employee): if location and employee: diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index b66adca1343..857588bd93e 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -319,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book): .select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount")) .where(gle.account == aca.fixed_asset_account) .where(gle.is_cancelled == 0) + .where(gle.is_opening == "No") .where(company.name == filters.company) .where(asset.docstatus == 1) ) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 20267e9ae10..db93a3d7e79 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -284,15 +284,15 @@ def get_columns(filters): def get_message(): - return """ - Valid till :    + return f""" + {_("Valid Till")}:   - Expires in a week or less + {_("Expires in a week or less")}    - Expires today / Already Expired + {_("Expires today or already expired")} """ diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 4666c953fbe..36341a090dc 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -637,7 +637,8 @@ class SubcontractingController(StockController): if use_serial_batch_fields: rm_obj.use_serial_batch_fields = 1 - self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + if not self.flags.get("reset_raw_materials"): + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) if self.doctype == "Subcontracting Receipt": if not use_serial_batch_fields: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 0c535af5be2..09abcb6351e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -24,6 +24,7 @@ frappe.ui.form.on("Production Plan", { query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query", filters: { company: frm.doc.company, + item_code: frm.doc.item_code, }, }; }); @@ -105,6 +106,8 @@ frappe.ui.form.on("Production Plan", { __("View") ); + let has_create_buttons = false; + if (frm.doc.status !== "Completed") { if (frm.doc.status === "Closed") { frm.add_custom_button( @@ -134,6 +137,7 @@ frappe.ui.form.on("Production Plan", { }, __("Create") ); + has_create_buttons = true; } if ( @@ -148,12 +152,13 @@ frappe.ui.form.on("Production Plan", { }, __("Create") ); + has_create_buttons = true; } } - } - if (frm.doc.status !== "Closed") { - frm.page.set_inner_btn_group_as_primary(__("Create")); + if (has_create_buttons && frm.doc.status !== "Closed") { + frm.page.set_inner_btn_group_as_primary(__("Create")); + } } frm.trigger("material_requirement"); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d44f48a08ed..a1422bcb265 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1543,6 +1543,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() + existing_sub_assembly_items = set() sub_assembly_items = defaultdict(int) if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): @@ -1576,7 +1577,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d item_details = {} if doc.get("sub_assembly_items"): item_details = get_raw_materials_of_sub_assembly_items( - so_item_details[doc.get("sales_order")].keys() if so_item_details else [], + existing_sub_assembly_items, item_details, company, bom_no, @@ -1839,7 +1840,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.required_bom_qty)) + .select(Sum(child.quantity * child.conversion_factor)) .where( (table.docstatus == 1) & (child.item_code == item_code) @@ -1955,6 +1956,7 @@ def get_raw_materials_of_sub_assembly_items( sub_assembly_items, planned_qty=planned_qty, ) + existing_sub_assembly_items.add(item.item_code) else: if not item.conversion_factor and item.purchase_uom: item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) @@ -1992,6 +1994,9 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page if filters.get("sales_orders"): query = query.where(so_table.name.isin(filters.get("sales_orders"))) + if filters.get("item_code"): + query = query.where(table.item_code == filters.get("item_code")) + if txt: query = query.where(table.parent.like(f"%{txt}%")) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 3c9e29b2811..28c9e63bc1f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1637,11 +1637,17 @@ class TestProductionPlan(FrappeTestCase): def test_calculation_of_sub_assembly_items(self): make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) + make_item("Sub Assembly Item 2", properties={"is_stock_item": 1}) make_item("RM Item 1", properties={"is_stock_item": 1}) make_item("RM Item 2", properties={"is_stock_item": 1}) + make_item("_Test FG Item 3", properties={"is_stock_item": 1}) + make_item("_Test FG Item 4", properties={"is_stock_item": 1}) make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"]) + make_bom(item="Sub Assembly Item 2", raw_materials=["RM Item 2"]) make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"]) make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"]) + make_bom(item="_Test FG Item 3", raw_materials=["RM Item 1"]) + make_bom(item="_Test FG Item 4", raw_materials=["Sub Assembly Item 2"]) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1677,12 +1683,39 @@ class TestProductionPlan(FrappeTestCase): "warehouse": "_Test Warehouse - _TC", }, ) + # Assembly item with similar RM item + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 3", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 3", "default_bom"), + "planned_qty": 10, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) + # Sub-assembly item with similar RM item + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 4", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 4", "default_bom"), + "planned_qty": 10, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) plan.save() plan.get_sub_assembly_items() - self.assertEqual(plan.sub_assembly_items[0].qty, 20) - self.assertEqual(plan.sub_assembly_items[1].qty, 50) + self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1 + self.assertEqual(plan.sub_assembly_items[1].qty, 50) # Sub Assembly For FG 2 + self.assertEqual(plan.sub_assembly_items[2].qty, 10) # Sub Assembly For FG 4 from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, @@ -1690,8 +1723,11 @@ class TestProductionPlan(FrappeTestCase): mr_items = get_items_for_material_requests(plan.as_dict()) - self.assertEqual(mr_items[0].get("quantity"), 80) - self.assertEqual(mr_items[1].get("quantity"), 70) + # RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock) + self.assertEqual(mr_items[0].get("quantity"), 90) + + # RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock) + self.assertEqual(mr_items[1].get("quantity"), 80) def test_production_plan_for_partial_sub_assembly_items(self): from erpnext.controllers.status_updater import OverAllowanceError diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 63af3e5cbe6..9867db0dd1c 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -113,6 +113,13 @@ class ProductionPlanReport: self.orders = query.run(as_dict=True) def get_raw_materials(self): + """Retrieve raw materials and source warehouses for production orders. + + This method collects BOM or Work Order items depending on the selected + filter and updates `self.raw_materials_dict`, `self.warehouses`, + and `self.item_codes` accordingly. + """ + if not self.orders: return self.warehouses = [d.warehouse for d in self.orders] @@ -135,7 +142,7 @@ class ProductionPlanReport: ) or [] ) - self.warehouses.extend([d.source_warehouse for d in raw_materials]) + self.warehouses.extend([d.warehouse for d in raw_materials]) else: bom_nos = [] diff --git a/erpnext/projects/web_form/tasks/tasks.py b/erpnext/projects/web_form/tasks/tasks.py index b42297314a9..fbd0866e0ac 100644 --- a/erpnext/projects/web_form/tasks/tasks.py +++ b/erpnext/projects/web_form/tasks/tasks.py @@ -1,15 +1,17 @@ +import urllib.parse + import frappe def get_context(context): - if frappe.form_dict.project: - context.parents = [ - {"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project} - ] - context.success_url = "/projects?project=" + frappe.form_dict.project + if project := frappe.form_dict.project: + title = frappe.utils.data.escape_html(project) + route = "/projects?" + urllib.parse.urlencode({"project": project}) + context.parents = [{"title": title, "route": route}] + context.success_url = route - elif context.doc and context.doc.get("project"): - context.parents = [ - {"title": context.doc.project, "route": "/projects?project=" + context.doc.project} - ] - context.success_url = "/projects?project=" + context.doc.project + elif context.doc and (project := context.doc.get("project")): + title = frappe.utils.data.escape_html(project) + route = "/projects?" + urllib.parse.urlencode({"project": project}) + context.parents = [{"title": title, "route": route}] + context.success_url = route diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1afca307d94..8c0be8042ac 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -171,13 +171,15 @@ erpnext.buying = { shipping_address: this.frm.doc.shipping_address }, callback: (r) => { - this.frm.set_value("billing_address", r.message.primary_address || ""); + if (!this.frm.doc.billing_address) + this.frm.set_value("billing_address", r.message.primary_address || ""); - if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return; - this.frm.set_value( - "shipping_address", - r.message.shipping_address || this.frm.doc.shipping_address || "" - ); + if ( + !frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") || + this.frm.doc.shipping_address + ) + return; + this.frm.set_value("shipping_address", r.message.shipping_address || ""); }, }); erpnext.utils.set_letter_head(this.frm) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ae3b7404f7f..66c54ec6639 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1022,19 +1022,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } else { set_pricing(); } + }; - } - - if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { - let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); - - if (!is_drop_ship) { - erpnext.utils.get_shipping_address(this.frm, function() { - set_party_account(set_pricing); - }); - } + if ( + frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && + ["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) && + !this.frm.doc.shipping_address + ) { + let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier); + if (!is_drop_ship) { + erpnext.utils.get_shipping_address(this.frm, function() { + set_party_account(set_pricing); + }); + } } else { set_party_account(set_pricing); } diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index ffff536f068..bf4ef8666cd 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -109,6 +109,7 @@ erpnext.sales_common = { ); this.toggle_editable_price_list_rate(); + this.change_warehouse_labels_for_return(); } company() { @@ -500,6 +501,33 @@ erpnext.sales_common = { this.frm.set_value("discount_amount", 0); this.frm.set_value("additional_discount_percentage", 0); } + + is_return() { + let reset = !this.frm.doc.is_return; + this.change_warehouse_labels_for_return(reset); + } + + change_warehouse_labels_for_return(reset) { + // swap source and target warehouse labels for return + let source_warehouse_label = __("Source Warehouse"); + let target_warehouse_label = __("Set Target Warehouse"); + + if (this.frm.doc.doctype == "Delivery Note") { + source_warehouse_label = __("Set Source Warehouse"); + } + + if (reset) { + // reset to original labels + this.frm.set_df_property("set_warehouse", "label", source_warehouse_label); + this.frm.set_df_property("set_target_warehouse", "label", target_warehouse_label); + return; + } + + if (this.frm.doc.is_return) { + this.frm.set_df_property("set_warehouse", "label", target_warehouse_label); + this.frm.set_df_property("set_target_warehouse", "label", source_warehouse_label); + } + } }; }, }; diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index e02d7a3d785..996ee949a13 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -457,7 +457,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { (["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) && !this.frm.doc.is_return) || (this.frm.doc.doctype === "Stock Entry" && - this.frm.doc.purpose === "Material Receipt") + (this.frm.doc.purpose === "Material Receipt" || + (this.frm.doc.purpose === "Manufacture" && this.item.is_finished_item))) ) { is_inward = true; } diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index f29cd253818..4e6f8203f58 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -44,6 +44,15 @@ frappe.ui.form.on("Sales Order", { }; }); + frm.set_query("sales_person", "sales_team", function () { + return { + filters: { + is_group: 0, + enabled: 1, + }, + }; + }); + frm.set_df_property("packed_items", "cannot_add_rows", true); frm.set_df_property("packed_items", "cannot_delete_rows", true); }, diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 9491da7ec3b..b826c52f20e 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -262,6 +262,20 @@ def update_roles(): def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): + if frappe.db.exists("Role Profile", role_profile_name): + role_profile = frappe.get_doc("Role Profile", role_profile_name) + existing_roles = [row.role for row in role_profile.roles] + + role_profile.roles = [row for row in role_profile.roles if row.role in roles] + + for role in roles: + if role not in existing_roles: + role_profile.append("roles", {"role": role}) + + role_profile.save(ignore_permissions=True) + + continue + role_profile = frappe.new_doc("Role Profile") role_profile.role_profile = role_profile_name for role in roles: diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 9701a37b6dc..03cbd99f5c4 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -74,6 +74,8 @@ def update_page_info(bootinfo): def bootinfo(bootinfo): if bootinfo.get("user") and bootinfo["user"].get("name"): bootinfo["user"]["employee"] = "" + frappe.session.data.employee = "" employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name") if employee: bootinfo["user"]["employee"] = employee + frappe.session.data.employee = employee diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 9955b485c22..699b7a9562d 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -158,7 +158,9 @@ class Batch(Document): @frappe.whitelist() def recalculate_batch_qty(self): - batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True) + batches = get_batch_qty( + batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True + ) batch_qty = 0.0 if batches: for row in batches: @@ -260,6 +262,7 @@ def get_batch_qty( "posting_date": posting_date, "posting_time": posting_time, "batch_no": batch_no, + "based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), "ignore_voucher_nos": ignore_voucher_nos, "for_stock_levels": for_stock_levels, "consider_negative_batches": consider_negative_batches, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 0cd4e24ff93..527d672cc6a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( if ( doc.docstatus == 1 && !doc.is_return && + doc.per_returned != 100 && doc.status != "Closed" && flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 6cadbdc5e47..d2a15c2bd66 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -10,6 +10,8 @@ from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values +from frappe.query_builder import DocType +from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt from erpnext.accounts.party import get_due_date @@ -790,35 +792,39 @@ def get_list_context(context=None): def get_invoiced_qty_map(delivery_note): """returns a map: {dn_detail: invoiced_qty}""" - invoiced_qty_map = {} + sii = DocType("Sales Invoice Item") - for dn_detail, qty in frappe.db.sql( - """select dn_detail, qty from `tabSales Invoice Item` - where delivery_note=%s and docstatus=1""", - delivery_note, - ): - if not invoiced_qty_map.get(dn_detail): - invoiced_qty_map[dn_detail] = 0 - invoiced_qty_map[dn_detail] += qty + invoiced_qty_map = frappe._dict( + ( + frappe.qb.from_(sii) + .select(sii.dn_detail, Sum(sii.qty).as_("qty")) + .where((sii.delivery_note == delivery_note) & (sii.docstatus == 1)) + .groupby(sii.dn_detail) + ).run() + ) return invoiced_qty_map def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" + dn = DocType("Delivery Note") + dni = DocType("Delivery Note Item") + returned_qty_map = frappe._dict( - frappe.db.sql( - """select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name = dn_item.parent - and dn.docstatus = 1 - and dn.is_return = 1 - and dn.return_against = %s - and dn_item.qty <= 0 - group by dn_item.item_code - """, - delivery_note, - ) + ( + frappe.qb.from_(dni) + .join(dn) + .on(dn.name == dni.parent) + .select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty")) + .where( + (dn.docstatus == 1) + & (dn.is_return == 1) + & (dn.return_against == delivery_note) + & (dni.qty <= 0) + ) + .groupby(dni.dn_detail) + ).run() ) return returned_qty_map diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bc7023fbebb..c4f878fe85d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1115,7 +1115,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate buying_settings = frappe.get_single("Buying Settings") over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") - total_amount, total_billed_amount = 0, 0 + total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) if adjust_incoming_rate: @@ -1155,6 +1155,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate ) * item.qty adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) + pi_landed_cost_amount += adjusted_amt item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) elif amount and item.billed_amt > amount: per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100 @@ -1165,6 +1166,9 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate ) ) + if pi_landed_cost_amount < 0: + total_billed_amount += abs(pi_landed_cost_amount) + percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 5f8a9b58e91..463544a9952 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -778,7 +778,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Material Request Item", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "expense_account", @@ -1038,7 +1039,8 @@ "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", "no_copy": 1, - "options": "Serial and Batch Bundle" + "options": "Serial and Batch Bundle", + "search_index": 1 }, { "depends_on": "eval:doc.use_serial_batch_fields === 0", @@ -1147,7 +1149,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 17:10:43.780622", + "modified": "2025-10-14 12:59:20.384056", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1158,4 +1160,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} 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 ca3e7ec1ad7..977a41f0369 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 @@ -294,7 +294,7 @@ class SerialandBatchBundle(Document): } ) - if self.returned_against and self.docstatus == 1: + if (self.returned_against or self.voucher_type == "Stock Reconciliation") and self.docstatus == 1: kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no if self.docstatus == 1: @@ -2677,7 +2677,10 @@ def get_stock_ledgers_for_serial_nos(kwargs): else: query = query.where(stock_ledger_entry[field] == kwargs.get(field)) - if kwargs.voucher_no: + if kwargs.ignore_voucher_detail_no: + query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no) + + elif kwargs.voucher_no: query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no) return query.run(as_dict=True) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 08ce705f26f..cde5be4d6ee 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1323,18 +1323,9 @@ class TestStockEntry(FrappeTestCase): posting_date="2021-07-02", # Illegal SE purpose="Material Transfer", ), - dict( - item_code=item_code, - qty=2, - from_warehouse=warehouse_names[0], - to_warehouse=warehouse_names[1], - batch_no=batch_no, - posting_date="2021-07-02", # Illegal SE - purpose="Material Transfer", - ), ] - self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle_batch(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 9824911404e..cedf0872873 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -188,6 +188,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Basic Rate (as per Stock UOM)", + "non_negative": 1, "oldfieldname": "incoming_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -446,7 +447,8 @@ "no_copy": 1, "options": "Stock Entry", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "ste_detail", @@ -454,7 +456,8 @@ "label": "Stock Entry Child", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_51", @@ -613,7 +616,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-03-26 21:01:58.544797", + "modified": "2025-10-14 15:10:38.373099", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index bd0a0beceef..9140599e7ba 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -589,6 +589,10 @@ class StockReconciliation(StockController): if row.get(field): key.append(row.get(field)) + for dimension in get_inventory_dimensions(): + if row.get(dimension.get("fieldname")): + key.append(row.get(dimension.get("fieldname"))) + if key in item_warehouse_combinations: self.validation_messages.append( _get_msg(row_num, _("Same item and warehouse combination already entered.")) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 589861809d4..bea1cb7386e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1385,12 +1385,12 @@ def get_batch_current_qty(batch): def throw_negative_batch_validation(batch_no, qty): - frappe.msgprint( + # This validation is important for backdated stock transactions with batch items + frappe.throw( _( "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." ).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)), - title=_("Warning!"), - indicator="orange", + title=_("Negative Stock Error"), ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a06412373e2..a7a319e296e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -732,6 +732,10 @@ class update_entries_after: elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry( dependant_sle.voucher_no ): + if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"): + return + + val["transfer_entry_to_repost"] = True self.distinct_item_warehouses[key] = val self.new_items_found = True @@ -888,9 +892,8 @@ class update_entries_after: sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) - if not sle.is_adjustment_entry: - sle.stock_value_difference = stock_value_difference - elif sle.is_adjustment_entry and not self.args.get("sle_id"): + sle.stock_value_difference = stock_value_difference + if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0: sle.stock_value_difference = ( get_stock_value_difference( sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index f5dd1c4f16c..99a6abc8b91 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -195,6 +195,7 @@ class SubcontractingReceipt(SubcontractingController): @frappe.whitelist() def reset_raw_materials(self): self.supplied_items = [] + self.flags.reset_raw_materials = True self.create_raw_materials_supplied() def validate_closed_subcontracting_order(self):