From 0adb7156cd4ee644897b85a7373b1a07125d082e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 May 2025 11:10:23 +0530 Subject: [PATCH 01/25] fix: dont auto-fetch latest exchange rate - also use correct currency field for comparison (cherry picked from commit 4ccd0a740706c3bfa01ed25959580107d360aa60) --- erpnext/crm/doctype/opportunity/opportunity.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 1c8a80a0ed6..b78eae5f109 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -111,6 +111,13 @@ frappe.ui.form.on("Opportunity", { }, __("Create") ); + + let company_currency = erpnext.get_currency(frm.doc.company); + if (company_currency != frm.doc.currency) { + frm.add_custom_button(__("Fetch Latest Exchange Rate"), function () { + frm.trigger("currency"); + }); + } } if (!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus == 0) { @@ -152,7 +159,7 @@ frappe.ui.form.on("Opportunity", { currency: function (frm) { let company_currency = erpnext.get_currency(frm.doc.company); - if (company_currency != frm.doc.company) { + if (company_currency != frm.doc.currency) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { @@ -278,7 +285,6 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { } this.setup_queries(); - this.frm.trigger("currency"); } refresh() { From 09e7bfbacb80c5e42ce6b47ff7dcf63e5810f826 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 14:04:12 +0530 Subject: [PATCH 02/25] perf: Skip link checking on repost's remove_attached_file (backport #45061) (#47450) perf: Skip link checking on repost's remove_attached_file (#45061) This is internal detail, doesn't need to do horrible link checks in framework. (cherry picked from commit 4f690affc94d7a11ba601938a77ccf1ff01a3323) Co-authored-by: Ankush Menat --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ecbb10b6cb5..06598aa285b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -342,7 +342,7 @@ def remove_attached_file(docname): if file_name := frappe.db.get_value( "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" ): - frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) + frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True, force=True) def repost_sl_entries(doc): From 7abe199e2ab2d68e370e643adaf9c280249e38d4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 7 May 2025 16:03:59 +0530 Subject: [PATCH 03/25] fix: warning message for COGS account in the stock entry (cherry picked from commit bba6b0ff45ac13a0f5d3516bcd3ddec852e9652b) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a23ac8342a0..ca5e3f7eb10 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -6,7 +6,7 @@ import json from collections import defaultdict import frappe -from frappe import _ +from frappe import _, bold from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum from frappe.utils import ( @@ -526,6 +526,14 @@ class StockEntry(StockController): OpeningEntryAccountError, ) + if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold": + frappe.msgprint( + _( + "At row {0}: You have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" + ).format(d.idx, bold(get_link_to_form("Account", d.expense_account))), + title=_("Warning : Cost of Goods Sold Account"), + ) + def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" From 7ba7d1a2a4bb9f8f61f180ec10dd52dc82f4ae6d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 7 May 2025 20:56:05 +0530 Subject: [PATCH 04/25] fix: error while making SABB for backdated stock reco (cherry picked from commit ad25636afb10ea35469c27de0275ed2f93661594) --- .../stock_reconciliation/stock_reconciliation.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 76a540f3e92..4ea683a904d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -203,9 +203,19 @@ class StockReconciliation(StockController): ) ) - if self.docstatus == 1: - bundle.voucher_no = self.name - bundle.submit() + if ( + self.docstatus == 1 + and item.current_serial_and_batch_bundle + and frappe.db.get_value( + "Serial and Batch Bundle", item.current_serial_and_batch_bundle, "docstatus" + ) + == 0 + ): + sabb_doc = frappe.get_doc( + "Serial and Batch Bundle", item.current_serial_and_batch_bundle + ) + sabb_doc.voucher_no = self.name + sabb_doc.submit() item.db_set( { From 67d24e9635cec7fa2be5517cfbfc9c5f8a83e701 Mon Sep 17 00:00:00 2001 From: Yaiphalemba Mangshatabam Date: Thu, 8 May 2025 14:09:06 +0530 Subject: [PATCH 05/25] fix: typo in event.js "Sales Partners" -> "Sales Partner" (cherry picked from commit edee75c7576cd094e0e2d29845be46cd988032b5) --- erpnext/public/js/event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/event.js b/erpnext/public/js/event.js index a6733915a5c..2950ace888d 100644 --- a/erpnext/public/js/event.js +++ b/erpnext/public/js/event.js @@ -47,7 +47,7 @@ frappe.ui.form.on("Event", { frm.add_custom_button( __("Add Sales Partners"), function () { - new frappe.desk.eventParticipants(frm, "Sales Partners"); + new frappe.desk.eventParticipants(frm, "Sales Partner"); }, __("Add Participants") ); From 4a37f2a925f4bfc15a4450b51dedac64a34233e8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 May 2025 15:45:46 +0530 Subject: [PATCH 06/25] fix: broken test suite due to incorrect OR filter (cherry picked from commit 37d74e387d68c64be36b381aa3267b3459e2b358) --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index f8cd61d50ea..3cc405aa658 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -214,13 +214,12 @@ def get_or_create_account(company_name, account): default_root_type = "Liability" root_type = account.get("root_type", default_root_type) + or_filters = {"account_name": account.get("account_name")} + if account.get("account_number"): + or_filters.update({"account_number": account.get("account_number")}) + existing_accounts = frappe.get_all( - "Account", - filters={"company": company_name, "root_type": root_type}, - or_filters={ - "account_name": account.get("account_name"), - "account_number": account.get("account_number"), - }, + "Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters ) if existing_accounts: From 64ae4e1fecb0d04f666416844a6b8b3f1da9e808 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 9 May 2025 13:38:42 +0530 Subject: [PATCH 07/25] fix: timesheet portal showing total billing hours (cherry picked from commit b04a07fda0627f94b389588993610063ae9b0390) --- erpnext/projects/doctype/timesheet/timesheet.py | 2 +- erpnext/templates/includes/timesheet/timesheet_row.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 7d194b7c37e..758b25f0de0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -535,7 +535,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20 table.name, child_table.activity_type, table.status, - table.total_billable_hours, + child_table.billing_hours, (table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"), child_table.project, ) diff --git a/erpnext/templates/includes/timesheet/timesheet_row.html b/erpnext/templates/includes/timesheet/timesheet_row.html index 0f9cc77e89d..8905262a88e 100644 --- a/erpnext/templates/includes/timesheet/timesheet_row.html +++ b/erpnext/templates/includes/timesheet/timesheet_row.html @@ -5,7 +5,7 @@ {{ doc.name }} -
{{ doc.total_billable_hours }}
+
{{ doc.billing_hours }}
{{ doc.project or '' }}
{{ doc.sales_invoice or '' }}
{{ _(doc.activity_type) }}
From 309ea7b9cfa9481d14ab50d6f343dae29e6246fa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 14:31:46 +0530 Subject: [PATCH 08/25] fix: added PR/PI overbilling validation (backport #47385) (#47497) * fix: added PR/PI overbilling validation (cherry picked from commit f4ffc57b5107f0d9da0b16824dbac23952befc9a) * test: added test (cherry picked from commit b406ec724b2f1703d8e5dbacf7bb82ffe3e0cc8a) * fix: linter error (cherry picked from commit 27e842ba02a546043c8ed1c0be9888c9b1c132d9) --------- Co-authored-by: Mihir Kandoi --- .../purchase_invoice/test_purchase_invoice.py | 40 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 9 +++++ 2 files changed, 49 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7c4ba47f9ba..a2c64765ed9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1695,6 +1695,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Configure Buying Settings to allow rate change frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + # Configure Accounts Settings to allow 300% over billing + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300) + # Create PR: rate = 1000, qty = 5 pr = make_purchase_receipt( item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) @@ -2756,6 +2759,43 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(invoice.grand_total, 300) + def test_pr_pi_over_billing(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_purchase_invoice_from_pr, + ) + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + pr = make_purchase_receipt(qty=10, rate=10) + pi = make_purchase_invoice_from_pr(pr.name) + + pi.items[0].rate = 12 + + # Test 1 - This will fail because over billing is not allowed + self.assertRaises(frappe.ValidationError, pi.submit) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked + pi.submit() + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20) + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 12 + + # Test 3 - This will now submit because over billing is allowed upto 20% + pi.submit() + + pi.reload() + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 13 + + # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail + self.assertRaises(frappe.ValidationError, pi.submit) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b7cf97589af..6d4a348ef9d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1081,6 +1081,7 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty 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 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -1119,6 +1120,14 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) + elif item.billed_amt > item.amount: + per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100 + if per_over_billed > over_billing_allowance: + frappe.throw( + _("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format( + item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance + ) + ) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) From b18692c1202e7aba117793319b29803ac8bfaf25 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 15:10:11 +0530 Subject: [PATCH 09/25] fix: POS non-stock item mistakenly hidden as unavailable (backport #47493) (#47506) fix: POS non-stock item mistakenly hidden as unavailable (#47493) (cherry picked from commit 57f3489dfab6f624ab6370eaa8d877af920976e8) Co-authored-by: Diptanil Saha --- erpnext/selling/page/point_of_sale/point_of_sale.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 9be5a656a8e..640577bb72c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: - bin_join_selection = ", `tabBin` bin" - bin_join_condition = ( - "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" - ) + bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name" + bin_join_condition = "AND item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0)" items_data = frappe.db.sql( """ From 6dbdc36af911358fc31cbf0c672932a8718dd994 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 24 Apr 2025 19:32:38 +0530 Subject: [PATCH 10/25] fix: accumulate values for all the fiscal years in Profit And Loss Statement (cherry picked from commit 685132236128456fe9ed5e3677454f92cc631e08) --- .../profit_and_loss_statement/profit_and_loss_statement.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 2b6280c74b5..ccb4d26f77b 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -35,7 +35,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) expense = get_data( @@ -46,7 +45,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) net_profit_loss = get_net_profit_loss( From d61a85e31697847f32def4bfb56fcee49a6874e3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 15:07:02 +0530 Subject: [PATCH 11/25] fix: typo (cherry picked from commit 61d13ce232c25122ac9a47104ccae0a23aea5f69) --- .../profit_and_loss_statement/test_profit_and_loss_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index 816c2b9950f..de29e701a21 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -57,7 +57,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): period_end_date=fy.year_end_date, filter_based_on="Fiscal Year", periodicity="Monthly", - accumulated_vallues=True, + accumulated_values=True, ) def test_profit_and_loss_output_and_summary(self): From 98cb9c6b96b3eb9dcc89b9e5d333cd224b3cac94 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 15:48:39 +0530 Subject: [PATCH 12/25] test: accumulate filter on P&L report (cherry picked from commit afff6b84ce3c86e3a3621df1efc771adb59d02ed) --- .../test_profit_and_loss_statement.py | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index de29e701a21..0c4ab37ae58 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -2,8 +2,9 @@ # MIT License. See license.txt import frappe +from frappe.desk.query_report import export_query from frappe.tests.utils import FrappeTestCase -from frappe.utils import getdate, today +from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.financial_statements import get_period_list @@ -90,3 +91,82 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): with self.subTest(current_period_key=current_period_key): self.assertEqual(acc[current_period_key], 150) self.assertEqual(acc["total"], 150) + + def test_p_and_l_export(self): + self.create_sales_invoice(qty=1, rate=150) + + filters = self.get_report_filters() + frappe.local.form_dict = frappe._dict( + { + "report_name": "Profit and Loss Statement", + "file_format_type": "CSV", + "filters": filters, + "visible_idx": [0, 1, 2, 3, 4, 5, 6], + } + ) + export_query() + contents = frappe.response["filecontent"].decode() + sales_account = frappe.db.get_value("Company", self.company, "default_income_account") + + self.assertIn(sales_account, contents) + + def test_accumulate_filter(self): + # ensure 2 fiscal years + cur_fy = self.get_fiscal_year() + find_for = add_days(cur_fy.year_start_date, -1) + _x = frappe.db.get_all( + "Fiscal Year", + filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)}, + )[0] + prev_fy = frappe.get_doc("Fiscal Year", _x.name) + prev_fy.append("companies", {"company": self.company}) + prev_fy.save() + + # make SI on both of them + prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True) + prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1) + prev_fy_si.save().submit() + income_acc = prev_fy_si.items[0].income_account + + self.create_sales_invoice(qty=1, rate=120) + + # Unaccumualted + filters = frappe._dict( + company=self.company, + from_fiscal_year=prev_fy.name, + to_fiscal_year=cur_fy.name, + period_start_date=prev_fy.year_start_date, + period_end_date=cur_fy.year_end_date, + filter_based_on="Date Range", + periodicity="Yearly", + accumulated_values=False, + ) + result = execute(filters) + columns = [result[0][2], result[0][3]] + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 120.0, + } + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) + + # accumualted + filters.update({"accumulated_values": True}) + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 570.0, + } + result = execute(filters) + columns = [result[0][2], result[0][3]] + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) From 9ca96a63c33108e0d86684ddc45ed5cd72f1ef81 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 May 2025 16:30:24 +0530 Subject: [PATCH 13/25] refactor(test): don't default to accumulate (cherry picked from commit 54e4e7918ea54d145c732fa176eb19f42d4313d2) --- .../profit_and_loss_statement/test_profit_and_loss_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index 0c4ab37ae58..137c4a86f9d 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -58,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): period_end_date=fy.year_end_date, filter_based_on="Fiscal Year", periodicity="Monthly", - accumulated_values=True, + accumulated_values=False, ) def test_profit_and_loss_output_and_summary(self): From b6bf13ff02e71b81c0d686b0a6d460e30fa6e4dd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 23:29:51 +0530 Subject: [PATCH 14/25] refactor: available serial no report (backport #47333) (#47500) * refactor: available serial no report (cherry picked from commit 74eb611563ac7ad01f3ce6fa44367163fd7ac2ea) * chore: further optimizations (cherry picked from commit 653e0a2e3a17514335f0c6249221284c93c2f555) --------- Co-authored-by: Mihir Kandoi --- .../available_serial_no.js | 38 ---- .../available_serial_no.py | 162 ++++-------------- 2 files changed, 30 insertions(+), 170 deletions(-) diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.js b/erpnext/stock/report/available_serial_no/available_serial_no.js index 17f8c666e04..c69c6503de8 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.js +++ b/erpnext/stock/report/available_serial_no/available_serial_no.js @@ -51,49 +51,11 @@ frappe.query_reports["Available Serial No"] = { }; }, }, - { - fieldname: "item_group", - label: __("Item Group"), - fieldtype: "Link", - options: "Item Group", - }, - { - fieldname: "batch_no", - label: __("Batch No"), - fieldtype: "Link", - options: "Batch", - on_change() { - const batch_no = frappe.query_report.get_filter_value("batch_no"); - if (batch_no) { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 1); - } else { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 0); - } - }, - }, - { - fieldname: "brand", - label: __("Brand"), - fieldtype: "Link", - options: "Brand", - }, { fieldname: "voucher_no", label: __("Voucher #"), fieldtype: "Data", }, - { - fieldname: "project", - label: __("Project"), - fieldtype: "Link", - options: "Project", - }, - { - fieldname: "include_uom", - label: __("Include UOM"), - fieldtype: "Link", - options: "UOM", - }, { fieldname: "valuation_field_type", label: __("Valuation Field Type"), diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index bdde9c7f3b6..6911b979ae4 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -3,108 +3,62 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum from frappe.utils import cint, flt -from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list -from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for from erpnext.stock.report.stock_ledger.stock_ledger import ( - check_inventory_dimension_filters_applied, get_item_details, - get_item_group_condition, get_opening_balance, - get_opening_balance_from_batch, get_stock_ledger_entries, ) -from erpnext.stock.utils import ( - is_reposting_item_valuation_in_progress, - update_included_uom_in_report, -) +from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - include_uom = filters.get("include_uom") columns = get_columns(filters) items = get_items(filters) sl_entries = get_stock_ledger_entries(filters, items) - item_details = get_item_details(items, sl_entries, include_uom) + item_details = get_item_details(items, sl_entries, False) - opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries) + opening_row = get_opening_balance_data(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) - data, conversion_factors = process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision - ) + data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) - update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data def get_opening_balance_data(filters, columns, sl_entries): - if filters.get("batch_no"): - opening_row = get_opening_balance_from_batch(filters, columns, sl_entries) - else: - opening_row = get_opening_balance(filters, columns, sl_entries) - - actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0 - stock_value = opening_row.get("stock_value") if opening_row else 0 - return opening_row, actual_qty, stock_value + opening_row = get_opening_balance(filters, columns, sl_entries) + return opening_row -def process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision -): +def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): data = [] - conversion_factors = [] if opening_row: data.append(opening_row) - conversion_factors.append(0) - batch_balance_dict = frappe._dict({}) + available_serial_nos = {} + if sabb_list := [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]: + available_serial_nos = get_serial_nos_from_sle_list(sabb_list) - if actual_qty and filters.get("batch_no"): - batch_balance_dict[filters.batch_no] = [actual_qty, stock_value] - - available_serial_nos = get_serial_nos_from_sle_list( - [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle] - ) + if not available_serial_nos: + return [], [] for sle in sl_entries: - update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision - ) + update_stock_ledger_entry(sle, item_details, precision) update_available_serial_nos(available_serial_nos, sle) data.append(sle) - if filters.get("include_uom"): - conversion_factors.append(item_details[sle.item_code].conversion_factor) - - return data, conversion_factors + return data -def update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision -): +def update_stock_ledger_entry(sle, item_details, precision): item_detail = item_details[sle.item_code] sle.update(item_detail) - if filters.get("batch_no") or check_inventory_dimension_filters_applied(filters): - actual_qty += flt(sle.actual_qty, precision) - stock_value += sle.stock_value_difference - - if sle.batch_no: - batch_balance_dict.setdefault(sle.batch_no, [0, 0]) - batch_balance_dict[sle.batch_no][0] += sle.actual_qty - - if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: - actual_qty = sle.qty_after_transaction - stock_value = sle.stock_value - - sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.actual_qty: @@ -120,13 +74,10 @@ def update_available_serial_nos(available_serial_nos, sle): else available_serial_nos.get(sle.serial_and_batch_bundle) ) key = (sle.item_code, sle.warehouse) + sle.serial_no = "\n".join(serial_nos) if serial_nos else "" if key not in available_serial_nos: - stock_balance = get_stock_balance_for( - sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time - ) - serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else [] - available_serial_nos.setdefault(key, serials) - sle.balance_serial_no = "\n".join(serials) + available_serial_nos.setdefault(key, serial_nos) + sle.balance_serial_no = "\n".join(serial_nos) return existing_serial_no = available_serial_nos[key] @@ -151,25 +102,14 @@ def get_columns(filters): }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", - "width": 90, + "width": 60, }, ] - for dimension in get_inventory_dimensions(): - columns.append( - { - "label": _(dimension.doctype), - "fieldname": dimension.fieldname, - "fieldtype": "Link", - "options": dimension.doctype, - "width": 110, - } - ) - columns.extend( [ { @@ -201,20 +141,11 @@ def get_columns(filters): "width": 150, }, { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, + "label": _("Serial No (In/Out)"), + "fieldname": "serial_no", + "width": 150, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Link", - "options": "Brand", - "width": 100, - }, - {"label": _("Description"), "fieldname": "description", "width": 200}, + {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150}, { "label": _("Incoming Rate"), "fieldname": "incoming_rate", @@ -257,28 +188,6 @@ def get_columns(filters): "width": 110, "options": "Company:company:default_currency", }, - {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - { - "label": _("Voucher #"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 100, - }, - { - "label": _("Batch"), - "fieldname": "batch_no", - "fieldtype": "Link", - "options": "Batch", - "width": 100, - }, - { - "label": _("Serial No"), - "fieldname": "serial_no", - "fieldtype": "Link", - "options": "Serial No", - "width": 100, - }, { "label": _("Serial and Batch Bundle"), "fieldname": "serial_and_batch_bundle", @@ -286,12 +195,12 @@ def get_columns(filters): "options": "Serial and Batch Bundle", "width": 100, }, - {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, { - "label": _("Project"), - "fieldname": "project", - "fieldtype": "Link", - "options": "Project", + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", "width": 100, }, { @@ -310,19 +219,8 @@ def get_columns(filters): def get_items(filters): item = frappe.qb.DocType("Item") query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1) - conditions = [] if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) - else: - if brand := filters.get("brand"): - conditions.append(item.brand == brand) - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) - - if conditions: - for condition in conditions: - query = query.where(condition) + query = query.where(item.name == item_code) return query.run(pluck=True) From 811fe4fee6180a4204be97f59b08cf56311005f5 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Tue, 13 May 2025 11:26:21 +0530 Subject: [PATCH 15/25] Merge pull request #47367 from Abdeali099/gl-report-field-float-to-currency fix: Use `Currency` instead of `Float` in GL report to show details (cherry picked from commit e4e0bb68ecc3ff2dce679481051ff53c0a77dcd1) --- .../report/general_ledger/general_ledger.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 28554125b67..0bb14604991 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -563,17 +563,20 @@ def get_account_type_map(company): def get_result_as_list(data, filters): - balance, _balance_in_account_currency = 0, 0 + balance = 0 for d in data: if not d.get("posting_date"): - balance, _balance_in_account_currency = 0, 0 + balance = 0 balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance d["account_currency"] = filters.account_currency + d["presentation_currency"] = filters.presentation_currency + return data @@ -599,11 +602,8 @@ def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] else: - if filters.get("company"): - currency = get_company_currency(filters["company"]) - else: - company = get_default_company() - currency = get_company_currency(company) + company = filters.get("company") or get_default_company() + filters["presentation_currency"] = currency = get_company_currency(company) columns = [ { @@ -624,19 +624,22 @@ def get_columns(filters): { "label": _("Debit ({0})").format(currency), "fieldname": "debit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Balance ({0})").format(currency), "fieldname": "balance", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, ] From b6e5e3347d828a79e7ced19552dd73ad92763d64 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 13 May 2025 13:39:01 +0530 Subject: [PATCH 16/25] fix: condition for advance_account assignment (cherry picked from commit ded46ce3d899f243dd201a9888bb4aa3c7abfb6e) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1c91b9ef8e3..81b545e967a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2361,7 +2361,7 @@ def get_outstanding_reference_documents(args, validate=False): accounts = get_party_account( args.get("party_type"), args.get("party"), args.get("company"), include_advance=True ) - advance_account = accounts[1] if len(accounts) >= 1 else None + advance_account = accounts[1] if len(accounts) > 1 else None if party_account == advance_account: party_account = accounts[0] From 25fabda40ae14620d8cc8d13c994d7e347eccb68 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 8 May 2025 13:53:51 +0530 Subject: [PATCH 17/25] fix(payment-reconciliation): use reconciliation_takes_effect_on from company (cherry picked from commit 19f1ffbdc2ee986a65089f82fa9bd05805afaae1) --- .../doctype/payment_entry/payment_entry.json | 15 +++------------ .../doctype/payment_entry/payment_entry.py | 15 ++++++++++++--- erpnext/accounts/utils.py | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index c7e05f70d7f..abc4eec0ffb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -21,7 +21,6 @@ "party_name", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", - "advance_reconciliation_takes_effect_on", "column_break_11", "bank_account", "party_bank_account", @@ -786,18 +785,9 @@ "options": "No\nYes", "print_hide": 1, "search_index": 1 - }, - { - "default": "Oldest Of Invoice Or Advance", - "fetch_from": "company.reconciliation_takes_effect_on", - "fieldname": "advance_reconciliation_takes_effect_on", - "fieldtype": "Select", - "hidden": 1, - "label": "Advance Reconciliation Takes Effect On", - "no_copy": 1, - "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [ @@ -809,7 +799,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2025-03-24 16:18:19.920701", + "modified": "2025-05-08 11:18:10.238085", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -849,6 +839,7 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1c91b9ef8e3..738559c7059 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1492,9 +1492,15 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - if self.advance_reconciliation_takes_effect_on == "Advance Payment Date": + if ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Advance Payment Date" + ): posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Oldest Of Invoice Or Advance" + ): date_field = "posting_date" if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -1504,7 +1510,10 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif ( + frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") + == "Reconciliation Date" + ): posting_date = nowdate() frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b46e427382f..5449c869e82 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -714,9 +714,15 @@ def update_reference_in_payment_entry( # Update Reconciliation effect date in reference if payment_entry.book_advance_payments_in_separate_party_account: - if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date": + if ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Advance Payment Date" + ): reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Oldest Of Invoice Or Advance" + ): date_field = "posting_date" if d.against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -724,7 +730,10 @@ def update_reference_in_payment_entry( if getdate(reconcile_on) < getdate(payment_entry.posting_date): reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif ( + frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") + == "Reconciliation Date" + ): reconcile_on = nowdate() reference_details.update({"reconcile_effect_on": reconcile_on}) From bafd9ed15e2bc8b0a39d0865d307edef6f0ce984 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 8 May 2025 16:32:30 +0530 Subject: [PATCH 18/25] chore: simplify repeated condition checks (cherry picked from commit 7bc62cedc6313f511e353283c1d8d1b7b8cb111d) --- .../doctype/payment_entry/payment_entry.py | 18 ++++++------------ erpnext/accounts/utils.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 738559c7059..dd3a43faf27 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1492,15 +1492,12 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - if ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Advance Payment Date" - ): + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", self.company, "reconciliation_takes_effect_on" + ) + if reconciliation_takes_effect_on == "Advance Payment Date": posting_date = self.posting_date - elif ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Oldest Of Invoice Or Advance" - ): + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -1510,10 +1507,7 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - elif ( - frappe.get_cached_value("Company", self.company, "reconciliation_takes_effect_on") - == "Reconciliation Date" - ): + elif reconciliation_takes_effect_on == "Reconciliation Date": posting_date = nowdate() frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5449c869e82..6fa0e3e9802 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -713,16 +713,13 @@ def update_reference_in_payment_entry( update_advance_paid = [] # Update Reconciliation effect date in reference + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", payment_entry.company, "reconciliation_takes_effect_on" + ) if payment_entry.book_advance_payments_in_separate_party_account: - if ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Advance Payment Date" - ): + if reconciliation_takes_effect_on == "Advance Payment Date": reconcile_on = payment_entry.posting_date - elif ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Oldest Of Invoice Or Advance" - ): + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if d.against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -730,10 +727,7 @@ def update_reference_in_payment_entry( if getdate(reconcile_on) < getdate(payment_entry.posting_date): reconcile_on = payment_entry.posting_date - elif ( - frappe.get_cached_value("Company", payment_entry.company, "reconciliation_takes_effect_on") - == "Reconciliation Date" - ): + elif reconciliation_takes_effect_on == "Reconciliation Date": reconcile_on = nowdate() reference_details.update({"reconcile_effect_on": reconcile_on}) From 7f55d59a7b8735b0eaabc57560e2670bbfdc35f5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 13 May 2025 14:45:51 +0530 Subject: [PATCH 19/25] chore: drop redundant patch --- erpnext/patches.txt | 1 - ...kbox_to_select_for_reconciliation_effect.py | 18 ------------------ 2 files changed, 19 deletions(-) delete mode 100644 erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0bf4128551e..cf8ae44a8d7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -389,7 +389,6 @@ erpnext.patches.v15_0.enable_allow_existing_serial_no erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts erpnext.patches.v15_0.update_asset_status_to_work_in_progress erpnext.patches.v15_0.rename_manufacturing_settings_field -erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit diff --git a/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py b/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py deleted file mode 100644 index 883921cfdf8..00000000000 --- a/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py +++ /dev/null @@ -1,18 +0,0 @@ -import frappe - - -def execute(): - """ - A New select field 'reconciliation_takes_effect_on' has been added to control Advance Payment Reconciliation dates. - Migrate old checkbox configuration to new select field on 'Company' and 'Payment Entry' - """ - companies = frappe.db.get_all("Company", fields=["name", "reconciliation_takes_effect_on"]) - for x in companies: - new_value = ( - "Advance Payment Date" if x.reconcile_on_advance_payment_date else "Oldest Of Invoice Or Advance" - ) - frappe.db.set_value("Company", x.name, "reconciliation_takes_effect_on", new_value) - - frappe.db.sql( - """update `tabPayment Entry` set advance_reconciliation_takes_effect_on = if(reconcile_on_advance_payment_date = 0, 'Oldest Of Invoice Or Advance', 'Advance Payment Date')""" - ) From 39c029133f4f88e7e809feed845ee0320d590acf Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 13 May 2025 13:12:39 +0530 Subject: [PATCH 20/25] fix: ignore "Account Closing Balance" doctype on Period Closing Voucher cancellation (cherry picked from commit d6602d63fcb69d78560762e9a86c0b6bf8adbbdc) --- .../period_closing_voucher/period_closing_voucher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 790ada3f63e..2d065632419 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -133,7 +133,12 @@ class PeriodClosingVoucher(AccountsController): self.make_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Account Closing Balance", + ) self.block_if_future_closing_voucher_exists() self.db_set("gle_processing_status", "In Progress") self.cancel_gl_entries() From 96d3bfd2d9901ac740239de9a21eecd1a7cd8c99 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 13 May 2025 16:40:59 +0530 Subject: [PATCH 21/25] feat: add non depreciable category checkbox in asset category (cherry picked from commit fbbfd6531b70b3babf23e003b4481c95652cf07f) # Conflicts: # erpnext/assets/doctype/asset_category/asset_category.json --- .../doctype/asset_category/asset_category.json | 14 +++++++++++++- .../doctype/asset_category/asset_category.py | 5 ++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json index a25f5469039..73bc603017b 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.json +++ b/erpnext/assets/doctype/asset_category/asset_category.json @@ -12,6 +12,7 @@ "column_break_3", "depreciation_options", "enable_cwip_accounting", + "non_depreciable_category", "finance_book_detail", "finance_books", "section_break_2", @@ -63,10 +64,20 @@ "fieldname": "enable_cwip_accounting", "fieldtype": "Check", "label": "Enable Capital Work in Progress Accounting" + }, + { + "default": "0", + "fieldname": "non_depreciable_category", + "fieldtype": "Check", + "label": "Non Depreciable Category" } ], "links": [], +<<<<<<< HEAD "modified": "2021-02-24 15:05:38.621803", +======= + "modified": "2025-05-13 15:33:03.791814", +>>>>>>> fbbfd6531b (feat: add non depreciable category checkbox in asset category) "modified_by": "Administrator", "module": "Assets", "name": "Asset Category", @@ -111,8 +122,9 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 8c2d301a895..16e564ad1bd 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -17,15 +17,14 @@ class AssetCategory(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.assets.doctype.asset_category_account.asset_category_account import ( - AssetCategoryAccount, - ) + from erpnext.assets.doctype.asset_category_account.asset_category_account import AssetCategoryAccount from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook accounts: DF.Table[AssetCategoryAccount] asset_category_name: DF.Data enable_cwip_accounting: DF.Check finance_books: DF.Table[AssetFinanceBook] + non_depreciable_category: DF.Check # end: auto-generated types def validate(self): From a75931c90f78ca156449cdec1e9c4a81cd308191 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 13 May 2025 16:43:03 +0530 Subject: [PATCH 22/25] fix: do not mandate depreciation accounts for non depreciable asset category (cherry picked from commit 32cb7d6388f02e6748483640938b6206926667c4) --- erpnext/assets/doctype/asset/depreciation.py | 51 +++----------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ea1a95e7a71..9b4ec6f67d8 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -330,45 +330,6 @@ def _make_journal_entry_for_depreciation( row.db_update() -def get_depreciation_accounts(asset_category, company): - fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None - - accounts = frappe.db.get_value( - "Asset Category Account", - filters={"parent": asset_category, "company_name": company}, - fieldname=[ - "fixed_asset_account", - "accumulated_depreciation_account", - "depreciation_expense_account", - ], - as_dict=1, - ) - - if accounts: - fixed_asset_account = accounts.fixed_asset_account - accumulated_depreciation_account = accounts.accumulated_depreciation_account - depreciation_expense_account = accounts.depreciation_expense_account - - if not accumulated_depreciation_account or not depreciation_expense_account: - accounts = frappe.get_cached_value( - "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] - ) - - if not accumulated_depreciation_account: - accumulated_depreciation_account = accounts[0] - if not depreciation_expense_account: - depreciation_expense_account = accounts[1] - - if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account: - frappe.throw( - _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( - asset_category, company - ) - ) - - return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account - - def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") @@ -721,8 +682,8 @@ def get_asset_details(asset, finance_book=None): value_after_depreciation = asset.get_value_after_depreciation(finance_book) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) - fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts( - asset.asset_category, asset.company, accumulated_depr_amount + fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts( + asset.asset_category, asset.company ) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -738,9 +699,13 @@ def get_asset_details(asset, finance_book=None): ) -def get_asset_accounts(asset_category, company, accumulated_depr_amount): +def get_depreciation_accounts(asset_category, company): fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None + non_depreciable_category = frappe.db.get_value( + "Asset Category", asset_category, "non_depreciable_category" + ) + accounts = frappe.db.get_value( "Asset Category Account", filters={"parent": asset_category, "company_name": company}, @@ -760,7 +725,7 @@ def get_asset_accounts(asset_category, company, accumulated_depr_amount): if not fixed_asset_account: frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category)) - if accumulated_depr_amount: + if not non_depreciable_category: accounts = frappe.get_cached_value( "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] ) From 242a119f952adead8074478b772c5ded111a000e Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 13 May 2025 16:44:13 +0530 Subject: [PATCH 23/25] fix: only depreciable category assets are allowed for depreciation (cherry picked from commit d715db1226d83f49644ab5716002cd228d234e16) --- erpnext/assets/doctype/asset/asset.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e1a5398db85..bae6defe6de 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -122,6 +122,7 @@ class Asset(AccountsController): # end: auto-generated types def validate(self): + self.validate_category() self.validate_precision() self.set_purchase_doc_row_item() self.validate_asset_values() @@ -343,6 +344,17 @@ class Asset(AccountsController): title=_("Missing Finance Book"), ) + def validate_category(self): + non_depreciable_category = frappe.db.get_value( + "Asset Category", self.asset_category, "non_depreciable_category" + ) + if self.calculate_depreciation and non_depreciable_category: + frappe.throw( + _( + "This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category." + ) + ) + def validate_precision(self): if self.gross_purchase_amount: self.gross_purchase_amount = flt( From dcfae61a7a3511d8912eb2c696accd282ceb0497 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 13 May 2025 17:07:14 +0530 Subject: [PATCH 24/25] fix: resolved conflicts --- erpnext/assets/doctype/asset_category/asset_category.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json index 73bc603017b..575b7a0c45f 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.json +++ b/erpnext/assets/doctype/asset_category/asset_category.json @@ -73,11 +73,7 @@ } ], "links": [], -<<<<<<< HEAD - "modified": "2021-02-24 15:05:38.621803", -======= "modified": "2025-05-13 15:33:03.791814", ->>>>>>> fbbfd6531b (feat: add non depreciable category checkbox in asset category) "modified_by": "Administrator", "module": "Assets", "name": "Asset Category", From 56d0357f6fc0d86faf9af4ad7d6ed6f36e7d0805 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 19:02:01 +0530 Subject: [PATCH 25/25] feat: add routing/sequencing to work order operations (backport #46975) (#47534) * feat: add routing/sequencing to work order operations (#46975) * feat: add routing/sequencing to work order operations * fix: add validation and remove reorderin for non sequence id operations * chore: readability * fix: logical error * fix: logical error * chore: added row number in error message (cherry picked from commit f1159b6ea65985357a7b0bc69f012cce32ec8ead) # Conflicts: # erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- .../doctype/work_order/test_work_order.py | 75 +++++++++++++++++++ .../doctype/work_order/work_order.json | 2 +- .../doctype/work_order/work_order.py | 33 ++++++-- .../work_order_operation.json | 14 ++-- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index dfa02c03772..cd57c7c24f8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2665,6 +2665,81 @@ class TestWorkOrder(FrappeTestCase): ) frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on) + def test_operations_time_planning_calculation(self): + from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_operations + + operations = [ + {"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 1}, + {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 4}, + {"operation": "Test Operation C", "workstation": "Test Workstation A", "time_in_mins": 3}, + {"operation": "Test Operation D", "workstation": "Test Workstation A", "time_in_mins": 2}, + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom = make_bom( + item="_Test FG Item", raw_materials=["_Test Item"], with_operations=1, routing=routing_doc.name + ) + + wo = make_wo_order_test_record( + item="_Test FG Item", + bom_no=bom.name, + qty=5, + source_warehouse="_Test Warehouse 1 - _TC", + skip_transfer=1, + fg_warehouse="_Test Warehouse 2 - _TC", + ) + + # Initial check + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation C") + self.assertEqual(wo.operations[3].operation, "Test Operation D") + + wo = frappe.copy_doc(wo) + wo.operations[3].sequence_id = 2 + wo.submit() + + # Test 2 : Sort line items in child table based on sequence ID + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation D") + self.assertEqual(wo.operations[3].operation, "Test Operation C") + + wo = frappe.copy_doc(wo) + wo.operations[3].sequence_id = 1 + wo.submit() + + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation C") + self.assertEqual(wo.operations[2].operation, "Test Operation B") + self.assertEqual(wo.operations[3].operation, "Test Operation D") + + wo = frappe.copy_doc(wo) + wo.operations[0].sequence_id = 3 + wo.submit() + + self.assertEqual(wo.operations[0].operation, "Test Operation C") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation D") + self.assertEqual(wo.operations[3].operation, "Test Operation A") + + wo = frappe.copy_doc(wo) + wo.operations[1].sequence_id = 0 + + # Test 3 - Error should be thrown if any one operation does not have sequence id but others do + self.assertRaises(frappe.ValidationError, wo.submit) + + workstation = frappe.get_doc("Workstation", "Test Workstation A") + workstation.production_capacity = 4 + workstation.save() + + wo = frappe.copy_doc(wo) + wo.operations[1].sequence_id = 2 + wo.submit() + + # Test 4 - If Sequence ID is same then planned start time for both operations should be same + self.assertEqual(wo.operations[1].planned_start_time, wo.operations[2].planned_start_time) + def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 63c74b61c4d..8231e924cb0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-01-10 16:34:16", + "creation": "2025-04-09 12:09:40.634472", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0bf383de285..270c23e913d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -624,19 +624,30 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - for index, row in enumerate(self.operations): + if all([op.sequence_id for op in self.operations]): + self.operations = sorted(self.operations, key=lambda op: op.sequence_id) + for idx, op in enumerate(self.operations): + op.idx = idx + 1 + elif any([op.sequence_id for op in self.operations]): + frappe.throw( + _( + "Row #{0}: Incorrect Sequence ID. If any single operation has a Sequence ID then all other operations must have one too." + ).format(next((op.idx for op in self.operations if not op.sequence_id), None)) + ) + + for idx, row in enumerate(self.operations): qty = self.qty while qty > 0: qty = split_qty_based_on_batch_size(self, row, qty) if row.job_card_qty > 0: - self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) + self.prepare_data_for_job_card(row, idx, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) - def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): - self.set_operation_start_end_time(index, row) + def prepare_data_for_job_card(self, row, idx, plan_days, enable_capacity_planning): + self.set_operation_start_end_time(row, idx) job_card_doc = create_job_card( self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning @@ -661,12 +672,24 @@ class WorkOrder(Document): row.db_update() - def set_operation_start_end_time(self, idx, row): + def set_operation_start_end_time(self, row, idx): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" if idx == 0: # first operation at planned_start date row.planned_start_time = self.planned_start_date + elif self.operations[idx - 1].sequence_id: + if self.operations[idx - 1].sequence_id == row.sequence_id: + row.planned_start_time = self.operations[idx - 1].planned_start_time + else: + last_ops_with_same_sequence_ids = sorted( + [op for op in self.operations if op.sequence_id == self.operations[idx - 1].sequence_id], + key=lambda op: get_datetime(op.planned_end_time), + ) + row.planned_start_time = ( + get_datetime(last_ops_with_same_sequence_ids[-1].planned_end_time) + + get_mins_between_operations() + ) else: row.planned_start_time = ( get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index de1f67f13fd..0185812a4b6 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,6 +1,6 @@ { "actions": [], - "creation": "2014-10-16 14:35:41.950175", + "creation": "2025-04-09 12:12:19.824560", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -102,13 +102,15 @@ "fieldname": "planned_start_time", "fieldtype": "Datetime", "label": "Planned Start Time", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "planned_end_time", "fieldtype": "Datetime", "label": "Planned End Time", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "column_break_10", @@ -191,7 +193,6 @@ { "fieldname": "sequence_id", "fieldtype": "Int", - "hidden": 1, "label": "Sequence ID", "print_hide": 1 }, @@ -219,10 +220,11 @@ "read_only": 1 } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-06-09 14:03:01.612909", + "modified": "2025-04-09 16:21:47.110564", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -232,4 +234,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +}