From ecdff8f320f3f1779d2e87e1e71f13e80cffad8f Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Wed, 22 Jan 2025 07:41:16 +0100 Subject: [PATCH 01/23] fix: heatmap for Customer and Supplier not rendering (#44717) --- erpnext/buying/doctype/supplier/supplier.py | 1 + erpnext/selling/doctype/customer/customer.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 2774f1aeffb..9c30715351f 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -13,6 +13,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_ from erpnext.accounts.party import ( get_dashboard_info, + get_timeline_data, validate_party_accounts, ) from erpnext.utilities.transaction_base import TransactionBase diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 182e10a4765..11345ea7d95 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -20,6 +20,7 @@ from frappe.utils.user import get_users_with_role from erpnext.accounts.party import ( get_dashboard_info, + get_timeline_data, validate_party_accounts, ) from erpnext.utilities.transaction_base import TransactionBase From 1622fc8728d0b854013db610cfdd0a09745b5a63 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:15:13 +0530 Subject: [PATCH 02/23] fix: set preferred email in Employee via backend controller (backport #45320) (#45378) fix: set preferred email in Employee via backend controller (#45320) fix: set preferred email in Employee (backend) Set "Preferred Email" for Employee via validate. Unset value when prefered_contact_email is also unset. (cherry picked from commit 4481ca83ff8d616cee416851f497af9fbaeb6b13) Co-authored-by: gavin --- erpnext/setup/doctype/employee/employee.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 9f8f3861cf3..3dc6f4b73e8 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -42,6 +42,7 @@ class Employee(NestedSet): self.validate_email() self.validate_status() self.validate_reports_to() + self.set_preferred_email() self.validate_preferred_email() if self.user_id: @@ -184,9 +185,7 @@ class Employee(NestedSet): def set_preferred_email(self): preferred_email_field = frappe.scrub(self.prefered_contact_email) - if preferred_email_field: - preferred_email = self.get(preferred_email_field) - self.prefered_email = preferred_email + self.prefered_email = self.get(preferred_email_field) if preferred_email_field else None def validate_status(self): if self.status == "Left": From b6b453ca5de524f313ea1bd2a45908259e68287b Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Jan 2025 13:21:30 +0530 Subject: [PATCH 03/23] fix: Wrong `bank_ac_no` filter + simplify convoluted logic (cherry picked from commit 85217968113f854c9702dc7487b48c375f4bbf3c) # Conflicts: # erpnext/accounts/doctype/bank_transaction/auto_match_party.py --- .../bank_transaction/auto_match_party.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 230407ba5a4..e9a912b57ab 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -45,25 +45,36 @@ class AutoMatchbyAccountIBAN: if not (self.bank_party_account_number or self.bank_party_iban): return None - result = self.match_account_in_party() - return result + return self.match_account_in_party() def match_account_in_party(self) -> tuple | None: - """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee""" - result = None - parties = get_parties_in_order(self.deposit) - or_filters = self.get_or_filters() + """ + Returns (Party Type, Party) if a matching account is found in Bank Account or Employee: + 1. Get party from a matching (iban/account no) Bank Account + 2. If not found, get party from Employee with matching bank account details (iban/account no) + """ + if not (self.bank_party_account_number or self.bank_party_iban): + # Nothing to match + return None - for party in parties: - party_result = frappe.db.get_all( - "Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1 - ) + # Search for a matching Bank Account that has party set + party_result = frappe.db.get_all( + "Bank Account", + or_filters=self.get_or_filters(), + filters={"party_type": ("is", "set"), "party": ("is", "set")}, + fields=["party", "party_type"], + limit_page_length=1, + ) + if result := party_result[0] if party_result else None: + return (result["party_type"], result["party"]) - if party == "Employee" and not party_result: - # Search in Bank Accounts first for Employee, and then Employee record - if "bank_account_no" in or_filters: - or_filters["bank_ac_no"] = or_filters.pop("bank_account_no") + # If no party is found, search in Employee (since it has bank account details) + if employee_result := frappe.db.get_all( + "Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1 + ): + return ("Employee", employee_result[0]) +<<<<<<< HEAD party_result = frappe.db.get_all( party, or_filters=or_filters, pluck="name", limit_page_length=1 ) @@ -78,9 +89,14 @@ class AutoMatchbyAccountIBAN: return result def get_or_filters(self) -> dict: +======= + def get_or_filters(self, party: str | None = None) -> dict: + """Return OR filters for Bank Account and IBAN""" +>>>>>>> 8521796811 (fix: Wrong `bank_ac_no` filter + simplify convoluted logic) or_filters = {} if self.bank_party_account_number: - or_filters["bank_account_no"] = self.bank_party_account_number + bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no" + or_filters[bank_ac_field] = self.bank_party_account_number if self.bank_party_iban: or_filters["iban"] = self.bank_party_iban @@ -100,8 +116,7 @@ class AutoMatchbyPartyNameDescription: if not (self.bank_party_name or self.description): return None - result = self.match_party_name_desc_in_party() - return result + return self.match_party_name_desc_in_party() def match_party_name_desc_in_party(self) -> tuple | None: """Fuzzy search party name and/or description against parties in the system""" @@ -110,7 +125,7 @@ class AutoMatchbyPartyNameDescription: for party in parties: filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} - field = party.lower() + "_name" + field = f"{party.lower()}_name" names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"]) for field in ["bank_party_name", "description"]: @@ -137,13 +152,7 @@ class AutoMatchbyPartyNameDescription: ) party_name, skip = self.process_fuzzy_result(result) - if not party_name: - return None, skip - - return ( - party, - party_name, - ), skip + return ((party, party_name), skip) if party_name else (None, skip) def process_fuzzy_result(self, result: list | None): """ @@ -161,8 +170,8 @@ class AutoMatchbyPartyNameDescription: if len(result) == 1: return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True - second_result = result[1] if first_result[SCORE] > CUTOFF: + second_result = result[1] # If multiple matches with the same score, return None but discontinue matching # Matches were found but were too close to distinguish between if first_result[SCORE] == second_result[SCORE]: @@ -174,8 +183,8 @@ class AutoMatchbyPartyNameDescription: def get_parties_in_order(deposit: float) -> list: - parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive - if flt(deposit) > 0: - parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay - - return parties + return ( + ["Customer", "Supplier", "Employee"] # most -> least likely to pay us + if flt(deposit) > 0 + else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us + ) From 69464ab7ff90c127d3322e3ee7c7dfdd84689e4b Mon Sep 17 00:00:00 2001 From: Shanuka Hewage <89955436+Shanuka-98@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:57:04 +0530 Subject: [PATCH 04/23] fix: add condition to check if item is delivered by supplier in make_purchase_order_for_default_supplier() (#45370) --- erpnext/selling/doctype/sales_order/sales_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 441e1788356..6d135812358 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1044,7 +1044,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier - and doc.item_code in items_to_map, + and doc.item_code in items_to_map + and doc.delivered_by_supplier == 1, }, }, target_doc, From 35f801feda2e082a5a8fb741e99c85073ced4b52 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Jan 2025 13:28:21 +0100 Subject: [PATCH 05/23] fix: Merge Conflicts --- .../bank_transaction/auto_match_party.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index e9a912b57ab..66aab9d62dd 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -74,25 +74,8 @@ class AutoMatchbyAccountIBAN: ): return ("Employee", employee_result[0]) -<<<<<<< HEAD - party_result = frappe.db.get_all( - party, or_filters=or_filters, pluck="name", limit_page_length=1 - ) - - if party_result: - result = ( - party, - party_result[0], - ) - break - - return result - - def get_or_filters(self) -> dict: -======= def get_or_filters(self, party: str | None = None) -> dict: """Return OR filters for Bank Account and IBAN""" ->>>>>>> 8521796811 (fix: Wrong `bank_ac_no` filter + simplify convoluted logic) or_filters = {} if self.bank_party_account_number: bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no" From 452b20502124f5f5a467941dd95a3b906477eae0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:19:32 +0530 Subject: [PATCH 06/23] fix: fix creating documents from sales invoice (backport #45346) (#45407) * fix: fix creating documents from sales invoice (#45346) Co-authored-by: Meike Nedwidek (cherry picked from commit 1758e125e00ea1427d0da5e4365d4d2191477130) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.js * fix: resolved conflict --------- Co-authored-by: meike289 <63092915+meike289@users.noreply.github.com> Co-authored-by: Nabin Hait --- .../doctype/sales_invoice/sales_invoice.js | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 73e51f2fcb4..e1add14eed7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -9,6 +9,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e setup(doc) { this.setup_posting_date_time_check(); super.setup(doc); + this.frm.make_methods = { + Dunning: this.make_dunning.bind(this), + "Invoice Discounting": this.make_invoice_discounting.bind(this), + }; } company() { super.company(); @@ -94,26 +98,35 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - if (doc.outstanding_amount>0) { - cur_frm.add_custom_button(__('Payment Request'), function() { - me.make_payment_request(); - }, __('Create')); + if (doc.outstanding_amount > 0) { + this.frm.add_custom_button( + __("Payment Request"), + function () { + me.make_payment_request(); + }, + __("Create") + ); + this.frm.add_custom_button( + __("Invoice Discounting"), + this.make_invoice_discounting.bind(this), + __("Create") + ); - cur_frm.add_custom_button(__('Invoice Discounting'), function() { - cur_frm.events.create_invoice_discounting(cur_frm); - }, __('Create')); + const payment_is_overdue = doc.payment_schedule + .map((row) => Date.parse(row.due_date) < Date.now()) + .reduce((prev, current) => prev || current, false); - if (doc.due_date < frappe.datetime.get_today()) { - cur_frm.add_custom_button(__('Dunning'), function() { - cur_frm.events.create_dunning(cur_frm); - }, __('Create')); + if (payment_is_overdue) { + this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create")); } } if (doc.docstatus === 1) { - cur_frm.add_custom_button(__('Maintenance Schedule'), function () { - cur_frm.cscript.make_maintenance_schedule(); - }, __('Create')); + this.frm.add_custom_button( + __("Maintenance Schedule"), + this.make_maintenance_schedule.bind(this), + __("Create") + ); } if(!doc.auto_repeat) { @@ -146,6 +159,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } + make_invoice_discounting() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting", + frm: this.frm, + }); + } + + make_dunning() { + frappe.model.open_mapped_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", + frm: this.frm, + }); + } + make_maintenance_schedule() { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule", @@ -948,20 +975,6 @@ frappe.ui.form.on('Sales Invoice', { frm.set_df_property('return_against', 'label', __('Adjustment Against')); } }, - - create_invoice_discounting: function(frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting", - frm: frm - }); - }, - - create_dunning: function(frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", - frm: frm - }); - } }); frappe.ui.form.on("Sales Invoice Timesheet", { From c6bc928f50e71eb43c708c92b6b10b06b548ca31 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:19:17 +0100 Subject: [PATCH 07/23] fix: secure bulk transaction (backport #45386) (#45425) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: secure bulk transaction (#45386) --- .../purchase_invoice/purchase_invoice_list.js | 16 ++++++++----- .../sales_invoice/sales_invoice_list.js | 16 ++++++++----- .../purchase_order/purchase_order_list.js | 24 ++++++++++++------- .../supplier_quotation_list.js | 20 +++++++++++----- .../doctype/quotation/quotation_list.js | 16 ++++++++----- .../doctype/sales_order/sales_order_list.js | 24 ++++++++++++------- .../delivery_note/delivery_note_list.js | 22 ++++++++++------- erpnext/utilities/bulk_transaction.py | 3 +++ 8 files changed, 90 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 4350ba11148..3f2d2265161 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = { }, onload: function (listview) { - listview.page.add_action_item(__("Purchase Receipt"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); - }); + if (frappe.model.can_create("Purchase Receipt")) { + listview.page.add_action_item(__("Purchase Receipt"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); + }); + } - listview.page.add_action_item(__("Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index f971f68a454..46d301b0ff7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = { right_column: "grand_total", onload: function (listview) { - listview.page.add_action_item(__("Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); - }); + if (frappe.model.can_create("Delivery Note")) { + listview.page.add_action_item(__("Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); + }); + } - listview.page.add_action_item(__("Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index c1bf1f3b8d9..955e19bc6b5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -51,16 +51,22 @@ frappe.listview_settings["Purchase Order"] = { listview.call_for_selected_items(method, { status: "Submitted" }); }); - listview.page.add_action_item(__("Purchase Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); - }); + if (frappe.model.can_create("Purchase Invoice")) { + listview.page.add_action_item(__("Purchase Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); + }); + } - listview.page.add_action_item(__("Purchase Receipt"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); - }); + if (frappe.model.can_create("Purchase Receipt")) { + listview.page.add_action_item(__("Purchase Receipt"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); + }); + } - listview.page.add_action_item(__("Advance Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Advance Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 99fe24d8770..1a2a514a680 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -11,12 +11,20 @@ frappe.listview_settings["Supplier Quotation"] = { }, onload: function (listview) { - listview.page.add_action_item(__("Purchase Order"), () => { - erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); - }); + if (frappe.model.can_create("Purchase Order")) { + listview.page.add_action_item(__("Purchase Order"), () => { + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); + }); + } - listview.page.add_action_item(__("Purchase Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice"); - }); + if (frappe.model.can_create("Purchase Invoice")) { + listview.page.add_action_item(__("Purchase Invoice"), () => { + erpnext.bulk_transaction_processing.create( + listview, + "Supplier Quotation", + "Purchase Invoice" + ); + }); + } }, }; diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index ae744b9cba3..b795c3fe0bc 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -12,13 +12,17 @@ frappe.listview_settings["Quotation"] = { }; } - listview.page.add_action_item(__("Sales Order"), () => { - erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); - }); + if (frappe.model.can_create("Sales Order")) { + listview.page.add_action_item(__("Sales Order"), () => { + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); + }); + } - listview.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); - }); + if (frappe.model.can_create("Sales Invoice")) { + listview.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); + }); + } }, get_indicator: function (doc) { diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 61a29c9bcd7..96164eebcdc 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -60,16 +60,22 @@ frappe.listview_settings["Sales Order"] = { listview.call_for_selected_items(method, { status: "Submitted" }); }); - listview.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); - }); + if (frappe.model.can_create("Sales Invoice")) { + listview.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); + }); + } - listview.page.add_action_item(__("Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); - }); + if (frappe.model.can_create("Delivery Note")) { + listview.page.add_action_item(__("Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); + }); + } - listview.page.add_action_item(__("Advance Payment"), () => { - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); - }); + if (frappe.model.can_create("Payment Entry")) { + listview.page.add_action_item(__("Advance Payment"), () => { + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); + }); + } }, }; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index c6b98c4134c..dd09f6cfcf5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -63,16 +63,20 @@ frappe.listview_settings["Delivery Note"] = { } }; - // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); + if (frappe.model.can_create("Delivery Trip")) { + doclist.page.add_action_item(__("Create Delivery Trip"), action); + } - doclist.page.add_action_item(__("Create Delivery Trip"), action); + if (frappe.model.can_create("Sales Invoice")) { + doclist.page.add_action_item(__("Sales Invoice"), () => { + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); + }); + } - doclist.page.add_action_item(__("Sales Invoice"), () => { - erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); - }); - - doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => { - erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); - }); + if (frappe.model.can_create("Packing Slip")) { + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), () => { + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); + }); + } }, }; diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 42cfc807aaa..3fd9c9c0f73 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -8,6 +8,9 @@ from frappe.utils import get_link_to_form, today @frappe.whitelist() def transaction_processing(data, from_doctype, to_doctype): + frappe.has_permission(from_doctype, "read", throw=True) + frappe.has_permission(to_doctype, "create", throw=True) + if isinstance(data, str): deserialized_data = json.loads(data) else: From 24dc1bf1a330aab5d60374f93037ab93d95f01c6 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Fri, 24 Jan 2025 22:51:22 +0530 Subject: [PATCH 08/23] fix: resolved pos return setting to default mode of payment instead of user selection (#45377) (#45436) * fix: resolved pos return setting to default mode of payment instead of user selection * refactor: removed console log statement * refactor: moved get_payment_data to sales_and_purchase_return.py --- .../controllers/sales_and_purchase_return.py | 71 +++++++++++++++---- .../public/js/controllers/taxes_and_totals.js | 41 ++++++++++- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index b2a4a4e0f7e..839bd5e154f 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -75,7 +75,11 @@ def validate_returned_items(doc): if doc.doctype != "Purchase Invoice": select_fields += ",serial_no, batch_no" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: + if doc.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Subcontracting Receipt", + ]: select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql( @@ -105,7 +109,12 @@ def validate_returned_items(doc): for d in doc.get("items"): key = d.item_code raise_exception = False - if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]: + if doc.doctype in [ + "Purchase Receipt", + "Purchase Invoice", + "Sales Invoice", + "POS Invoice", + ]: field = frappe.scrub(doc.doctype) + "_item" if d.get(field): key = (d.item_code, d.get(field)) @@ -175,7 +184,11 @@ def validate_returned_items(doc): def validate_quantity(doc, key, args, ref, valid_items, already_returned_items): fields = ["stock_qty"] - if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]: + if doc.doctype in [ + "Purchase Receipt", + "Purchase Invoice", + "Subcontracting Receipt", + ]: fields.extend(["received_qty", "rejected_qty"]) already_returned_data = already_returned_items.get(key) or {} @@ -203,7 +216,8 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items): frappe.throw(_("{0} must be negative in return document").format(label)) elif returned_qty >= reference_qty and args.get(column): frappe.throw( - _("Item {0} has already been returned").format(args.item_code), StockOverReturnError + _("Item {0} has already been returned").format(args.item_code), + StockOverReturnError, ) elif abs(flt(current_stock_qty, stock_qty_precision)) > max_returnable_qty: frappe.throw( @@ -242,7 +256,11 @@ def get_ref_item_dict(valid_items, ref_item_row): if ref_item_row.get("rate", 0) > item_dict["rate"]: item_dict["rate"] = ref_item_row.get("rate", 0) - if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: + if ref_item_row.parenttype in [ + "Purchase Invoice", + "Purchase Receipt", + "Subcontracting Receipt", + ]: item_dict["received_qty"] += ref_item_row.received_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty @@ -257,7 +275,11 @@ def get_ref_item_dict(valid_items, ref_item_row): def get_already_returned_items(doc): column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: + if doc.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Subcontracting Receipt", + ]: column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty, sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" @@ -384,7 +406,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): paid_amount = 0.00 base_paid_amount = 0.00 data.base_amount = flt( - data.amount * source.conversion_rate, source.precision("base_paid_amount") + data.amount * source.conversion_rate, + source.precision("base_paid_amount"), ) paid_amount += data.amount base_paid_amount += data.base_amount @@ -544,10 +567,17 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): }, doctype + " Item": { "doctype": doctype + " Item", - "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"}, + "field_map": { + "serial_no": "serial_no", + "batch_no": "batch_no", + "bom": "bom", + }, "postprocess": update_item, }, - "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, + "Payment Schedule": { + "doctype": "Payment Schedule", + "postprocess": update_terms, + }, }, target_doc, set_missing_values, @@ -580,13 +610,20 @@ def get_rate_for_return( item_row, ) - if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): + if voucher_type in ( + "Purchase Receipt", + "Purchase Invoice", + "Subcontracting Receipt", + ): select_field = "incoming_rate" else: select_field = "abs(stock_value_difference / actual_qty)" rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) - if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]: + if not (rate and return_against) and voucher_type in [ + "Sales Invoice", + "Delivery Note", + ]: rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate") if not rate and sle: @@ -629,7 +666,11 @@ def get_filters( return_against_item_field, item_row, ): - filters = {"voucher_type": voucher_type, "voucher_no": return_against, "item_code": item_code} + filters = { + "voucher_type": voucher_type, + "voucher_no": return_against, + "item_code": item_code, + } if item_row: reference_voucher_detail_no = item_row.get(return_against_item_field) @@ -669,3 +710,9 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): serial_nos.extend(get_serial_nos(row.get(serial_no_field))) return serial_nos + + +@frappe.whitelist() +def get_payment_data(invoice): + payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) + return payment diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8ca72d55c23..f715aa8836e 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -806,7 +806,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } - set_total_amount_to_default_mop() { + async set_total_amount_to_default_mop() { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; @@ -828,6 +828,45 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } + /* + During returns, if an user select mode of payment other than + default mode of payment, it should retain the user selection + instead resetting it to default mode of payment. + */ + + let payment_amount = 0; + this.frm.doc.payments.forEach(payment => { + payment_amount += payment.amount + }); + + if (payment_amount == total_amount_to_pay) { + return; + } + + /* + For partial return, if the payment was made using single mode of payment + it should set the return to that mode of payment only. + */ + + let return_against_mop = await frappe.call({ + method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', + args: { + invoice: this.frm.doc.return_against + } + }); + + if (return_against_mop.message.length === 1) { + this.frm.doc.payments.forEach(payment => { + if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0; + } + }); + this.frm.refresh_fields(); + return; + } + this.frm.doc.payments.find(payment => { if (payment.default) { payment.amount = total_amount_to_pay; From 4d2352af00ddfd3d57eaf217fd1b14f786c3aba7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:21:34 +0530 Subject: [PATCH 09/23] fix: currency decimal on POS Past Order List (backport #45524) (#45526) fix: currency decimal on POS Past Order List (#45524) * fix: currency decimal on POS * fix: removed precision (cherry picked from commit 2ac8c92e7fc30a50cf6b84bfea3cc334e6fa64bd) Co-authored-by: Diptanil Saha --- erpnext/selling/page/point_of_sale/pos_past_order_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index c450d8a109a..e05a3b0f9db 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -110,7 +110,7 @@ erpnext.PointOfSale.PastOrderList = class {
-
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
+
${format_currency(invoice.grand_total, invoice.currency) || 0}
${posting_datetime}
From cca5fbd81a77c6594ecb1b9cf6ef3a84b7580ee5 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:41:52 +0530 Subject: [PATCH 10/23] feat: add company level validation for accounting dimension (cherry picked from commit 60efd3e2195a1e87cade172786ce38917fe9ab8f) # Conflicts: # erpnext/controllers/accounts_controller.py --- .../accounting_dimension.json | 3 +- erpnext/controllers/accounts_controller.py | 63 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json index 5858f10bb0b..f05d20a0a49 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json @@ -31,7 +31,8 @@ "label": "Reference Document Type", "options": "DocType", "read_only_depends_on": "eval:!doc.__islocal", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "default": "0", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ecbf1177ee4..65dfc380fd7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -8,7 +8,7 @@ from collections import defaultdict import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied -from frappe.query_builder import Criterion +from frappe.query_builder import Criterion, DocType from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -250,6 +250,15 @@ class AccountsController(TransactionBase): apply_pricing_rule_on_transaction(self) self.set_total_in_words() +<<<<<<< HEAD +======= + self.set_default_letter_head() + self.validate_company_in_accounting_dimension() + + def set_default_letter_head(self): + if hasattr(self, "letter_head") and not self.letter_head: + self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head") +>>>>>>> 60efd3e219 (feat: add company level validation for accounting dimension) def init_internal_values(self): # init all the internal values as 0 on sa @@ -355,6 +364,58 @@ class AccountsController(TransactionBase): (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) ).run() +<<<<<<< HEAD +======= + def remove_serial_and_batch_bundle(self): + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)}, + ) + + for bundle in bundles: + frappe.delete_doc("Serial and Batch Bundle", bundle.name) + + batches = frappe.get_all( + "Batch", filters={"reference_doctype": self.doctype, "reference_name": self.name} + ) + for row in batches: + frappe.delete_doc("Batch", row.name) + + def validate_company_in_accounting_dimension(self): + doc_field = DocType("DocField") + accounting_dimension = DocType("Accounting Dimension") + query = ( + frappe.qb.from_(accounting_dimension) + .select(accounting_dimension.document_type) + .join(doc_field) + .on(doc_field.parent == accounting_dimension.document_type) + .where(doc_field.fieldname == "company") + ).run(as_list=True) + + dimension_list = sum(query, ["Project"]) + self.validate_company(dimension_list) + + if childs := self.get_all_children(): + for child in childs: + self.validate_company(dimension_list, child) + + def validate_company(self, dimension_list, child=None): + for dimension in dimension_list: + if not child: + dimension_value = self.get(frappe.scrub(dimension)) + else: + dimension_value = child.get(frappe.scrub(dimension)) + + if dimension_value: + company = frappe.get_cached_value(dimension, dimension_value, "company") + if company and company != self.company: + frappe.throw( + _("{0}: {1} does not belong to the Company: {2}").format( + dimension, frappe.bold(dimension_value), self.company + ) + ) + +>>>>>>> 60efd3e219 (feat: add company level validation for accounting dimension) def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" From 2fb1aaa5c33a1b395bd980138defe41b82a6d387 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:45:51 +0530 Subject: [PATCH 11/23] test: add new unit test for company validation in accounting dimension (cherry picked from commit c94091d68ffc1453b9042f6f2f675d81dcd76849) # Conflicts: # erpnext/controllers/tests/test_accounts_controller.py --- .../tests/test_accounts_controller.py | 260 +++++++++++++++++- 1 file changed, 245 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index a184009b1e1..4be13be81fe 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -14,6 +14,11 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account +<<<<<<< HEAD +======= +from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer +from erpnext.projects.doctype.project.test_project import make_project +>>>>>>> c94091d68f (test: add new unit test for company validation in accounting dimension) from erpnext.stock.doctype.item.test_item import create_item @@ -1344,32 +1349,32 @@ class TestAccountsController(FrappeTestCase): # Invoices si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si1.department = "Management" + si1.department = "Management - _TC" si1.save().submit() si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si2.department = "Operations" + si2.department = "Operations - _TC" si2.save().submit() # Payments cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note1.department = "Management" + cr_note1.department = "Management - _TC" cr_note1.is_return = 1 cr_note1.save().submit() cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note2.department = "Legal" + cr_note2.department = "Legal - _TC" cr_note2.is_return = 1 cr_note2.save().submit() pe1 = get_payment_entry(si1.doctype, si1.name) pe1.references = [] - pe1.department = "Research & Development" + pe1.department = "Research & Development - _TC" pe1.save().submit() pe2 = get_payment_entry(si1.doctype, si1.name) pe2.references = [] - pe2.department = "Management" + pe2.department = "Management - _TC" pe2.save().submit() je1 = self.create_journal_entry( @@ -1382,7 +1387,7 @@ class TestAccountsController(FrappeTestCase): ) je1.accounts[0].party_type = "Customer" je1.accounts[0].party = self.customer - je1.accounts[0].department = "Management" + je1.accounts[0].department = "Management - _TC" je1.save().submit() # assert dimension filter's result @@ -1391,17 +1396,17 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(len(pr.invoices), 2) self.assertEqual(len(pr.payments), 5) - pr.department = "Legal" + pr.department = "Legal - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) - pr.department = "Management" + pr.department = "Management - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 3) - pr.department = "Research & Development" + pr.department = "Research & Development - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) @@ -1413,17 +1418,17 @@ class TestAccountsController(FrappeTestCase): # Invoice si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) - si.department = "Management" + si.department = "Management - _TC" si.save().submit() # Payment cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) - cr_note.department = "Management" + cr_note.department = "Management - _TC" cr_note.is_return = 1 cr_note.save().submit() pr = self.create_payment_reconciliation() - pr.department = "Management" + pr.department = "Management - _TC" pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) @@ -1456,7 +1461,7 @@ class TestAccountsController(FrappeTestCase): # Sales Invoice in Foreign Currency self.setup_dimensions() rate_in_account_currency = 1 - dpt = "Research & Development" + dpt = "Research & Development - _TC" si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) si.department = dpt @@ -1492,7 +1497,7 @@ class TestAccountsController(FrappeTestCase): def test_93_dimension_inheritance_on_advance(self): self.setup_dimensions() - dpt = "Research & Development" + dpt = "Research & Development - _TC" adv = self.create_payment_entry(amount=1, source_exc_rate=85) adv.department = dpt @@ -1739,3 +1744,228 @@ class TestAccountsController(FrappeTestCase): # Exchange Gain/Loss Journal should've been cancelled exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) self.assertEqual(exc_je_for_je1, []) +<<<<<<< HEAD +======= + + def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self): + """ + Customer advance booked under Liability + """ + self.setup_advance_accounts_in_party_master() + + adv = self.create_payment_entry(amount=1, source_exc_rate=83) + adv.save() # explicit 'save' is needed to trigger set_liability_account() + self.assertEqual(adv.paid_from, self.advance_received_usd) + adv.submit() + + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + si.debit_to = self.debtors_usd + si.save().submit() + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + pr = self.create_payment_reconciliation() + pr.receivable_payable_account = self.debtors_usd + pr.default_advance_account = self.advance_received_usd + pr.get_unreconciled_entries() + self.assertEqual(pr.invoices[0].invoice_number, si.name) + self.assertEqual(pr.payments[0].reference_name, adv.name) + + # Allocate and Reconcile + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exc Gain/Loss journal should've been creatad + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + adv.reload() + adv.cancel() + si.reload() + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + # Exc Gain/Loss journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 0) + self.assertEqual(len(exc_je_for_adv), 0) + + self.remove_advance_accounts_from_party_master() + + def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self): + """ + Supplier advance booked under Asset + """ + self.setup_advance_accounts_in_party_master() + + usd_amount = 1 + inr_amount = 85 + exc_rate = 85 + adv = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.advance_paid_usd, + paid_amount=inr_amount, + ) + adv.source_exchange_rate = 1 + adv.target_exchange_rate = exc_rate + adv.received_amount = usd_amount + adv.paid_amount = exc_rate * usd_amount + adv.posting_date = nowdate() + adv.save() + # Make sure that advance account is still set + self.assertEqual(adv.paid_to, self.advance_paid_usd) + adv.submit() + + pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1) + self.assertEqual(pi.credit_to, self.creditors_usd) + self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) + + pr = self.create_payment_reconciliation() + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.default_advance_account = self.advance_paid_usd + pr.get_unreconciled_entries() + self.assertEqual(pr.invoices[0].invoice_number, pi.name) + self.assertEqual(pr.payments[0].reference_name, adv.name) + + # Allocate and Reconcile + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0) + + # Exc Gain/Loss journal should've been creatad + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_pi, exc_je_for_adv) + + adv.reload() + adv.cancel() + pi.reload() + self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) + # Exc Gain/Loss journal should've been cancelled + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 0) + self.assertEqual(len(exc_je_for_adv), 0) + + self.remove_advance_accounts_from_party_master() + + def test_difference_posting_date_in_pi_and_si(self): + self.setup_advance_accounts_in_party_master() + + # create payment entry for customer + adv = self.create_payment_entry(amount=1, source_exc_rate=83) + adv.save() + self.assertEqual(adv.paid_from, self.advance_received_usd) + adv.submit() + adv.reload() + + # create sales invoice with advance received + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + si.debit_to = self.debtors_usd + si.append( + "advances", + { + "reference_type": adv.doctype, + "reference_name": adv.name, + "remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19", + "advance_amount": 1.0, + "allocated_amount": 1.0, + "exchange_gain_loss": 3.0, + "ref_exchange_rate": 83.0, + "difference_posting_date": add_days(nowdate(), -2), + }, + ) + si.save().submit() + + # exc Gain/Loss journal should've been creatad + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # check jv created with difference_posting_date in sales invoice + jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent) + sales_invoice = frappe.get_doc("Sales Invoice", si.name) + self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date) + + # create payment entry for supplier + usd_amount = 1 + inr_amount = 85 + exc_rate = 85 + adv = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.advance_paid_usd, + paid_amount=inr_amount, + ) + adv.source_exchange_rate = 1 + adv.target_exchange_rate = exc_rate + adv.received_amount = usd_amount + adv.paid_amount = exc_rate * usd_amount + adv.posting_date = nowdate() + adv.save() + self.assertEqual(adv.paid_to, self.advance_paid_usd) + adv.submit() + + # create purchase invoice with advance paid + pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + pi.append( + "advances", + { + "reference_type": adv.doctype, + "reference_name": adv.name, + "remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20", + "advance_amount": 1.0, + "allocated_amount": 1.0, + "exchange_gain_loss": 5.0, + "ref_exchange_rate": 85.0, + "difference_posting_date": add_days(nowdate(), -2), + }, + ) + pi.save().submit() + self.assertEqual(pi.credit_to, self.creditors_usd) + + # exc Gain/Loss journal should've been creatad + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_pi, exc_je_for_adv) + + # check jv created with difference_posting_date in purchase invoice + journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent) + purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name) + self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date) + + def test_company_validation_in_dimension(self): + si = create_sales_invoice(do_not_submit=True) + project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"}) + si.project = project.name + self.assertRaises(frappe.ValidationError, si.save) + + si_1 = create_sales_invoice(do_not_submit=True) + si_1.items[0].project = project.name + self.assertRaises(frappe.ValidationError, si_1.save) +>>>>>>> c94091d68f (test: add new unit test for company validation in accounting dimension) From 914f4bffea3446df6753f040c8fffd356406b6bb Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Mon, 20 Jan 2025 18:01:31 +0530 Subject: [PATCH 12/23] fix: use user defined discount amount or default (cherry picked from commit e2a32b72578c2ed3dd54297cca54f293c4131a25) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 ++-- erpnext/public/js/controllers/transaction.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 2f833237763..fce25daf1e1 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -328,8 +328,8 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get("child_docname"), - "discount_percentage": 0.0, - "discount_amount": 0, + "discount_percentage": args.get("discount_percentage") or 0.0, + "discount_amount": args.get("discount_amount") or 0.0, } ) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9f8aa84f836..49953f3224f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1527,7 +1527,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "serial_no": d.serial_no, "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, - "conversion_factor": d.conversion_factor || 1.0 + "conversion_factor": d.conversion_factor || 1.0, + "discount_percentage" : d.discount_percentage, + "discount_amount" : d.discount_amount, }); // if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list From c484563beacccbd05b50bf02d22e6d30fcf98ed7 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Thu, 23 Jan 2025 11:55:12 +0530 Subject: [PATCH 13/23] fix: remove applied pricing rule (cherry picked from commit 50223c6bec3e5a497034246945aa0188a415921b) # Conflicts: # erpnext/public/js/controllers/transaction.js --- .../doctype/pricing_rule/pricing_rule.py | 2 - erpnext/public/js/controllers/transaction.js | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index fce25daf1e1..902af5c2a77 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -328,8 +328,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get("child_docname"), - "discount_percentage": args.get("discount_percentage") or 0.0, - "discount_amount": args.get("discount_amount") or 0.0, } ) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 49953f3224f..99022e867cc 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1443,6 +1443,41 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } +<<<<<<< HEAD +======= + remove_pricing_rule_for_item(item) { + // capture pricing rule before removing it to delete free items + let removed_pricing_rule = item.pricing_rules; + if (item.pricing_rules){ + let me = this; + return this.frm.call({ + method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item", + args: { + pricing_rules: item.pricing_rules, + item_details: { + "doctype": item.doctype, + "name": item.name, + "item_code": item.item_code, + "pricing_rules": item.pricing_rules, + "parenttype": item.parenttype, + "parent": item.parent, + "price_list_rate": item.price_list_rate + }, + item_code: item.item_code, + rate: item.price_list_rate, + }, + callback: function(r) { + if (!r.exc && r.message) { + me.remove_pricing_rule(r.message, removed_pricing_rule, item.name); + me.calculate_taxes_and_totals(); + if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); + } + } + }); + } + } + +>>>>>>> 50223c6bec (fix: remove applied pricing rule) apply_pricing_rule(item, calculate_taxes_and_totals) { var me = this; var args = this._get_args(item); @@ -1705,7 +1740,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } +<<<<<<< HEAD remove_pricing_rule(item) { +======= + remove_pricing_rule(item, removed_pricing_rule, row_name) { +>>>>>>> 50223c6bec (fix: remove applied pricing rule) let me = this; const fields = ["discount_percentage", "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; @@ -1739,6 +1778,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.trigger_price_list_rate(); } + else if(!item.is_free_item && row_name){ + me.frm.doc.items.forEach(d => { + if (d.name != row_name) return; + + Object.assign(d, item); + }); + } } trigger_price_list_rate() { From 03068ab96c94dae49d592af65ff08afaaa22af3c Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 17 Jan 2025 11:46:17 +0530 Subject: [PATCH 14/23] fix: set company related values (cherry picked from commit 454067198ee8a8f649cc9db7109032587c43a098) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b7546a9ce33..a407f90b51f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3860,6 +3860,7 @@ class TestSalesInvoice(FrappeTestCase): si = create_sales_invoice(do_not_submit=True) project = frappe.new_doc("Project") + project.company = "_Test Company" project.project_name = "Test Total Billed Amount" project.save() From 9767dc61a620f7a3d2bf97d8d10f9efa652400fb Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Tue, 21 Jan 2025 17:38:24 +0530 Subject: [PATCH 15/23] chore: update variable names for improved readability (cherry picked from commit 36bae552992f4d9009e96397db052e6e85cc2775) --- erpnext/controllers/accounts_controller.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 65dfc380fd7..055dc5aae8f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -384,7 +384,7 @@ class AccountsController(TransactionBase): def validate_company_in_accounting_dimension(self): doc_field = DocType("DocField") accounting_dimension = DocType("Accounting Dimension") - query = ( + dimension_list = ( frappe.qb.from_(accounting_dimension) .select(accounting_dimension.document_type) .join(doc_field) @@ -392,12 +392,11 @@ class AccountsController(TransactionBase): .where(doc_field.fieldname == "company") ).run(as_list=True) - dimension_list = sum(query, ["Project"]) + dimension_list = sum(dimension_list, ["Project"]) self.validate_company(dimension_list) - if childs := self.get_all_children(): - for child in childs: - self.validate_company(dimension_list, child) + for child in self.get_all_children() or []: + self.validate_company(dimension_list, child) def validate_company(self, dimension_list, child=None): for dimension in dimension_list: From b8e4d80b4e1c1ab2dcc727deb57a6023330e855c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Jan 2025 15:48:28 +0530 Subject: [PATCH 16/23] chore: resolve conflicts --- erpnext/controllers/accounts_controller.py | 26 -- .../tests/test_accounts_controller.py | 230 ------------------ 2 files changed, 256 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 055dc5aae8f..160c478f299 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -250,16 +250,8 @@ class AccountsController(TransactionBase): apply_pricing_rule_on_transaction(self) self.set_total_in_words() -<<<<<<< HEAD -======= - self.set_default_letter_head() self.validate_company_in_accounting_dimension() - def set_default_letter_head(self): - if hasattr(self, "letter_head") and not self.letter_head: - self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head") ->>>>>>> 60efd3e219 (feat: add company level validation for accounting dimension) - def init_internal_values(self): # init all the internal values as 0 on sa if self.docstatus.is_draft(): @@ -364,23 +356,6 @@ class AccountsController(TransactionBase): (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) ).run() -<<<<<<< HEAD -======= - def remove_serial_and_batch_bundle(self): - bundles = frappe.get_all( - "Serial and Batch Bundle", - filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)}, - ) - - for bundle in bundles: - frappe.delete_doc("Serial and Batch Bundle", bundle.name) - - batches = frappe.get_all( - "Batch", filters={"reference_doctype": self.doctype, "reference_name": self.name} - ) - for row in batches: - frappe.delete_doc("Batch", row.name) - def validate_company_in_accounting_dimension(self): doc_field = DocType("DocField") accounting_dimension = DocType("Accounting Dimension") @@ -414,7 +389,6 @@ class AccountsController(TransactionBase): ) ) ->>>>>>> 60efd3e219 (feat: add company level validation for accounting dimension) def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 4be13be81fe..1e0f4cce27b 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -14,11 +14,6 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account -<<<<<<< HEAD -======= -from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer -from erpnext.projects.doctype.project.test_project import make_project ->>>>>>> c94091d68f (test: add new unit test for company validation in accounting dimension) from erpnext.stock.doctype.item.test_item import create_item @@ -1744,228 +1739,3 @@ class TestAccountsController(FrappeTestCase): # Exchange Gain/Loss Journal should've been cancelled exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) self.assertEqual(exc_je_for_je1, []) -<<<<<<< HEAD -======= - - def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self): - """ - Customer advance booked under Liability - """ - self.setup_advance_accounts_in_party_master() - - adv = self.create_payment_entry(amount=1, source_exc_rate=83) - adv.save() # explicit 'save' is needed to trigger set_liability_account() - self.assertEqual(adv.paid_from, self.advance_received_usd) - adv.submit() - - si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - si.debit_to = self.debtors_usd - si.save().submit() - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) - - pr = self.create_payment_reconciliation() - pr.receivable_payable_account = self.debtors_usd - pr.default_advance_account = self.advance_received_usd - pr.get_unreconciled_entries() - self.assertEqual(pr.invoices[0].invoice_number, si.name) - self.assertEqual(pr.payments[0].reference_name, adv.name) - - # Allocate and Reconcile - invoices = [x.as_dict() for x in pr.invoices] - payments = [x.as_dict() for x in pr.payments] - pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() - self.assertEqual(len(pr.invoices), 0) - self.assertEqual(len(pr.payments), 0) - self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) - - # Exc Gain/Loss journal should've been creatad - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) - - adv.reload() - adv.cancel() - si.reload() - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) - # Exc Gain/Loss journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 0) - self.assertEqual(len(exc_je_for_adv), 0) - - self.remove_advance_accounts_from_party_master() - - def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self): - """ - Supplier advance booked under Asset - """ - self.setup_advance_accounts_in_party_master() - - usd_amount = 1 - inr_amount = 85 - exc_rate = 85 - adv = create_payment_entry( - company=self.company, - payment_type="Pay", - party_type="Supplier", - party=self.supplier, - paid_from=self.cash, - paid_to=self.advance_paid_usd, - paid_amount=inr_amount, - ) - adv.source_exchange_rate = 1 - adv.target_exchange_rate = exc_rate - adv.received_amount = usd_amount - adv.paid_amount = exc_rate * usd_amount - adv.posting_date = nowdate() - adv.save() - # Make sure that advance account is still set - self.assertEqual(adv.paid_to, self.advance_paid_usd) - adv.submit() - - pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1) - self.assertEqual(pi.credit_to, self.creditors_usd) - self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) - - pr = self.create_payment_reconciliation() - pr.party_type = "Supplier" - pr.party = self.supplier - pr.receivable_payable_account = self.creditors_usd - pr.default_advance_account = self.advance_paid_usd - pr.get_unreconciled_entries() - self.assertEqual(pr.invoices[0].invoice_number, pi.name) - self.assertEqual(pr.payments[0].reference_name, adv.name) - - # Allocate and Reconcile - invoices = [x.as_dict() for x in pr.invoices] - payments = [x.as_dict() for x in pr.payments] - pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() - self.assertEqual(len(pr.invoices), 0) - self.assertEqual(len(pr.payments), 0) - self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0) - - # Exc Gain/Loss journal should've been creatad - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_pi, exc_je_for_adv) - - adv.reload() - adv.cancel() - pi.reload() - self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) - # Exc Gain/Loss journal should've been cancelled - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 0) - self.assertEqual(len(exc_je_for_adv), 0) - - self.remove_advance_accounts_from_party_master() - - def test_difference_posting_date_in_pi_and_si(self): - self.setup_advance_accounts_in_party_master() - - # create payment entry for customer - adv = self.create_payment_entry(amount=1, source_exc_rate=83) - adv.save() - self.assertEqual(adv.paid_from, self.advance_received_usd) - adv.submit() - adv.reload() - - # create sales invoice with advance received - si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - si.debit_to = self.debtors_usd - si.append( - "advances", - { - "reference_type": adv.doctype, - "reference_name": adv.name, - "remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19", - "advance_amount": 1.0, - "allocated_amount": 1.0, - "exchange_gain_loss": 3.0, - "ref_exchange_rate": 83.0, - "difference_posting_date": add_days(nowdate(), -2), - }, - ) - si.save().submit() - - # exc Gain/Loss journal should've been creatad - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) - - # check jv created with difference_posting_date in sales invoice - jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent) - sales_invoice = frappe.get_doc("Sales Invoice", si.name) - self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date) - - # create payment entry for supplier - usd_amount = 1 - inr_amount = 85 - exc_rate = 85 - adv = create_payment_entry( - company=self.company, - payment_type="Pay", - party_type="Supplier", - party=self.supplier, - paid_from=self.cash, - paid_to=self.advance_paid_usd, - paid_amount=inr_amount, - ) - adv.source_exchange_rate = 1 - adv.target_exchange_rate = exc_rate - adv.received_amount = usd_amount - adv.paid_amount = exc_rate * usd_amount - adv.posting_date = nowdate() - adv.save() - self.assertEqual(adv.paid_to, self.advance_paid_usd) - adv.submit() - - # create purchase invoice with advance paid - pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - pi.append( - "advances", - { - "reference_type": adv.doctype, - "reference_name": adv.name, - "remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20", - "advance_amount": 1.0, - "allocated_amount": 1.0, - "exchange_gain_loss": 5.0, - "ref_exchange_rate": 85.0, - "difference_posting_date": add_days(nowdate(), -2), - }, - ) - pi.save().submit() - self.assertEqual(pi.credit_to, self.creditors_usd) - - # exc Gain/Loss journal should've been creatad - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_pi, exc_je_for_adv) - - # check jv created with difference_posting_date in purchase invoice - journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent) - purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name) - self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date) - - def test_company_validation_in_dimension(self): - si = create_sales_invoice(do_not_submit=True) - project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"}) - si.project = project.name - self.assertRaises(frappe.ValidationError, si.save) - - si_1 = create_sales_invoice(do_not_submit=True) - si_1.items[0].project = project.name - self.assertRaises(frappe.ValidationError, si_1.save) ->>>>>>> c94091d68f (test: add new unit test for company validation in accounting dimension) From befc16cc971b6ecceb20e53e21b60143a8194369 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Jan 2025 16:11:53 +0530 Subject: [PATCH 17/23] refactor(test): update test data --- erpnext/projects/doctype/project/test_records.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/test_records.json b/erpnext/projects/doctype/project/test_records.json index 567f359b50d..1482336631b 100644 --- a/erpnext/projects/doctype/project/test_records.json +++ b/erpnext/projects/doctype/project/test_records.json @@ -1,6 +1,7 @@ [ { "project_name": "_Test Project", - "status": "Open" + "status": "Open", + "company": "_Test Company" } ] \ No newline at end of file From de531a81b64a5a1d7496416a626788985ec487a3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:57:05 +0100 Subject: [PATCH 18/23] chore: bump actions/cache to v4 (backport #45541) (#45545) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .github/workflows/patch.yml | 6 +++--- .github/workflows/server-tests-mariadb.yml | 6 +++--- .github/workflows/server-tests-postgres.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index c39b4be9acb..61c1eadb307 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -59,7 +59,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -68,7 +68,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -83,7 +83,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 2e272f6b38a..e600f9f7e5d 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -79,7 +79,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -88,7 +88,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -103,7 +103,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index 651c935c153..1e5a57d1dc3 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -66,7 +66,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -75,7 +75,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -90,7 +90,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} From 8befe7f244141301d2f33b1255366354414657b5 Mon Sep 17 00:00:00 2001 From: Sanket Shah <113279972+Sanket322@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:27:28 +0530 Subject: [PATCH 19/23] fix: update fields on change of item code In `Update Items` of `Sales Order` (#45125) * fix: update fields on change of item code * fix: minor update * fix: set the new values always * Revert "fix: set the new values always" This reverts commit 44daa0a641e489515eb73fc35fcea308d6cb49e8. --------- Co-authored-by: Sanket322 Co-authored-by: ruthra kumar (cherry picked from commit 9933d3c8ff047a28dbfb8cf1f76d8f283a204376) --- erpnext/public/js/utils.js | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 4d886d47f4e..b51d10666a8 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -628,6 +628,62 @@ erpnext.utils.update_child_items = function (opts) { filters: filters, }; }, + onchange: function () { + const me = this; + + frm.call({ + method: "erpnext.stock.get_item_details.get_item_details", + args: { + doc: frm.doc, + ctx: { + item_code: this.value, + set_warehouse: frm.doc.set_warehouse, + customer: frm.doc.customer || frm.doc.party_name, + quotation_to: frm.doc.quotation_to, + supplier: frm.doc.supplier, + currency: frm.doc.currency, + is_internal_supplier: frm.doc.is_internal_supplier, + is_internal_customer: frm.doc.is_internal_customer, + conversion_rate: frm.doc.conversion_rate, + price_list: frm.doc.selling_price_list || frm.doc.buying_price_list, + price_list_currency: frm.doc.price_list_currency, + plc_conversion_rate: frm.doc.plc_conversion_rate, + company: frm.doc.company, + order_type: frm.doc.order_type, + is_pos: cint(frm.doc.is_pos), + is_return: cint(frm.doc.is_return), + is_subcontracted: frm.doc.is_subcontracted, + ignore_pricing_rule: frm.doc.ignore_pricing_rule, + doctype: frm.doc.doctype, + name: frm.doc.name, + qty: me.doc.qty || 1, + uom: me.doc.uom, + pos_profile: cint(frm.doc.is_pos) ? frm.doc.pos_profile : "", + tax_category: frm.doc.tax_category, + child_doctype: frm.doc.doctype + " Item", + is_old_subcontracting_flow: frm.doc.is_old_subcontracting_flow, + }, + }, + callback: function (r) { + if (r.message) { + const { qty, price_list_rate: rate, uom, conversion_factor } = r.message; + + const row = dialog.fields_dict.trans_items.df.data.find( + (doc) => doc.idx == me.doc.idx + ); + if (row) { + Object.assign(row, { + conversion_factor: me.doc.conversion_factor || conversion_factor, + uom: me.doc.uom || uom, + qty: me.doc.qty || qty, + rate: me.doc.rate || rate, + }); + dialog.fields_dict.trans_items.grid.refresh(); + } + } + }, + }); + }, }, { fieldtype: "Link", From 9eda931b97d422408f60fa96bfbd84b59088acc3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 29 Jan 2025 11:54:42 +0530 Subject: [PATCH 20/23] chore: resolve conflicts --- erpnext/public/js/controllers/transaction.js | 48 -------------------- 1 file changed, 48 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 99022e867cc..a424d01489d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1443,41 +1443,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } -<<<<<<< HEAD -======= - remove_pricing_rule_for_item(item) { - // capture pricing rule before removing it to delete free items - let removed_pricing_rule = item.pricing_rules; - if (item.pricing_rules){ - let me = this; - return this.frm.call({ - method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item", - args: { - pricing_rules: item.pricing_rules, - item_details: { - "doctype": item.doctype, - "name": item.name, - "item_code": item.item_code, - "pricing_rules": item.pricing_rules, - "parenttype": item.parenttype, - "parent": item.parent, - "price_list_rate": item.price_list_rate - }, - item_code: item.item_code, - rate: item.price_list_rate, - }, - callback: function(r) { - if (!r.exc && r.message) { - me.remove_pricing_rule(r.message, removed_pricing_rule, item.name); - me.calculate_taxes_and_totals(); - if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); - } - } - }); - } - } - ->>>>>>> 50223c6bec (fix: remove applied pricing rule) apply_pricing_rule(item, calculate_taxes_and_totals) { var me = this; var args = this._get_args(item); @@ -1563,8 +1528,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0, - "discount_percentage" : d.discount_percentage, - "discount_amount" : d.discount_amount, }); // if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list @@ -1740,11 +1703,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } -<<<<<<< HEAD remove_pricing_rule(item) { -======= - remove_pricing_rule(item, removed_pricing_rule, row_name) { ->>>>>>> 50223c6bec (fix: remove applied pricing rule) let me = this; const fields = ["discount_percentage", "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; @@ -1778,13 +1737,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.trigger_price_list_rate(); } - else if(!item.is_free_item && row_name){ - me.frm.doc.items.forEach(d => { - if (d.name != row_name) return; - - Object.assign(d, item); - }); - } } trigger_price_list_rate() { From 1160df93501a33312ddf94f36018e78d55039cb7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:45:48 +0530 Subject: [PATCH 21/23] fix: add multiple item issue in stock entry (backport #45544) (#45579) fix: add multiple item issue in stock entry (#45544) (cherry picked from commit 5a023dc8d47dc12dcf2e3f031a409f592f54a867) Co-authored-by: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com> --- erpnext/stock/doctype/stock_entry/stock_entry.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6d18a21fbe3..1eefd552c29 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -803,7 +803,12 @@ frappe.ui.form.on('Stock Entry Detail', { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { if (v) { - frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered + // set_value trigger barcode function and barcode set qty to 1 in stock_controller.js, to avoid this set value manually instead of set value. + if (k != "barcode") { + frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered + } else { + d.barcode = v; + } } }); refresh_field("items"); From e682d2c9ae0b532abdaf55091f52e74b5dde51ed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:46:13 +0530 Subject: [PATCH 22/23] fix: get stock balance filtered by company for validating stock value in jv (backport #45549) (#45577) * fix: get stock balance filtered by company for validating stock value in jv (#45549) * fix: get stock balance filtered by company for validating stock value in jv * test: error is raised on validate (cherry picked from commit 9f20854bd9da74412cb220ce2caa2e246b9cf169) # Conflicts: # erpnext/accounts/doctype/journal_entry/test_journal_entry.py * fix: conflict --------- Co-authored-by: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Co-authored-by: ljain112 --- erpnext/accounts/utils.py | 2 +- erpnext/stock/utils.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index fe63fa0a654..8cada34f0b9 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1587,7 +1587,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) if wh_details.account == account and not wh_details.is_group ] - total_stock_value = get_stock_value_on(related_warehouses, posting_date) + total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company) precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 058db2a6e20..f951ac019f5 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -54,7 +54,10 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): def get_stock_value_on( - warehouses: list | str | None = None, posting_date: str | None = None, item_code: str | None = None + warehouses: list | str | None = None, + posting_date: str | None = None, + item_code: str | None = None, + company: str | None = None, ) -> float: if not posting_date: posting_date = nowdate() @@ -82,6 +85,9 @@ def get_stock_value_on( if item_code: query = query.where(sle.item_code == item_code) + if company: + query = query.where(sle.company == company) + return query.run(as_list=True)[0][0] From a0155279e0674de130d188febfc05c608712f314 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 28 Jan 2025 17:54:16 +0530 Subject: [PATCH 23/23] fix: update voucher outstanding from payment ledger (cherry picked from commit dd7707035148b4d8a2d36c57f0f5dbc476707734) --- .../purchase_invoice/purchase_invoice.py | 15 ++++++------- .../doctype/sales_invoice/sales_invoice.py | 22 +++++++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e131b01f9f3..045e754734d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -10,7 +10,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date -from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, validate_docs_for_voucher_types, @@ -33,7 +32,7 @@ from erpnext.accounts.general_ledger import ( merge_similar_entries, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.accounts.utils import get_account_currency, get_fiscal_year +from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status @@ -661,12 +660,12 @@ class PurchaseInvoice(BuyingController): def update_supplier_outstanding(self, update_outstanding): if update_outstanding == "No": - update_outstanding_amt( - self.credit_to, - "Supplier", - self.supplier, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, + update_voucher_outstanding( + voucher_type=self.doctype, + voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name, + account=self.credit_to, + party_type="Supplier", + party=self.supplier, ) def get_gl_entries(self, warehouse_account=None): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b3ef8e23985..2d96cb5ff8d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -24,7 +24,11 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import get_due_date, get_party_account, get_party_details -from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + get_account_currency, + update_voucher_outstanding, +) from erpnext.assets.doctype.asset.depreciation import ( depreciate_asset, get_disposal_account_and_cost_center, @@ -1019,14 +1023,14 @@ class SalesInvoice(SellingController): make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": - from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt - - update_outstanding_amt( - self.debit_to, - "Customer", - self.customer, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, + update_voucher_outstanding( + voucher_type=self.doctype, + voucher_no=self.return_against + if cint(self.is_return) and self.return_against + else self.name, + account=self.debit_to, + party_type="Customer", + party=self.customer, ) elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):