From c0ae1336f43cbaed5a1a226279a70252ae6500a7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:26:03 +0200 Subject: [PATCH 01/41] fix(Rename Tool): allow more than 500 rows (backport #47117) (#47225) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix(Rename Tool): allow more than 500 rows (#47117) --- .../doctype/rename_tool/rename_tool.js | 67 +++++++++++++++---- .../doctype/rename_tool/rename_tool.py | 9 ++- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js index 1b8b2be2610..47677a62500 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.js +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js @@ -18,29 +18,70 @@ frappe.ui.form.on("Rename Tool", { allowed_file_types: [".csv"], }, }; - if (!frm.doc.file_to_rename) { - frm.get_field("rename_log").$wrapper.html(""); - } + + frm.trigger("render_overview"); + frm.page.set_primary_action(__("Rename"), function () { - frm.get_field("rename_log").$wrapper.html("

Renaming...

"); frappe.call({ method: "erpnext.utilities.doctype.rename_tool.rename_tool.upload", args: { select_doctype: frm.doc.select_doctype, }, - callback: function (r) { - let html = r.message.join("
"); + freeze: true, + freeze_message: __("Scheduling..."), + callback: function () { + frappe.msgprint({ + message: __("Rename jobs for doctype {0} have been enqueued.", [ + frm.doc.select_doctype, + ]), + alert: true, + indicator: "green", + }); + frm.set_value("select_doctype", ""); + frm.set_value("file_to_rename", ""); - if (r.exc) { - r.exc = frappe.utils.parse_json(r.exc); - if (Array.isArray(r.exc)) { - html += "
" + r.exc.join("
"); - } - } + frm.trigger("render_overview"); + }, + error: function (r) { + frappe.msgprint({ + message: __("Rename jobs for doctype {0} have not been enqueued.", [ + frm.doc.select_doctype, + ]), + alert: true, + indicator: "red", + }); - frm.get_field("rename_log").$wrapper.html(html); + frm.trigger("render_overview"); }, }); }); }, + render_overview: function (frm) { + frappe.db + .get_list("RQ Job", { filters: { status: ["in", ["started", "queued", "finished", "failed"]] } }) + .then((jobs) => { + let counts = { + started: 0, + queued: 0, + finished: 0, + failed: 0, + }; + + for (const job of jobs) { + if (job.job_name !== "frappe.model.rename_doc.bulk_rename") { + continue; + } + + counts[job.status]++; + } + + frm.get_field("rename_log").$wrapper.html(` +

${__("Bulk Rename Jobs")}

+

${__("Queued")}: ${counts.queued}

+

${__("Started")}: ${counts.started}

+

${__("Finished")}: ${counts.finished}

+

${__("Failed")}: ${counts.failed}

+ `); + }); + }, }); diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 19b29f79aa1..230845e55de 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -45,4 +45,11 @@ def upload(select_doctype=None, rows=None): rows = read_csv_content_from_attached_file(frappe.get_doc("Rename Tool", "Rename Tool")) - return bulk_rename(select_doctype, rows=rows) + # bulk rename allows only 500 rows at a time, so we created one job per 500 rows + for i in range(0, len(rows), 500): + frappe.enqueue( + method=bulk_rename, + queue="long", + doctype=select_doctype, + rows=rows[i : i + 500], + ) From c140fd0f12bafd857f17d00972031667b5be2aec Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:36:18 +0530 Subject: [PATCH 02/41] fix: make asset quantity and amount editable (backport #47226) (#47227) fix: make asset quantity and amount editable (#47226) (cherry picked from commit 0d53e6ed7ccc5a44b0a4068237637f033632b6c5) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- erpnext/assets/doctype/asset/asset.js | 4 ---- erpnext/assets/doctype/asset/asset.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index b9ea888faf7..3ae91f0593b 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -658,10 +658,6 @@ frappe.ui.form.on("Asset", { } else { frm.set_value("purchase_invoice_item", data.purchase_invoice_item); } - - let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only - frm.set_df_property("gross_purchase_amount", "read_only", is_editable); - frm.set_df_property("asset_quantity", "read_only", is_editable); } }, }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6886a79bb0a..e1a5398db85 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1169,7 +1169,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}")) first_item = matching_items[0] - is_multiple_items = len(matching_items) > 1 return { "company": purchase_doc.company, @@ -1178,7 +1177,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): "asset_quantity": first_item.qty, "cost_center": first_item.cost_center or purchase_doc.get("cost_center"), "asset_location": first_item.get("asset_location"), - "is_multiple_items": is_multiple_items, "purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None, "purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None, } From 4a29a54804df8355c2ec277bb61cb6bf000923e3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:36:53 +0530 Subject: [PATCH 03/41] fix: update additional cost and total asset cost after asset repair (backport #47233) (#47235) fix: update additional cost and total asset cost after asset repair (#47233) * fix: add consumed stock's cost to the asset value after repair * fix: do not copy additional cost and total asset cost (cherry picked from commit ed8a8532e169b669d55380a3282b992b6117d7f8) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- erpnext/assets/doctype/asset/asset.json | 4 +++- erpnext/assets/doctype/asset_repair/asset_repair.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index cf3602ef966..83873f670ad 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -512,6 +512,7 @@ "fieldname": "total_asset_cost", "fieldtype": "Currency", "label": "Total Asset Cost", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -520,6 +521,7 @@ "fieldname": "additional_asset_cost", "fieldtype": "Currency", "label": "Additional Asset Cost", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -593,7 +595,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2025-04-15 16:33:17.189524", + "modified": "2025-04-24 15:31:47.373274", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4e73148828d..3938ae06b50 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -98,9 +98,11 @@ class AssetRepair(AccountsController): self.increase_asset_value() + total_repair_cost = self.get_total_value_of_stock_consumed() if self.capitalize_repair_cost: - self.asset_doc.total_asset_cost += self.repair_cost - self.asset_doc.additional_asset_cost += self.repair_cost + total_repair_cost += self.repair_cost + self.asset_doc.total_asset_cost += total_repair_cost + self.asset_doc.additional_asset_cost += total_repair_cost if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() @@ -139,9 +141,11 @@ class AssetRepair(AccountsController): self.decrease_asset_value() + total_repair_cost = self.get_total_value_of_stock_consumed() if self.capitalize_repair_cost: - self.asset_doc.total_asset_cost -= self.repair_cost - self.asset_doc.additional_asset_cost -= self.repair_cost + total_repair_cost += self.repair_cost + self.asset_doc.total_asset_cost -= total_repair_cost + self.asset_doc.additional_asset_cost -= total_repair_cost if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") From 0caba9f70dbefc7aa2114dcd69959a6cf1ea7cb5 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:33:30 +0530 Subject: [PATCH 04/41] fix: do not check for permission if values are not changed in employee doctype (#47238) --- erpnext/setup/doctype/employee/employee.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 41bc41d5d34..eb5284019da 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,9 +85,10 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not has_permission("User Permission", ptype="write") or ( - not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission") - ): + if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"): + return + + if not has_permission("User Permission", ptype="write", raise_exception=False): return employee_user_permission_exists = frappe.db.exists( From 10d843e49061e21ae9460c981fe5938545be1b5c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:23:08 +0530 Subject: [PATCH 05/41] fix: cancel pos closing entry failure for return pos invoices (backport #47248) (#47249) fix: cancel pos closing entry failure for return pos invoices (#47248) (cherry picked from commit c8ee5d9a4e1e29d595e8bfe02096d930937b963a) Co-authored-by: Diptanil Saha --- .../doctype/pos_invoice_merge_log/pos_invoice_merge_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index a8eba648545..96b3be79664 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -336,7 +336,7 @@ class POSInvoiceMergeLog(Document): for doc in invoice_docs: doc.load_from_db() inv = sales_invoice - if doc.is_return: + if doc.is_return and credit_notes: for key, value in credit_notes.items(): if doc.name in value: inv = key From 4bcea5556337e9dc5ccaa4d8fc55bb7e54fe1d81 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:33:04 +0530 Subject: [PATCH 06/41] fix: prohibit consolidated sales invoice return (backport #47251) (#47252) fix: prohibit consolidated sales invoice return (#47251) (cherry picked from commit 483c4a327169b58464c22038a308c35637cb0ed5) Co-authored-by: Diptanil Saha --- erpnext/controllers/sales_and_purchase_return.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 74c5e34ecfa..d5aa7823751 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -347,6 +347,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai "Company", company, "default_warehouse_for_sales_return" ) + if doctype == "Sales Invoice": + inv_is_consolidated, inv_is_pos = frappe.db.get_value( + "Sales Invoice", source_name, ["is_consolidated", "is_pos"] + ) + if inv_is_consolidated and inv_is_pos: + frappe.throw( + _("Cannot create return for consolidated invoice {0}.").format(source_name), + title=_("Cannot Create Return"), + ) + def set_missing_values(source, target): doc = frappe.get_doc(target) doc.is_return = 1 From 727c32d7898d2d5a48780d474d6a6ecaaa456221 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:50:43 +0200 Subject: [PATCH 07/41] fix: Re-insert missing "Serial No Warranty Expiry" Report (cherry picked from commit deefac0abfe295b59e55203982bb0ebf3fb3c83e) --- .../serial_no_warranty_expiry.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json index 75e2fac98fd..2f6acad6557 100644 --- a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json +++ b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json @@ -10,14 +10,14 @@ "is_standard": "Yes", "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", "letterhead": null, - "modified": "2024-09-26 13:07:23.451182", + "modified": "2025-04-24 13:07:23.451182", "modified_by": "Administrator", "module": "Stock", - "name": "Serial No Service Contract Expiry", + "name": "Serial No Warranty Expiry", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "Serial No", - "report_name": "Serial No Service Contract Expiry", + "report_name": "Serial No Warranty Expiry", "report_type": "Report Builder", "roles": [ { From f8da1599bbb4ade1f628f64c9617d058e58f0205 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:59:15 +0530 Subject: [PATCH 08/41] fix: consolidating pos invoices on the basis of accounting dimensions (backport #46961) (#47265) fix: consolidating pos invoices on the basis of accounting dimensions (#46961) * fix: consolidating pos invoices on the basis of accounting dimensions * fix: project field (cherry picked from commit c85edc3346dde165925aa32931bc448f4b89d697) Co-authored-by: Diptanil Saha --- .../pos_invoice_merge_log.py | 74 +++++++++++++++---- .../test_pos_invoice_merge_log.py | 55 ++++++++++++++ 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 96b3be79664..8216a9e7259 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -2,6 +2,7 @@ # For license information, please see license.txt +import hashlib import json import frappe @@ -302,10 +303,17 @@ class POSInvoiceMergeLog(Document): accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions] dimension_values = frappe.db.get_value( - "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1 + "POS Profile", + {"name": invoice.pos_profile}, + [*accounting_dimensions_fields, "cost_center", "project"], + as_dict=1, ) for dimension in accounting_dimensions: - dimension_value = dimension_values.get(dimension.fieldname) + dimension_value = ( + data[0].get(dimension.fieldname) + if data[0].get(dimension.fieldname) + else dimension_values.get(dimension.fieldname) + ) if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs): frappe.throw( @@ -317,6 +325,14 @@ class POSInvoiceMergeLog(Document): invoice.set(dimension.fieldname, dimension_value) + invoice.set( + "cost_center", + data[0].get("cost_center") if data[0].get("cost_center") else dimension_values.get("cost_center"), + ) + invoice.set( + "project", data[0].get("project") if data[0].get("project") else dimension_values.get("project") + ) + if self.merge_invoices_based_on == "Customer Group": invoice.flags.ignore_pos_profile = True invoice.pos_profile = "" @@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices): pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map[customer].append(invoice) + for customer, invoices in pos_invoice_customer_map.items(): + pos_invoice_customer_map[customer] = split_invoices_by_accounting_dimension(invoices) + return pos_invoice_customer_map +def split_invoices_by_accounting_dimension(pos_invoices): + # pos_invoices = { + # {'dim_field1': 'dim_field1_value1', 'dim_field2': 'dim_field2_value1'}: [], + # {'dim_field1': 'dim_field1_value2', 'dim_field2': 'dim_field2_value1'}: [] + # } + pos_invoice_accounting_dimensions_map = {} + for invoice in pos_invoices: + dimension_fields = [d.fieldname for d in get_checks_for_pl_and_bs_accounts()] + accounting_dimensions = frappe.db.get_value( + "POS Invoice", invoice.pos_invoice, [*dimension_fields, "cost_center", "project"], as_dict=1 + ) + + accounting_dimensions_dic_hash = hashlib.sha256( + json.dumps(accounting_dimensions).encode() + ).hexdigest() + + pos_invoice_accounting_dimensions_map.setdefault(accounting_dimensions_dic_hash, []) + pos_invoice_accounting_dimensions_map[accounting_dimensions_dic_hash].append(invoice) + + return pos_invoice_accounting_dimensions_map + + def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) if frappe.flags.in_test and not invoices: @@ -532,20 +573,21 @@ def split_invoices(invoices): def create_merge_logs(invoice_by_customer, closing_entry=None): try: - for customer, invoices in invoice_by_customer.items(): - for _invoices in split_invoices(invoices): - merge_log = frappe.new_doc("POS Invoice Merge Log") - merge_log.posting_date = ( - getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() - ) - merge_log.posting_time = ( - get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() - ) - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - merge_log.set("pos_invoices", _invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() + for customer, invoices_acc_dim in invoice_by_customer.items(): + for invoices in invoices_acc_dim.values(): + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc("POS Invoice Merge Log") + merge_log.posting_date = ( + getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + ) + merge_log.posting_time = ( + get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() + ) + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None + merge_log.set("pos_invoices", _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() if closing_entry: closing_entry.set_status(update=True, status="Submitted") closing_entry.db_set("error_message", "") diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index e0d37436be5..be7206fc9bd 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -455,3 +455,58 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + def test_separate_consolidated_invoice_for_different_accounting_dimensions(self): + """ + Creating 3 POS Invoices where first POS Invoice has different Cost Center than the other two. + Consolidate the Invoices. + Check whether the first POS Invoice is consolidated with a separate Sales Invoice than the other two. + Check whether the second and third POS Invoice are consolidated with the same Sales Invoice. + """ + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + frappe.db.sql("delete from `tabPOS Invoice`") + + create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0) + create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0) + + try: + test_user, pos_profile = init_user_and_profile() + + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.cost_center = "_Test POS Cost Center 1 - _TC" + pos_inv.save() + pos_inv.submit() + + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv2.save() + pos_inv2.submit() + + pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv3.save() + pos_inv3.submit() + + consolidate_pos_invoices() + + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + + pos_inv2.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice)) + + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + + self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") From b2294ed6e3380f9f43530f7fb11674cc20a45793 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 26 Apr 2025 16:31:37 +0530 Subject: [PATCH 09/41] fix: allow to change valuation method from FIFO to Moving Average (cherry picked from commit b454ed4b8f7f4c549a6590ad93b4842bc3a5bda7) --- erpnext/stock/doctype/item/item.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 003e0d4d3a0..c07ea6cdac1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -970,6 +970,11 @@ class Item(Document): changed_fields = [ field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field)) ] + + # Allow to change valuation method from FIFO to Moving Average not vice versa + if self.valuation_method == "Moving Average" and "valuation_method" in changed_fields: + changed_fields.remove("valuation_method") + if not changed_fields: return From 925cc40eface504ff2885343d2376de13c3af05a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Apr 2025 14:45:01 +0530 Subject: [PATCH 10/41] fix: enable use serial / batch fields on batch selection (cherry picked from commit a4471865a9485cd9aab33abca8f86354741c474f) --- erpnext/public/js/controllers/transaction.js | 18 ++++++++++-------- .../stock/doctype/stock_entry/stock_entry.js | 16 ++++++++++++++++ .../stock_reconciliation.js | 17 +++++++++++++++-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 59dd337d3af..1fcdd459a3f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -792,6 +792,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return; } + if (item.serial_no) { + item.use_serial_batch_fields = 1 + } + if (item && item.serial_no) { if (!item.item_code) { this.frm.trigger("item_code", cdt, cdn); @@ -1355,13 +1359,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } - batch_no(doc, cdt, cdn) { - let item = frappe.get_doc(cdt, cdn); - if (!this.is_a_mapped_document(item)) { - this.apply_price_list(item, true); - } - } - toggle_conversion_factor(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1587,7 +1584,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe batch_no(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.use_serial_batch_fields && row.batch_no) { + + if (row.batch_no) { + row.use_serial_batch_fields = 1 + } + + if (row.batch_no) { var params = this._get_args(row); params.batch_no = row.batch_no; params.uom = row.uom; diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0c619b22a33..223789fc8a3 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -950,6 +950,15 @@ frappe.ui.form.on("Stock Entry Detail", { }, batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.batch_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + } + validate_sample_quantity(frm, cdt, cdn); }, @@ -1074,6 +1083,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle serial_no(doc, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); + if (item.serial_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + } + if (item?.serial_no) { // Replace all occurences of comma with line feed item.serial_no = item.serial_no.replace(/,/g, "\n"); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 9307eee46f1..44dd2952409 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -289,8 +289,16 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, - batch_no: function (frm, cdt, cdn) { - frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); + batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.batch_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); + } }, qty: function (frm, cdt, cdn) { @@ -310,6 +318,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { var child = locals[cdt][cdn]; if (child.serial_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + const serial_nos = child.serial_no.trim().split("\n"); frappe.model.set_value(cdt, cdn, "qty", serial_nos.length); } From 3e733f6ba1029b4b693d4a3bbfd3ca07d65c8128 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 15:24:59 +0200 Subject: [PATCH 11/41] fix(PE): Set account types in get_payment_entry (backport #47246) (#47266) Co-authored-by: Corentin Forler <10946971+cogk@users.noreply.github.com> fix(PE): Set account types in get_payment_entry (#47246) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 00c9a337499..1f9747a77cb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2941,6 +2941,8 @@ def get_payment_entry( party_account_currency if payment_type == "Receive" else bank.account_currency ) pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency + pe.paid_from_account_type = frappe.db.get_value("Account", pe.paid_from, "account_type") + pe.paid_to_account_type = frappe.db.get_value("Account", pe.paid_to, "account_type") pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") From 5f101e763565f4146a44103ba94f56f670d69111 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Wed, 9 Apr 2025 19:19:47 +0530 Subject: [PATCH 12/41] feat: add dispatch address fields to purchase doctypes (cherry picked from commit 54b5205221fe5447b01938abda5fc25b90892994) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json # erpnext/buying/doctype/purchase_order/purchase_order.json --- .../purchase_invoice/purchase_invoice.json | 31 ++++++++++++++- .../purchase_invoice/purchase_invoice.py | 2 + .../purchase_order/purchase_order.json | 39 ++++++++++++++++++- .../doctype/purchase_order/purchase_order.py | 2 + .../purchase_receipt/purchase_receipt.json | 22 ++++++++++- .../purchase_receipt/purchase_receipt.py | 2 + 6 files changed, 93 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 0584b6026a7..b7e110e6a69 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -144,8 +144,10 @@ "contact_email", "company_shipping_address_section", "shipping_address", - "column_break_126", "shipping_address_display", + "column_break_126", + "dispatch_address", + "dispatch_address_display", "company_billing_address_section", "billing_address", "column_break_130", @@ -1627,13 +1629,37 @@ "fieldname": "update_outstanding_for_self", "fieldtype": "Check", "label": "Update Outstanding for Self" +<<<<<<< HEAD +======= + }, + { + "fieldname": "sender", + "fieldtype": "Data", + "label": "Sender", + "options": "Email" + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Select Dispatch Address ", + "options": "Address", + "print_hide": 1 +>>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) } ], + "grid_page_length": 50, "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2025-01-14 11:39:04.564610", + "modified": "2025-04-09 16:49:22.175081", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1688,6 +1714,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 98d1850bd8f..805e76ad64c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -117,6 +117,8 @@ class PurchaseInvoice(BuyingController): currency: DF.Link | None disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None due_date: DF.Date | None from_date: DF.Date | None grand_total: DF.Currency diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 8f4b035361a..3eda1789140 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -110,8 +110,10 @@ "contact_email", "shipping_address_section", "shipping_address", - "column_break_99", "shipping_address_display", + "column_break_99", + "dispatch_address", + "dispatch_address_display", "company_billing_address_section", "billing_address", "column_break_103", @@ -1269,13 +1271,47 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 +<<<<<<< HEAD +======= + }, + { + "fieldname": "advance_payment_status", + "fieldtype": "Select", + "hidden": 1, + "in_standard_filter": 1, + "label": "Advance Payment Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid", + "print_hide": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Dispatch Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address Details", + "print_hide": 1, + "read_only": 1 +>>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) } ], + "grid_page_length": 50, "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-03-20 16:03:31.611808", +======= + "modified": "2025-04-09 16:54:08.836106", +>>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1322,6 +1358,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date, supplier, grand_total", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index ee8ba35222f..a18d9fce186 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -92,6 +92,8 @@ class PurchaseOrder(BuyingController): customer_name: DF.Data | None disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None from_date: DF.Date | None grand_total: DF.Currency group_same_items: DF.Check diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 643f9e7a82f..8fce3be270e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -113,8 +113,10 @@ "contact_email", "section_break_98", "shipping_address", - "column_break_100", "shipping_address_display", + "column_break_100", + "dispatch_address", + "dispatch_address_display", "billing_address_section", "billing_address", "column_break_104", @@ -1267,13 +1269,28 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Dispatch Address Template", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 } ], + "grid_page_length": 50, "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-11-13 16:55:14.129055", + "modified": "2025-04-09 16:52:19.323878", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1334,6 +1351,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, posting_date, supplier", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 48e161980db..b7cf97589af 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -69,6 +69,8 @@ class PurchaseReceipt(BuyingController): currency: DF.Link disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None grand_total: DF.Currency group_same_items: DF.Check ignore_pricing_rule: DF.Check From 1fe1563daba678875c95ca26e193d94bd1c2039a Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 14 Apr 2025 16:41:31 +0530 Subject: [PATCH 13/41] feat: add dispatch address support in party details and controllers (cherry picked from commit 53d0b7be232a8af935dfbbee545cd86e9d5a290a) --- erpnext/accounts/party.py | 38 ++++++++++++++++++++++++ erpnext/controllers/buying_controller.py | 2 ++ erpnext/public/js/controllers/buying.js | 12 ++++++++ erpnext/public/js/utils/party.js | 4 +++ 4 files changed, 56 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5ca50b8d7c6..ca63abeb282 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -71,6 +71,7 @@ def get_party_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, pos_profile=None, ): if not party: @@ -92,6 +93,7 @@ def get_party_details( party_address, company_address, shipping_address, + dispatch_address, pos_profile, ) @@ -111,6 +113,7 @@ def _get_party_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, pos_profile=None, ): party_details = frappe._dict( @@ -134,6 +137,7 @@ def _get_party_details( party_address, company_address, shipping_address, + dispatch_address, ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) @@ -191,6 +195,7 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, *, ignore_permissions=False, ): @@ -219,6 +224,21 @@ def set_address_details( get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) ) + # dispatch address + elif party_type == "Supplier": + party_details.dispatch_address = dispatch_address or get_party_shipping_address( + party_type, party.name + ) + + party_details.dispatch_address_display = render_address( + party_details["dispatch_address"], check_permissions=not ignore_permissions + ) + + if doctype: + party_details.update( + get_fetch_values(doctype, "dispatch_address", party_details.dispatch_address) + ) + if company_address: party_details.company_address = company_address else: @@ -256,6 +276,24 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) + if doctype != "Supplier Quotation": + if dispatch_address: + party_details.update( + dispatch_address=dispatch_address, + dispatch_address_display=render_address( + dispatch_address, check_permissions=not ignore_permissions + ), + **get_fetch_values(doctype, "dispatch_address", dispatch_address), + ) + + # dispatch address - if not already set + if not party_details.dispatch_address: + party_details.update( + dispatch_address=party_details.supplier_address, + dispatch_address_display=party_details.address_display, + **get_fetch_values(doctype, "dispatch_address", party_details.supplier_address), + ) + party_address, shipping_address = ( party_details.get(billing_address_field), party_details.shipping_address_name, diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c2a36ac36d0..267b0ed5dd5 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -141,6 +141,7 @@ class BuyingController(SubcontractingController): company=self.company, party_address=self.get("supplier_address"), shipping_address=self.get("shipping_address"), + dispatch_address=self.get("dispatch_address"), company_address=self.get("billing_address"), fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), ignore_permissions=self.flags.ignore_permissions, @@ -238,6 +239,7 @@ class BuyingController(SubcontractingController): address_dict = { "supplier_address": "address_display", "shipping_address": "shipping_address_display", + "dispatch_address": "dispatch_address_display", "billing_address": "billing_address_display", } diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cbc59867e46..e0ed9f2b91e 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -54,6 +54,18 @@ erpnext.buying = { return erpnext.queries.company_address_query(this.frm.doc) }); } + + if(this.frm.get_field('dispatch_address')) { + this.frm.set_query("dispatch_address", () => { + if(this.frm.doc.supplier) { + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { link_doctype: 'Supplier', link_name: this.frm.doc.supplier } + }; + } else + return erpnext.queries.dispatch_address_query(this.frm.doc) + }); + } } setup_queries(doc, cdt, cdn) { diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a85423b8340..958defa32c7 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -71,6 +71,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) { if (!args.shipping_address && frm.doc.shipping_address) { args.shipping_address = frm.doc.shipping_address; } + + if (!args.dispatch_address && frm.doc.dispatch_address) { + args.dispatch_address = frm.doc.dispatch_address; + } } if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { From 290f0b94e56cbe92c684fa50b19f69fdef2a8f95 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 14 Apr 2025 18:06:44 +0530 Subject: [PATCH 14/41] fix: enhance dispatch address query logic and add supplier address query (cherry picked from commit 9a859e54b62d914a574b6e0005c8daebc1f21a86) --- erpnext/public/js/controllers/buying.js | 12 +++++------- erpnext/public/js/queries.js | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e0ed9f2b91e..a39a4ddf13a 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -57,13 +57,11 @@ erpnext.buying = { if(this.frm.get_field('dispatch_address')) { this.frm.set_query("dispatch_address", () => { - if(this.frm.doc.supplier) { - return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { link_doctype: 'Supplier', link_name: this.frm.doc.supplier } - }; - } else - return erpnext.queries.dispatch_address_query(this.frm.doc) + if(this.frm.doc.is_return){ + return erpnext.queries.company_address_query(this.frm.doc); + } + + return erpnext.queries.supplier_address_query(this.frm.doc); }); } } diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 63651ec8759..cfc602acf00 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -112,6 +112,23 @@ $.extend(erpnext.queries, { }; }, + supplier_address_query: function (doc) { + if (!doc.supplier) { + cur_frm.scroll_to_field("supplier"); + frappe.show_alert({ + message: __("Please set {0} first.", [ + __(frappe.meta.get_label(doc.doctype, "supplier", doc.name)), + ]), + indicator: "orange", + }); + } + + return { + query: "frappe.contacts.doctype.address.address.address_query", + filters: { link_doctype: "Supplier", link_name: doc.supplier }, + }; + }, + dispatch_address_query: function (doc) { var filters = { link_doctype: "Company", link_name: doc.company || "" }; var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier); From 5c300b893b49131f2d3fda27823619925fddea78 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 14 Apr 2025 18:48:32 +0530 Subject: [PATCH 15/41] fix: remove use of cur_frm (cherry picked from commit c4bd3123fb8e6822d267469ffe652e13381d2741) --- erpnext/public/js/controllers/buying.js | 2 +- erpnext/public/js/queries.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a39a4ddf13a..630c6609c7b 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -61,7 +61,7 @@ erpnext.buying = { return erpnext.queries.company_address_query(this.frm.doc); } - return erpnext.queries.supplier_address_query(this.frm.doc); + return erpnext.queries.supplier_address_query(this.frm); }); } } diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index cfc602acf00..801a78dad6a 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -112,12 +112,12 @@ $.extend(erpnext.queries, { }; }, - supplier_address_query: function (doc) { - if (!doc.supplier) { - cur_frm.scroll_to_field("supplier"); + supplier_address_query: function (frm) { + if (!frm.doc.supplier) { + frm.scroll_to_field("supplier"); frappe.show_alert({ message: __("Please set {0} first.", [ - __(frappe.meta.get_label(doc.doctype, "supplier", doc.name)), + __(frappe.meta.get_label(frm.doc.doctype, "supplier", frm.doc.name)), ]), indicator: "orange", }); @@ -125,7 +125,7 @@ $.extend(erpnext.queries, { return { query: "frappe.contacts.doctype.address.address.address_query", - filters: { link_doctype: "Supplier", link_name: doc.supplier }, + filters: { link_doctype: "Supplier", link_name: frm.doc.supplier }, }; }, From 93ea2f93b686a5501f112e8b2fa5832982ff58f2 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 17 Apr 2025 16:32:59 +0530 Subject: [PATCH 16/41] feat: add display dispatch address when dispatch address is selected (cherry picked from commit d12998e524469f2e49c9660a3043dca0188352f0) --- erpnext/public/js/controllers/buying.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 630c6609c7b..c14a0d0cdc2 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -305,6 +305,12 @@ erpnext.buying = { "shipping_address_display", true); } + dispatch_address(){ + var me = this; + erpnext.utils.get_address_display(this.frm, "dispatch_address", + "dispatch_address_display", true); + } + billing_address() { erpnext.utils.get_address_display(this.frm, "billing_address", "billing_address_display", true); From ac3b2ba0032eb40e61bb7cdc3eee398bb6563874 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Fri, 18 Apr 2025 13:24:47 +0530 Subject: [PATCH 17/41] fix: correct query for dispatch_address; remove unnecessary code; increase reusability; (cherry picked from commit 999ffe86a7ef3ca3227290817d5ee6d57ffb8819) --- erpnext/accounts/party.py | 69 ++++++++++--------------- erpnext/public/js/controllers/buying.js | 6 +-- erpnext/public/js/queries.js | 17 ------ 3 files changed, 27 insertions(+), 65 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index ca63abeb282..ab00097e426 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -202,43 +202,44 @@ def set_address_details( billing_address_field = ( "customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address" ) + party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) if doctype: party_details.update( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) ) + # address display party_details.address_display = render_address( party_details[billing_address_field], check_permissions=not ignore_permissions ) - # shipping address + + # Initialize shipping address fields based on party type if party_type in ["Customer", "Lead"]: - party_details.shipping_address_name = shipping_address or get_party_shipping_address( - party_type, party.name - ) - party_details.shipping_address = render_address( - party_details["shipping_address_name"], check_permissions=not ignore_permissions - ) - if doctype: - party_details.update( - get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) + party_shipping_address_field = "shipping_address_name" + party_shipping_address_display_field = "shipping_address" + is_party_type_supplier = False + + else: # Default to Supplier + party_shipping_address_field = "dispatch_address" + party_shipping_address_display_field = "dispatch_address_display" + is_party_type_supplier = True + + party_details[party_shipping_address_field] = ( + dispatch_address if is_party_type_supplier else shipping_address + ) or get_party_shipping_address(party_type, party.name) + + party_details[party_shipping_address_display_field] = render_address( + party_details[party_shipping_address_field], check_permissions=not ignore_permissions + ) + + if doctype: + party_details.update( + get_fetch_values( + doctype, party_shipping_address_field, party_details[party_shipping_address_field] ) - - # dispatch address - elif party_type == "Supplier": - party_details.dispatch_address = dispatch_address or get_party_shipping_address( - party_type, party.name ) - party_details.dispatch_address_display = render_address( - party_details["dispatch_address"], check_permissions=not ignore_permissions - ) - - if doctype: - party_details.update( - get_fetch_values(doctype, "dispatch_address", party_details.dispatch_address) - ) - if company_address: party_details.company_address = company_address else: @@ -276,27 +277,9 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) - if doctype != "Supplier Quotation": - if dispatch_address: - party_details.update( - dispatch_address=dispatch_address, - dispatch_address_display=render_address( - dispatch_address, check_permissions=not ignore_permissions - ), - **get_fetch_values(doctype, "dispatch_address", dispatch_address), - ) - - # dispatch address - if not already set - if not party_details.dispatch_address: - party_details.update( - dispatch_address=party_details.supplier_address, - dispatch_address_display=party_details.address_display, - **get_fetch_values(doctype, "dispatch_address", party_details.supplier_address), - ) - party_address, shipping_address = ( party_details.get(billing_address_field), - party_details.shipping_address_name, + party_details.get(party_shipping_address_field), ) party_details["tax_category"] = get_address_tax_category( diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index c14a0d0cdc2..2b854d649d8 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -57,11 +57,7 @@ erpnext.buying = { if(this.frm.get_field('dispatch_address')) { this.frm.set_query("dispatch_address", () => { - if(this.frm.doc.is_return){ - return erpnext.queries.company_address_query(this.frm.doc); - } - - return erpnext.queries.supplier_address_query(this.frm); + return erpnext.queries.address_query(this.frm.doc); }); } } diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 801a78dad6a..63651ec8759 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -112,23 +112,6 @@ $.extend(erpnext.queries, { }; }, - supplier_address_query: function (frm) { - if (!frm.doc.supplier) { - frm.scroll_to_field("supplier"); - frappe.show_alert({ - message: __("Please set {0} first.", [ - __(frappe.meta.get_label(frm.doc.doctype, "supplier", frm.doc.name)), - ]), - indicator: "orange", - }); - } - - return { - query: "frappe.contacts.doctype.address.address.address_query", - filters: { link_doctype: "Supplier", link_name: frm.doc.supplier }, - }; - }, - dispatch_address_query: function (doc) { var filters = { link_doctype: "Company", link_name: doc.company || "" }; var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier); From 62261a276f2ff0fcacf5aae3e4878c543b70c5f6 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 21 Apr 2025 13:44:20 +0530 Subject: [PATCH 18/41] refactor: address field position (cherry picked from commit 8ccd7a3e61e72a711d9cbdaa5e89dddaa6a42e05) # Conflicts: # erpnext/public/scss/erpnext.scss --- .../doctype/purchase_invoice/purchase_invoice.json | 8 ++++---- .../doctype/purchase_order/purchase_order.json | 6 +++--- erpnext/public/scss/erpnext.scss | 13 +++++++++++++ .../doctype/purchase_receipt/purchase_receipt.json | 8 ++++---- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index b7e110e6a69..b4b4e403646 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -143,11 +143,11 @@ "contact_mobile", "contact_email", "company_shipping_address_section", - "shipping_address", - "shipping_address_display", - "column_break_126", "dispatch_address", "dispatch_address_display", + "column_break_126", + "shipping_address", + "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_130", @@ -1548,7 +1548,7 @@ { "fieldname": "company_shipping_address_section", "fieldtype": "Section Break", - "label": "Company Shipping Address" + "label": "Shipping Address" }, { "fieldname": "column_break_126", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 3eda1789140..fad9f976c8a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -109,11 +109,11 @@ "contact_mobile", "contact_email", "shipping_address_section", - "shipping_address", - "shipping_address_display", - "column_break_99", "dispatch_address", "dispatch_address_display", + "column_break_99", + "shipping_address", + "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_103", diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 29a2696470f..eab615db7e5 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -590,6 +590,19 @@ body[data-route="pos"] { justify-content: center; } +<<<<<<< HEAD .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { +======= +.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor, +.frappe-control[data-fieldname="address_display"] .ql-editor, +.frappe-control[data-fieldname="shipping_address_display"] .ql-editor, +.frappe-control[data-fieldname="shipping_address"] .ql-editor, +.frappe-control[data-fieldname="dispatch_address_display"] .ql-editor, +.frappe-control[data-fieldname="dispatch_address"] .ql-editor, +.frappe-control[data-fieldname="source_address_display"] .ql-editor, +.frappe-control[data-fieldname="target_address_display"] .ql-editor, +.frappe-control[data-fieldname="billing_address_display"] .ql-editor, +.frappe-control[data-fieldname="company_address_display"] .ql-editor { +>>>>>>> 8ccd7a3e61 (refactor: address field position) white-space: normal; } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 8fce3be270e..c3933780203 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -112,11 +112,11 @@ "contact_mobile", "contact_email", "section_break_98", - "shipping_address", - "shipping_address_display", - "column_break_100", "dispatch_address", "dispatch_address_display", + "column_break_100", + "shipping_address", + "shipping_address_display", "billing_address_section", "billing_address", "column_break_104", @@ -1200,7 +1200,7 @@ { "fieldname": "section_break_98", "fieldtype": "Section Break", - "label": "Company Shipping Address" + "label": "Shipping Address" }, { "fieldname": "billing_address_section", From 7baa8f50fb70fa75d655c1b2cf610dcb90288b3b Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 21 Apr 2025 13:46:59 +0530 Subject: [PATCH 19/41] refactor: set address details for transactions (cherry picked from commit fb3b7d8c34a08af742bc83a71847a13d5f61dbcf) --- erpnext/accounts/party.py | 54 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index ab00097e426..d6df45d2dfc 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -199,47 +199,47 @@ def set_address_details( *, ignore_permissions=False, ): - billing_address_field = ( + # party_billing + party_billing_field = ( "customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address" ) - party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) + party_details[party_billing_field] = party_address or get_default_address(party_type, party.name) if doctype: party_details.update( - get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) + get_fetch_values(doctype, party_billing_field, party_details[party_billing_field]) ) - # address display party_details.address_display = render_address( - party_details[billing_address_field], check_permissions=not ignore_permissions + party_details[party_billing_field], check_permissions=not ignore_permissions ) - # Initialize shipping address fields based on party type + # party_shipping if party_type in ["Customer", "Lead"]: - party_shipping_address_field = "shipping_address_name" - party_shipping_address_display_field = "shipping_address" - is_party_type_supplier = False + party_shipping_field = "shipping_address_name" + party_shipping_display = "shipping_address" + default_shipping = shipping_address - else: # Default to Supplier - party_shipping_address_field = "dispatch_address" - party_shipping_address_display_field = "dispatch_address_display" - is_party_type_supplier = True + else: + # Supplier + party_shipping_field = "dispatch_address" + party_shipping_display = "dispatch_address_display" + default_shipping = dispatch_address - party_details[party_shipping_address_field] = ( - dispatch_address if is_party_type_supplier else shipping_address - ) or get_party_shipping_address(party_type, party.name) + party_details[party_shipping_field] = default_shipping or get_party_shipping_address( + party_type, party.name + ) - party_details[party_shipping_address_display_field] = render_address( - party_details[party_shipping_address_field], check_permissions=not ignore_permissions + party_details[party_shipping_display] = render_address( + party_details[party_shipping_field], check_permissions=not ignore_permissions ) if doctype: party_details.update( - get_fetch_values( - doctype, party_shipping_address_field, party_details[party_shipping_address_field] - ) + get_fetch_values(doctype, party_shipping_field, party_details[party_shipping_field]) ) + # company_address if company_address: party_details.company_address = company_address else: @@ -277,22 +277,20 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) - party_address, shipping_address = ( - party_details.get(billing_address_field), - party_details.get(party_shipping_address_field), + party_billing, party_shipping = ( + party_details.get(party_billing_field), + party_details.get(party_shipping_field), ) party_details["tax_category"] = get_address_tax_category( - party.get("tax_category"), - party_address, - shipping_address if party_type != "Supplier" else party_address, + party.get("tax_category"), party_billing, party_shipping ) if doctype in TRANSACTION_TYPES: with temporary_flag("company", company): get_regional_address_details(party_details, doctype, company) - return party_address, shipping_address + return party_billing, party_shipping @erpnext.allow_regional From d8c0e7156e4664d9f7a871e8c1eebda227626ea3 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 21 Apr 2025 13:49:14 +0530 Subject: [PATCH 20/41] fix: map dispatch address correctly for inter company transactions (cherry picked from commit ceaba4220bb61eef7f35d54072c8c79049288806) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 ++++- erpnext/stock/doctype/delivery_note/delivery_note.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7761dc832de..ed3003b2511 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2298,7 +2298,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): # Invert Addresses update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address( - target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name + ) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name ) update_address( target_doc, "billing_address", "billing_address_display", source_doc.customer_address diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index ba04abce8f3..689027d55bc 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1163,7 +1163,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): # Invert the address on target doc creation update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address( - target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name + ) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name ) update_address( target_doc, "billing_address", "billing_address_display", source_doc.customer_address From feb4038c0655b2e441251311691a1972fd4853e8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Apr 2025 11:36:24 +0530 Subject: [PATCH 21/41] chore: resolve conflicts --- .../purchase_invoice/purchase_invoice.json | 9 --------- .../purchase_order/purchase_order.json | 19 ------------------- erpnext/public/scss/erpnext.scss | 13 ------------- 3 files changed, 41 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index b4b4e403646..dfdcea5357a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1629,14 +1629,6 @@ "fieldname": "update_outstanding_for_self", "fieldtype": "Check", "label": "Update Outstanding for Self" -<<<<<<< HEAD -======= - }, - { - "fieldname": "sender", - "fieldtype": "Data", - "label": "Sender", - "options": "Email" }, { "fieldname": "dispatch_address_display", @@ -1651,7 +1643,6 @@ "label": "Select Dispatch Address ", "options": "Address", "print_hide": 1 ->>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) } ], "grid_page_length": 50, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index fad9f976c8a..7c9362ebaf9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1271,20 +1271,6 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 -<<<<<<< HEAD -======= - }, - { - "fieldname": "advance_payment_status", - "fieldtype": "Select", - "hidden": 1, - "in_standard_filter": 1, - "label": "Advance Payment Status", - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid", - "print_hide": 1 }, { "fieldname": "dispatch_address", @@ -1299,7 +1285,6 @@ "label": "Dispatch Address Details", "print_hide": 1, "read_only": 1 ->>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) } ], "grid_page_length": 50, @@ -1307,11 +1292,7 @@ "idx": 105, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-03-20 16:03:31.611808", -======= "modified": "2025-04-09 16:54:08.836106", ->>>>>>> 54b5205221 (feat: add dispatch address fields to purchase doctypes) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index eab615db7e5..29a2696470f 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -590,19 +590,6 @@ body[data-route="pos"] { justify-content: center; } -<<<<<<< HEAD .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { -======= -.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor, -.frappe-control[data-fieldname="address_display"] .ql-editor, -.frappe-control[data-fieldname="shipping_address_display"] .ql-editor, -.frappe-control[data-fieldname="shipping_address"] .ql-editor, -.frappe-control[data-fieldname="dispatch_address_display"] .ql-editor, -.frappe-control[data-fieldname="dispatch_address"] .ql-editor, -.frappe-control[data-fieldname="source_address_display"] .ql-editor, -.frappe-control[data-fieldname="target_address_display"] .ql-editor, -.frappe-control[data-fieldname="billing_address_display"] .ql-editor, -.frappe-control[data-fieldname="company_address_display"] .ql-editor { ->>>>>>> 8ccd7a3e61 (refactor: address field position) white-space: normal; } From 2e6112f21b359abace6a5bbc1d43001d967fd919 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:18:51 +0530 Subject: [PATCH 22/41] =?UTF-8?q?fix:=20update=20quantity=20validation=20u?= =?UTF-8?q?sing=20asset=20quantity=20field=20instead=20of=E2=80=A6=20(back?= =?UTF-8?q?port=20#46731)=20(#47284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: update quantity validation using asset quantity field instead of… (#46731) * fix: update quantity validation using asset quantity field instead of total records * fix: update throw message (cherry picked from commit eae08bc619a9a23698ec79dd0d4f84471bf4edfa) # Conflicts: # erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py * chore: resolved conflicts --------- Co-authored-by: l0gesh29 Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- .../landed_cost_voucher.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index eb84fdbc7c0..c80bcc8123b 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt +from frappe.utils import cint, flt import erpnext from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals @@ -268,14 +268,24 @@ class LandedCostVoucher(Document): ) docs = frappe.db.get_all( "Asset", - filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, - fields=["name", "docstatus"], + filters={ + receipt_document_type: item.receipt_document, + "item_code": item.item_code, + "docstatus": ["!=", 2], + }, + fields=["name", "docstatus", "asset_quantity"], ) - if not docs or len(docs) < item.qty: + + total_asset_qty = sum((cint(d.asset_quantity)) for d in docs) + + if not docs or total_asset_qty < item.qty: frappe.throw( _( - "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document." - ).format(len(docs), item.receipt_document, item.qty) + "For item {0}, only {1} asset have been created or linked to {2}. " + "Please create or link {3} more asset with the respective document." + ).format( + item.item_code, total_asset_qty, item.receipt_document, item.qty - total_asset_qty + ) ) if docs: for d in docs: From e0cea492361e9abd57a649ffe22188914ae66aa5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Apr 2025 13:27:57 +0530 Subject: [PATCH 23/41] fix: allow to make quality inspection after Purchase / Delivery (cherry picked from commit fad1a32e632056d5c62b864d1276b7fce161e228) --- erpnext/controllers/stock_controller.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fbc98a1a2f4..eb33187033d 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1047,6 +1047,16 @@ class StockController(AccountsController): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if self.doctype in [ + "Purchase Receipt", + "Purchase Invoice", + "Sales Invoice", + "Delivery Note", + ] and frappe.db.get_single_value( + "Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery" + ): + return + if not row.quality_inspection: msg = _("Row #{0}: Quality Inspection is required for Item {1}").format( row.idx, frappe.bold(row.item_code) From 0763a8d42d7924b7c34efa4728513cec30024ae1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Apr 2025 15:43:13 +0530 Subject: [PATCH 24/41] fix: set billing hours to hours --- erpnext/projects/doctype/timesheet/timesheet.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 09fdfad66ba..ae055b3f5d2 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -108,15 +108,10 @@ class Timesheet(Document): self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours def update_billing_hours(self, args): - if args.is_billable: - if flt(args.billing_hours) == 0.0: - args.billing_hours = args.hours - elif flt(args.billing_hours) > flt(args.hours): - frappe.msgprint( - _("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx), - indicator="orange", - alert=True, - ) + if args.is_billable and ( + flt(args.billing_hours) == 0.0 or flt(args.billing_hours) != flt(args.hours) + ): + args.billing_hours = args.hours else: args.billing_hours = 0 From a9df1f5f6bac60c04067255c83e0bdfff2f5a262 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Apr 2025 16:07:54 +0530 Subject: [PATCH 25/41] fix: update billing hours when hours is changed --- erpnext/projects/doctype/timesheet/timesheet.js | 1 + erpnext/projects/doctype/timesheet/timesheet.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 168b891e98c..4c78d939ebc 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -296,6 +296,7 @@ frappe.ui.form.on("Timesheet Detail", { hours: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); + update_billing_hours(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index ae055b3f5d2..aa344c74fe9 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -108,12 +108,15 @@ class Timesheet(Document): self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours def update_billing_hours(self, args): - if args.is_billable and ( - flt(args.billing_hours) == 0.0 or flt(args.billing_hours) != flt(args.hours) - ): - args.billing_hours = args.hours - else: - args.billing_hours = 0 + if args.is_billable: + if flt(args.billing_hours) == 0.0: + args.billing_hours = args.hours + elif flt(args.billing_hours) > flt(args.hours): + frappe.msgprint( + _("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx), + indicator="orange", + alert=True, + ) def set_status(self): self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] From 8a30a313023458771b094c824ac704bf378a80f9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Apr 2025 16:08:40 +0530 Subject: [PATCH 26/41] fix: missing else statement --- erpnext/projects/doctype/timesheet/timesheet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index aa344c74fe9..09fdfad66ba 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -117,6 +117,8 @@ class Timesheet(Document): indicator="orange", alert=True, ) + else: + args.billing_hours = 0 def set_status(self): self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] From 91bcefef8ceb43a993989d613e6f6cf7cbba63c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:38:27 +0530 Subject: [PATCH 28/41] fix: validation if no stock ledger entries against stock reco (backport #47292) (#47293) fix: validation if no stock ledger entries against stock reco (#47292) (cherry picked from commit 3d36d0b1df46008d9cec73393968a77d06aafced) Co-authored-by: rohitwaghchaure --- .../doctype/stock_reconciliation/stock_reconciliation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index c3d0480e820..0c28afe07e4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -726,6 +726,12 @@ class StockReconciliation(StockController): ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + elif self.docstatus == 1: + frappe.throw( + _( + "No stock ledger entries were created. Please set the quantity or valuation rate for the items properly and try again." + ) + ) def make_adjustment_entry(self, row, sl_entries): from erpnext.stock.stock_ledger import get_stock_value_difference From b0399fe9488101b4787a4508e96b6269ff28933e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 22:01:57 +0530 Subject: [PATCH 29/41] =?UTF-8?q?fix:=20QI=20reference=20not=20set=20if=20?= =?UTF-8?q?'Action=20If=20Quality=20Inspection=20Is=20Not=20Sub=E2=80=A6?= =?UTF-8?q?=20(backport=20#47294)=20(#47295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: QI reference not set if 'Action If Quality Inspection Is Not Sub… (#47294) fix: qi reference not set if 'Action If Quality Inspection Is Not Submitted' is blank (cherry picked from commit 0701a8cf5a1a410f1ce587e06258bbe8d08fa9c8) Co-authored-by: rohitwaghchaure --- .../doctype/quality_inspection/quality_inspection.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 8aed2277de3..021b7b1cf17 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -203,10 +203,11 @@ class QualityInspection(Document): self.get_item_specification_details() def on_update(self): - if ( - frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") - == "Warn" - ): + action_if_qi_in_draft = frappe.db.get_single_value( + "Stock Settings", "action_if_quality_inspection_is_not_submitted" + ) + + if not action_if_qi_in_draft or action_if_qi_in_draft == "Warn": self.update_qc_reference() def on_submit(self): From 6b1b30a4a613341ac3724230ac1712d826f347ed Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 24 Apr 2025 16:10:12 +0530 Subject: [PATCH 30/41] fix: price currency in supplier quotation comparison (cherry picked from commit 88926eb2a7bb8e6814064236a133b3b3bbc9301b) --- .../supplier_quotation_comparison.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) 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 085f30f84d9..ad181802c79 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -83,19 +83,11 @@ def prepare_data(supplier_quotation_data, filters): supplier_qty_price_map = {} group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" - company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: group = data.get(group_by_field) # get item or supplier value for this row - supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency") - - if supplier_currency: - exchange_rate = get_exchange_rate(supplier_currency, company_currency) - else: - exchange_rate = 1 - row = { "item_code": "" if group_by_field == "item_code" @@ -103,7 +95,7 @@ def prepare_data(supplier_quotation_data, filters): "supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"), "quotation": data.get("parent"), "qty": data.get("qty"), - "price": flt(data.get("amount") * exchange_rate, float_precision), + "price": flt(data.get("amount"), float_precision), "uom": data.get("uom"), "price_list_currency": data.get("price_list_currency"), "currency": data.get("currency"), @@ -209,6 +201,13 @@ def get_columns(filters): columns = [ {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90}, {"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80}, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, { "fieldname": "currency", "label": _("Currency"), @@ -223,13 +222,6 @@ def get_columns(filters): "options": "currency", "width": 110, }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, { "fieldname": "price_per_unit", "label": _("Price per Unit (Stock UOM)"), From 954fec16f4a4fff59a064c7ccf2f5d6dfcb9e475 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 28 Apr 2025 23:51:51 +0530 Subject: [PATCH 31/41] fix: commas in rfq portal js (cherry picked from commit bd727e069be72e3cb819670ced225998e0b6c704) --- erpnext/templates/includes/rfq.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index 37beb5a584b..cc998a90030 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -31,8 +31,8 @@ rfq = class rfq { var me = this; $('.rfq-items').on("change", ".rfq-qty", function(){ me.idx = parseFloat($(this).attr('data-idx')); - me.qty = parseFloat($(this).val()) || 0; - me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.qty = parseFloat(flt($(this).val())) || 0; + me.rate = parseFloat(flt($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val())); me.update_qty_rate(); $(this).val(format_number(me.qty, doc.number_format, 2)); }) @@ -42,8 +42,8 @@ rfq = class rfq { var me = this; $(".rfq-items").on("change", ".rfq-rate", function(){ me.idx = parseFloat($(this).attr('data-idx')); - me.rate = parseFloat($(this).val()) || 0; - me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.rate = parseFloat(flt($(this).val())) || 0; + me.qty = parseFloat(flt($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val())); me.update_qty_rate(); $(this).val(format_number(me.rate, doc.number_format, 2)); }) From 9495a2ac9df2b531e7dc70c3b35b438abb2ae72b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 29 Apr 2025 11:45:46 +0530 Subject: [PATCH 32/41] feat: change sabb qty automatically incase of internal transfer PR if sabb only has 1 batch (#47256) * feat: change sabb qty automatically incase of internal transfer PR if sabb only has 1 batch * fix: prevent creation of SABB on every save * perf: optimize code * fix: remove unnecessary conditon * refactor: change if to elif * fix: remove dn_item_qty and set to item.qty * test: added test (cherry picked from commit 47927b38a9aa185102a12688af4b1d784827f560) --- erpnext/controllers/buying_controller.py | 22 ++++++++ erpnext/controllers/stock_controller.py | 10 +++- .../purchase_receipt/test_purchase_receipt.py | 55 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 267b0ed5dd5..3fdf92e7990 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -98,7 +98,29 @@ class BuyingController(SubcontractingController): item.from_warehouse, type_of_transaction="Outward", do_not_submit=True, + qty=item.qty, ) + elif ( + not self.is_new() + and item.serial_and_batch_bundle + and next( + ( + old_item + for old_item in self.get_doc_before_save().items + if old_item.name == item.name and old_item.qty != item.qty + ), + None, + ) + and len( + sabe := frappe.get_all( + "Serial and Batch Entry", + filters={"parent": item.serial_and_batch_bundle, "serial_no": ["is", "not set"]}, + pluck="name", + ) + ) + == 1 + ): + frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty) def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index eb33187033d..80f860b4553 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -811,7 +811,7 @@ class StockController(AccountsController): ) def make_package_for_transfer( - self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None + self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0 ): return make_bundle_for_material_transfer( is_new=self.is_new(), @@ -822,6 +822,7 @@ class StockController(AccountsController): warehouse=warehouse, type_of_transaction=type_of_transaction, do_not_submit=do_not_submit, + qty=qty, ) def get_sl_entries(self, d, args): @@ -1815,15 +1816,20 @@ def make_bundle_for_material_transfer(**kwargs): kwargs.type_of_transaction = "Inward" bundle_doc = frappe.copy_doc(bundle_doc) + bundle_doc.docstatus = 0 bundle_doc.warehouse = kwargs.warehouse bundle_doc.type_of_transaction = kwargs.type_of_transaction bundle_doc.voucher_type = kwargs.voucher_type bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no bundle_doc.is_cancelled = 0 + qty = 0 + if len(bundle_doc.entries) == 1 and kwargs.qty < bundle_doc.total_qty and not bundle_doc.has_serial_no: + qty = kwargs.qty + for row in bundle_doc.entries: row.is_outward = 0 - row.qty = abs(row.qty) + row.qty = abs(qty or row.qty) row.stock_value_difference = abs(row.stock_value_difference) if kwargs.type_of_transaction == "Outward": row.qty *= -1 diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8e8532a904b..984f313b848 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,8 @@ from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowti from pypika import functions as fn import erpnext +import erpnext.controllers +import erpnext.controllers.status_updater from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.buying_controller import QtyMismatchError @@ -4129,6 +4131,59 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertTrue(sles) + def test_internal_pr_qty_change_only_single_batch(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + def get_sabb_qty(sabb): + return frappe.get_value("Serial and Batch Bundle", sabb, "total_qty") + + item = make_item("Item with only Batch", {"has_batch_no": 1}) + item.create_new_batch = 1 + item.save() + + make_purchase_receipt( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + ) + + dn = create_delivery_note( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + customer="_Test Internal Customer 2", + cost_center="Main - TCP1", + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + ) + pr = make_inter_company_purchase_receipt(dn.name) + + pr.items[0].warehouse = "Stores - TCP1" + pr.items[0].qty = 8 + pr.save() + + # Test 1 - Check if SABB qty is changed on first save + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 8) + + pr.items[0].qty = 6 + pr.items[0].received_qty = 6 + pr.save() + + # Test 2 - Check if SABB qty is changed when saved again + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 6) + + pr.items[0].qty = 12 + pr.items[0].received_qty = 12 + + # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN + self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From c2235e2d17d207dc448a3c72450263725b611ada Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 24 Apr 2025 11:33:18 +0530 Subject: [PATCH 33/41] fix(payment request): get advance amount based on transaction currency (cherry picked from commit b570d97b4d5ca4ea753cfe11d321fe9ffd1bc52c) --- .../accounts/doctype/payment_request/payment_request.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0027f0aa8be..344248b57b6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -670,7 +670,12 @@ def get_amount(ref_doc, payment_account=None): dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid + advance_amount = flt(ref_doc.advance_paid) + if ref_doc.party_account_currency != ref_doc.currency: + advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate) + + grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount + elif dt in ["Sales Invoice", "Purchase Invoice"]: if ( dt == "Sales Invoice" From 0927155171f7075e096443faace4f7f86756753f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 24 Apr 2025 18:54:03 +0530 Subject: [PATCH 34/41] fix: compare total debit/credit with precision for Inter Company Journal Entry (cherry picked from commit 5fe247557ef65034e69879db57a6f3d875a2aa4a) --- .../accounts/doctype/journal_entry/journal_entry.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e8ac493d659..a29eae8d5aa 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -250,11 +250,20 @@ class JournalEntry(AccountsController): def validate_inter_company_accounts(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: - doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference) + doc = frappe.db.get_value( + "Journal Entry", + self.inter_company_journal_entry_reference, + ["company", "total_debit", "total_credit"], + as_dict=True, + ) account_currency = frappe.get_cached_value("Company", self.company, "default_currency") previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency") if account_currency == previous_account_currency: - if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: + credit_precision = self.precision("total_credit") + debit_precision = self.precision("total_debit") + if (flt(self.total_credit, credit_precision) != flt(doc.total_debit, debit_precision)) or ( + flt(self.total_debit, debit_precision) != flt(doc.total_credit, credit_precision) + ): frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) def validate_depr_entry_voucher_type(self): From afb67f1f0cba831df327f8644cc8f141fd83439e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:40:10 +0530 Subject: [PATCH 35/41] fix: add transaction_date in field_no_map when creating PO from SQ (backport #47257) (#47313) * fix: add transaction_date in field_no_map when creating PO from SQ (cherry picked from commit 3790c6c551dd03e9b9235a8732c526dd25a3e14a) * fix: test case (cherry picked from commit acd152978092d7fcb9a6b82cf47be7d46a4470d8) # Conflicts: # erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py * fix: remove unused import (cherry picked from commit 9e640341fd8410b6e2fe4bfddad8188ceb0134ab) # Conflicts: # erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py * chore: fix conflicts * chore: remove unused imports --------- Co-authored-by: Mihir Kandoi --- .../buying/doctype/supplier_quotation/supplier_quotation.py | 1 + .../doctype/supplier_quotation/test_supplier_quotation.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 69de7068b68..215022e18a6 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -234,6 +234,7 @@ def make_purchase_order(source_name, target_doc=None): { "Supplier Quotation": { "doctype": "Purchase Order", + "field_no_map": ["transaction_date"], "validation": { "docstatus": ["=", 1], }, diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 13c851c7353..84df61de373 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -4,6 +4,7 @@ import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today class TestPurchaseOrder(FrappeTestCase): @@ -25,7 +26,7 @@ class TestPurchaseOrder(FrappeTestCase): for doc in po.get("items"): if doc.get("item_code"): - doc.set("schedule_date", "2013-04-12") + doc.set("schedule_date", add_days(today(), 1)) po.insert() From fc8a8b5433a5448ce1a03e211e935d63a3e03cbd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:40:45 +0530 Subject: [PATCH 36/41] fix: remove invalid email account creation (backport #47318) (#47323) fix: remove invalid email account creation (#47318) (cherry picked from commit 7423e4187fd8bb43c1adda9ce4468c51d16df5b5) Co-authored-by: Diptanil Saha --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 270a9e06054..9f68145e71d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -266,8 +266,6 @@ def install(country=None): {"doctype": "Issue Priority", "name": _("Low")}, {"doctype": "Issue Priority", "name": _("Medium")}, {"doctype": "Issue Priority", "name": _("High")}, - {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, From 2edd12b26d8d226e32d6ac9c3ae14ff61d90e3f5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:51:18 +0530 Subject: [PATCH 37/41] fix: prevent cancellation of last asset movement (backport #47291) (#47312) fix: prevent cancellation of last asset movement (#47291) * fix: prevent cancellation of last asset movement * test: movement cancellation * fix: allow cancellation of asset movement when cancelling asset (cherry picked from commit 9dee4ac891cac2b9121c3d459554b65c52cb4198) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- .../doctype/asset_movement/asset_movement.py | 12 ++++++ .../asset_movement/test_asset_movement.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index ad9b4380fa9..c68f9044ae1 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -152,6 +152,9 @@ class AssetMovement(Document): """, args, ) + + self.validate_movement_cancellation(d, latest_movement_entry) + if latest_movement_entry: current_location = latest_movement_entry[0][0] current_employee = latest_movement_entry[0][1] @@ -179,3 +182,12 @@ class AssetMovement(Document): d.asset, _("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), ) + + def validate_movement_cancellation(self, row, latest_movement_entry): + asset_doc = frappe.get_doc("Asset", row.asset) + if not latest_movement_entry and asset_doc.docstatus == 1: + frappe.throw( + _( + "Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking." + ).format(row.asset) + ) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 52590d2ba86..2d0d68cb25f 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -147,6 +147,45 @@ class TestAssetMovement(unittest.TestCase): movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def test_last_movement_cancellation_validation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = "2020-06-06" + asset.purchase_date = "2020-06-06" + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + }, + ) + if asset.docstatus == 0: + asset.submit() + + AssetMovement = frappe.qb.DocType("Asset Movement") + AssetMovementItem = frappe.qb.DocType("Asset Movement Item") + + asset_movement = ( + frappe.qb.from_(AssetMovement) + .join(AssetMovementItem) + .on(AssetMovementItem.parent == AssetMovement.name) + .select(AssetMovement.name) + .where( + (AssetMovementItem.asset == asset.name) + & (AssetMovement.company == asset.company) + & (AssetMovement.docstatus == 1) + ) + ).run(as_dict=True) + + asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name) + self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel) + def create_asset_movement(**args): args = frappe._dict(args) From d640c79c1c4fafd00fe47bbd85b937ebe575c840 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 29 Apr 2025 16:21:41 +0530 Subject: [PATCH 38/41] fix: validate if from and to time are present on submission of job card (#47325) (cherry picked from commit 7499c25a3c7045e14ca0c4bd3985e7f408e3eead) --- erpnext/manufacturing/doctype/job_card/job_card.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 659bb01b4d4..abea4bca606 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -705,6 +705,12 @@ class JobCard(Document): bold("Job Card"), get_link_to_form("Job Card", self.name) ) ) + else: + for row in self.time_logs: + if not row.from_time or not row.to_time: + frappe.throw( + _("Row #{0}: From Time and To Time fields are required").format(row.idx), + ) precision = self.precision("total_completed_qty") total_completed_qty = flt( From 0056fb1d0fbbf17ab3d475181e1c63aa853e0418 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:27:56 +0530 Subject: [PATCH 39/41] fix: require email OR phone in shipment doctype not both (backport #47300) (#47330) fix: require email OR phone in shipment doctype not both (#47300) (cherry picked from commit fc02a6510ee74191383107076c0b7fff3a1d0f91) Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/shipment/shipment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 8843d383531..7f1e1c8d729 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -162,7 +162,7 @@ frappe.ui.form.on("Shipment", { args: { contact: contact_name }, callback: function (r) { if (r.message) { - if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { + if (!(r.message.contact_email || r.message.contact_phone || r.message.contact_mobile)) { if (contact_type == "Delivery") { frm.set_value("delivery_contact_name", ""); frm.set_value("delivery_contact", ""); From 171b6876112fd5ffd19d20999f1a92d5adf6fb61 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:12:51 +0530 Subject: [PATCH 40/41] fix: allow selling asset at zero rate (backport #47326) (#47332) fix: allow selling asset at zero rate (#47326) (cherry picked from commit 05afad78fcac6fab1b3faa783428c464db7fb00d) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ed3003b2511..afe41846b70 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1348,7 +1348,7 @@ class SalesInvoice(SellingController): ) for item in self.get("items"): - if flt(item.base_net_amount, item.precision("base_net_amount")): + if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset: # Do not book income for transfer within same company if self.is_internal_transfer(): continue From d15b7ca9af106989866e9b6fac800cc1ed6e71fd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:12:36 +0530 Subject: [PATCH 41/41] fix: fix sub assembly qty calculation in production plan when bom level >= 1 (backport #47296) (#47315) * fix: fix sub assembly qty calculation in production plan when bom level >= 1 (cherry picked from commit bfc4ce1d5dd67a7b2a3b1d7c931077446bbdd036) * fix: logical error (cherry picked from commit ee10afc0747a15f7da623ab6e1c5dceb0364424a) --------- Co-authored-by: Mihir Kandoi --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 681abc8ddde..dbcf07bbccf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1768,6 +1768,7 @@ def get_sub_assembly_items( continue else: stock_qty = stock_qty - _bin_dict.projected_qty + sub_assembly_items.append(d.item_code) elif warehouse: bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))