From 33d38ba3a71f00a62688d31722f3dc934f95dc6f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 23 Apr 2024 10:38:37 +0530 Subject: [PATCH 01/17] fix: mode of payment has precedance Mode of Payment is given precedence over company/party bank account (cherry picked from commit 4aef96987988c66ef2f1c41392661711f9b6e98b) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 309141fe4c2..c0716ff19ae 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1088,7 +1088,9 @@ frappe.ui.form.on('Payment Entry', { }, callback: function(r) { if (r.message) { - frm.set_value(field, r.message.account); + if (!frm.doc.mode_of_payment) { + frm.set_value(field, r.message.account); + } frm.set_value('bank', r.message.bank); frm.set_value('bank_account_no', r.message.bank_account_no); } From 113351e8507075196cb37350bb34fa9fb5fb2484 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Apr 2024 22:44:31 +0530 Subject: [PATCH 02/17] perf: timeout issue while submitting purchase receipt (v14) --- erpnext/stock/doctype/serial_no/serial_no.py | 89 ++++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 50f47687828..10d5ae643b0 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -8,16 +8,7 @@ import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname from frappe.query_builder.functions import Coalesce -from frappe.utils import ( - add_days, - cint, - cstr, - flt, - get_link_to_form, - getdate, - nowdate, - safe_json_loads, -) +from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so @@ -618,16 +609,14 @@ def auto_make_serial_nos(args): voucher_type = args.get("voucher_type") item_code = args.get("item_code") for serial_no in serial_nos: - is_new = False if frappe.db.exists("Serial No", serial_no): sr = frappe.get_cached_doc("Serial No", serial_no) - elif args.get("actual_qty", 0) > 0: - sr = frappe.new_doc("Serial No") - is_new = True + sr = update_args_for_serial_no(sr, serial_no, args) + elif args.get("actual_qty", 0) > 0 and serial_no: + created_numbers.append(serial_no) - sr = update_args_for_serial_no(sr, serial_no, args, is_new=is_new) - if is_new: - created_numbers.append(sr.name) + if created_numbers: + make_bulk_serial_nos(args, created_numbers) form_links = list(map(lambda d: get_link_to_form("Serial No", d), created_numbers)) @@ -647,6 +636,72 @@ def auto_make_serial_nos(args): frappe.msgprint(message, multiple_title) +def make_bulk_serial_nos(args, serial_nos): + # for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: + + if isinstance(args, dict): + args = frappe._dict(args) + + serial_nos_details = [] + item_details = frappe.get_cached_value("Item", args.item_code, ["item_name", "description"], as_dict=1) + + supplier = None + if args.voucher_type in ["Purchase Receipt", "Purchase Invoice"]: + supplier = frappe.get_cached_value(args.voucher_type, args.voucher_no, "supplier") + + for serial_no in serial_nos: + serial_nos_details.append( + ( + serial_no, + serial_no, + now(), + now(), + frappe.session.user, + frappe.session.user, + args.warehouse, + args.company, + args.item_code, + item_details.item_name, + item_details.description, + "Active", + args.batch_no, + args.get("work_order"), + supplier, + args.voucher_type, + args.voucher_no, + args.posting_date, + args.posting_time, + flt(args.incoming_rate), + ) + ) + + if serial_nos_details: + fields = [ + "name", + "serial_no", + "creation", + "modified", + "owner", + "modified_by", + "warehouse", + "company", + "item_code", + "item_name", + "description", + "status", + "batch_no", + "work_order", + "supplier", + "purchase_document_type", + "purchase_document_no", + "purchase_date", + "purchase_time", + "purchase_rate", + ] + + frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) + + def get_items_html(serial_nos, item_code): body = ", ".join(serial_nos) return f"""
From 59010c9a617a6e91b3b90b1970c18c7a226b5238 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:26:29 +0530 Subject: [PATCH 03/17] fix: handle stock balance unbuffered_cursor error (backport #41186) (#41187) fix: handle stock balance unbuffered_cursor error (#41186) (cherry picked from commit 341fb6d8f3b507700638c59548093784b12dde04) Co-authored-by: Ankush Menat --- erpnext/stock/report/stock_balance/stock_balance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index f0fddac2c64..5a79cdf1827 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -141,6 +141,8 @@ class StockBalanceReport: if self.filters.get("show_stock_ageing_data"): self.sle_entries = self.sle_query.run(as_dict=True) + # HACK: This is required to avoid causing db query in flt + _system_settings = frappe.get_cached_doc("System Settings") with frappe.db.unbuffered_cursor(): if not self.filters.get("show_stock_ageing_data"): self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) From 3fcdcef1786fac193cf1f6065e927bfe22d2747a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 26 Apr 2024 10:43:30 +0530 Subject: [PATCH 04/17] fix: duplicate column in the stock ledger report (cherry picked from commit be7fd6bfb42081e3e3fbb24e82dd5b3dbdc681d8) --- erpnext/stock/report/stock_ledger/stock_ledger.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index a9f74149d51..bde1434e600 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -156,13 +156,6 @@ def get_columns(filters): "width": 100, "convertible": "qty", }, - { - "label": _("Voucher #"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 150, - }, { "label": _("Warehouse"), "fieldname": "warehouse", From 1a7b3c437dcd18da3425696af597528dc2e9d751 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Thu, 25 Apr 2024 16:42:38 +0530 Subject: [PATCH 05/17] fix: args when get the delivery note in delivery trip (cherry picked from commit 2f359e201d9cbc7e06ccec13ef87280eb6314880) # Conflicts: # erpnext/stock/doctype/delivery_note/delivery_note.py --- erpnext/stock/doctype/delivery_note/delivery_note.py | 6 ++++++ erpnext/stock/doctype/delivery_trip/delivery_trip.js | 1 + 2 files changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 2ad3f485d08..995e1b6e85d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -739,6 +739,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() +<<<<<<< HEAD def make_delivery_trip(source_name, target_doc=None): def update_stop_details(source_doc, target_doc, source_parent): target_doc.customer = source_parent.customer @@ -753,6 +754,11 @@ def make_delivery_trip(source_name, target_doc=None): delivery_notes = [] +======= +def make_delivery_trip(source_name, target_doc=None, kwargs=None): + if not target_doc: + target_doc = frappe.new_doc("Delivery Trip") +>>>>>>> 2f359e201d (fix: args when get the delivery note in delivery trip) doclist = get_mapped_doc( "Delivery Note", source_name, diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 4f8649c0bfa..ba4a3b3486c 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -51,6 +51,7 @@ frappe.ui.form.on("Delivery Trip", { frm.add_custom_button( __("Delivery Note"), () => { + frm.clear_table('delivery_stops'); erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", source_doctype: "Delivery Note", From abe64aa1aba81ac4147b835b45cd8ae6f38dcd96 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Thu, 25 Apr 2024 16:46:29 +0530 Subject: [PATCH 06/17] fix: args when get the delivery note in delivery trip (cherry picked from commit ca577f7aaa595ec05754d54ac3a672ad4187c8af) --- erpnext/stock/doctype/delivery_trip/delivery_trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index ba4a3b3486c..77eae534d17 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Delivery Trip", { frm.add_custom_button( __("Delivery Note"), () => { - frm.clear_table('delivery_stops'); + frm.clear_table("delivery_stops"); erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", source_doctype: "Delivery Note", From 2f6fee98776dd54145ed5ca13a4343e73518df09 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 26 Apr 2024 13:15:32 +0530 Subject: [PATCH 07/17] chore: fix conflicts --- erpnext/stock/doctype/delivery_note/delivery_note.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 995e1b6e85d..18adf6ab1f9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -739,8 +739,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() -<<<<<<< HEAD -def make_delivery_trip(source_name, target_doc=None): +def make_delivery_trip(source_name, target_doc=None, kwargs=None): def update_stop_details(source_doc, target_doc, source_parent): target_doc.customer = source_parent.customer target_doc.address = source_parent.shipping_address_name @@ -754,11 +753,6 @@ def make_delivery_trip(source_name, target_doc=None): delivery_notes = [] -======= -def make_delivery_trip(source_name, target_doc=None, kwargs=None): - if not target_doc: - target_doc = frappe.new_doc("Delivery Trip") ->>>>>>> 2f359e201d (fix: args when get the delivery note in delivery trip) doclist = get_mapped_doc( "Delivery Note", source_name, From 949aa9346c4d28b13dc43a2c31b573572fdddfea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 26 Apr 2024 13:25:46 +0530 Subject: [PATCH 08/17] fix: 'NoneType' object has no attribute '_read_rowdata_packet_unbuffered' --- .../batch_wise_balance_history.py | 1 + erpnext/stock/report/stock_ageing/stock_ageing.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index c8c26fd66cb..8e27b8c4f69 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -131,6 +131,7 @@ def get_stock_ledger_entries(filters): def get_item_warehouse_batch_map(filters, float_precision): + _system_settings = frappe.get_cached_doc("System Settings") with frappe.db.unbuffered_cursor(): sle = get_stock_ledger_entries(filters) sle = sle.run(as_dict=True, as_iterator=True) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 26bf99e1ed7..c09137e645b 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -229,11 +229,12 @@ class FIFOSlots: """ stock_ledger_entries = self.sle + _system_settings = frappe.get_cached_doc("System Settings") with frappe.db.unbuffered_cursor(): - if self.sle is None: - self.sle = self.__get_stock_ledger_entries() + if stock_ledger_entries is None: + stock_ledger_entries = self.__get_stock_ledger_entries() - for d in self.sle: + for d in stock_ledger_entries: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) if d.voucher_type == "Stock Reconciliation": From 1e1319351dfad56e586ebf8be58e5db7a35de82b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:25:51 +0530 Subject: [PATCH 09/17] feat: allow to do reposting for all stock transactions (audit) (backport #41165) (#41205) * feat: allow to do reposting for all transactions (audit) (cherry picked from commit aefbe21b464d7895b412562fb04d91ae6feed373) # Conflicts: # erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json # erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py * chore: fix conflicts * chore: fix conflicts --------- Co-authored-by: Rohit Waghchaure --- erpnext/controllers/stock_controller.py | 7 ++- erpnext/stock/doctype/bin/bin.py | 2 +- .../stock_reposting_settings.json | 11 ++++- .../stock_reposting_settings.py | 4 ++ .../test_stock_reposting_settings.py | 48 +++++++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 15a79c8efb4..41085181720 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1008,7 +1008,12 @@ def is_reposting_pending(): ) -def future_sle_exists(args, sl_entries=None): +def future_sle_exists(args, sl_entries=None, allow_force_reposting=True): + if allow_force_reposting and frappe.db.get_single_value( + "Stock Reposting Settings", "do_reposting_for_each_stock_transaction" + ): + return True + key = (args.voucher_type, args.voucher_no) if not hasattr(frappe.local, "future_sle"): frappe.local.future_sle = {} diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 75753c8b0f5..c7b36b6aa3f 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -202,7 +202,7 @@ def update_qty(bin_name, args): sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction - if future_sle_exists(args): + if future_sle_exists(args, allow_force_reposting=False): last_sle_qty = ( frappe.qb.from_(sle) .select(sle.qty_after_transaction) diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 68afd996b49..cbbb0ce0990 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -13,6 +13,7 @@ "end_time", "limits_dont_apply_on", "item_based_reposting", + "do_reposting_for_each_stock_transaction", "errors_notification_section", "notify_reposting_error_to_role" ], @@ -65,12 +66,18 @@ "fieldname": "errors_notification_section", "fieldtype": "Section Break", "label": "Errors Notification" + }, + { + "default": "0", + "fieldname": "do_reposting_for_each_stock_transaction", + "fieldtype": "Check", + "label": "Do reposting for each Stock Transaction" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-01 16:14:29.080697", + "modified": "2024-04-24 12:19:40.204888", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", @@ -91,4 +98,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index 51fb5ac4c40..10104fb9a60 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -11,6 +11,10 @@ class StockRepostingSettings(Document): def validate(self): self.set_minimum_reposting_time_slot() + def before_save(self): + if self.do_reposting_for_each_stock_transaction: + self.item_based_reposting = 1 + def set_minimum_reposting_time_slot(self): """Ensure that timeslot for reposting is at least 12 hours.""" if not self.limit_reposting_timeslot: diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py index a6dc72d7a42..e53659c1735 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py @@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase): users = get_recipients() self.assertTrue(user in users) + + def test_do_reposting_for_each_stock_transaction(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1) + if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): + frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) + + item = make_item( + "_Test item for reposting check for each transaction", properties={"is_stock_item": 1} + ).name + + stock_entry = make_stock_entry( + item_code=item, + qty=1, + rate=100, + stock_entry_type="Material Receipt", + target="_Test Warehouse - _TC", + ) + + riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") + self.assertTrue(riv) + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) + + def test_do_not_reposting_for_each_stock_transaction(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) + if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): + frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) + + item = make_item( + "_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1} + ).name + + stock_entry = make_stock_entry( + item_code=item, + qty=1, + rate=100, + stock_entry_type="Material Receipt", + target="_Test Warehouse - _TC", + ) + + riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") + self.assertFalse(riv) From 866b0c6ac75c2e02e83513a168b17cccf98c14d0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:41:54 +0530 Subject: [PATCH 10/17] chore: delete invalid translations (backport #41227) (#41228) chore: delete invalid translations (#41227) (cherry picked from commit 067419b7cdbe43d4845e1a98c0889ba3c8de6ce2) Co-authored-by: Ankush Menat --- erpnext/translations/en.csv | 1 - 1 file changed, 1 deletion(-) delete mode 100644 erpnext/translations/en.csv diff --git a/erpnext/translations/en.csv b/erpnext/translations/en.csv deleted file mode 100644 index 7fac9e1785b..00000000000 --- a/erpnext/translations/en.csv +++ /dev/null @@ -1 +0,0 @@ -Married,既婚, From 54313b5db902c108458392b6d389d16e9213b55a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Apr 2024 20:53:10 +0530 Subject: [PATCH 11/17] fix: multiple pricing rules with discount amount and discount percentage not working (#41211) --- .../doctype/pricing_rule/pricing_rule.py | 16 ++++++ .../doctype/pricing_rule/test_pricing_rule.py | 53 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index f39aa026b47..bc30118a0c6 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -486,6 +486,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"): # Apply discount on discounted rate item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100) + elif args.price_list_rate: + value = pricing_rule.get(field, 0) + calculate_discount_percentage = False + if field == "discount_percentage": + field = "discount_amount" + value = args.price_list_rate * (value / 100) + calculate_discount_percentage = True + + if field not in item_details: + item_details.setdefault(field, 0) + + item_details[field] += value if pricing_rule else args.get(field, 0) + if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount: + item_details.discount_percentage = flt( + (flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100 + ) else: if field not in item_details: item_details.setdefault(field, 0) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ab52d6c9729..cb0c223bffa 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -978,6 +978,59 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(so.items[1].item_code, "_Test Item") self.assertEqual(so.items[1].qty, 4) + def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 1", + "name": "_Test Pricing Rule 1", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "discount_percentage": 10, + "apply_multiple_pricing_rules": 1, + "company": "_Test Company", + } + + frappe.get_doc(test_record.copy()).insert() + + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 2", + "name": "_Test Pricing Rule 2", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Amount", + "discount_amount": 100, + "apply_multiple_pricing_rules": 1, + "company": "_Test Company", + } + + frappe.get_doc(test_record.copy()).insert() + + so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True) + self.assertEqual(so.items[0].discount_amount, 200) + self.assertEqual(so.items[0].rate, 800) + + frappe.delete_doc_if_exists("Sales Order", so.name) + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") + test_dependencies = ["Campaign"] From eb22fb9326c93cd7e6d990f2fb85e05f698983df Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 26 Apr 2024 10:50:51 +0530 Subject: [PATCH 12/17] fix: incorrectly applying TDS when Advance is in previous FY (cherry picked from commit b195f519e2266a63d4a38ec7c39c5d23392dad36) --- .../tax_withholding_category.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 58ce2840b4b..d8b2079e5ac 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -253,6 +253,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if taxable_vouchers: tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) + # If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it. + # updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the + # rest of the below logic to function properly + # ---FY 2023-------------||---------------------FY 2024-----------------------||-- + # ---Advance-------------||---------Inv_1--------Inv_2------------------------||-- + if tax_deducted_on_advances: + tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details) + tax_amount = 0 if party_type == "Supplier": @@ -389,7 +397,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details): frappe.qb.from_(at) .inner_join(pe) .on(pe.name == at.parent) - .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount) .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) .where(at.parent.isin(advances)) .where(at.account_head == tax_details.account_head) @@ -414,6 +422,16 @@ def get_deducted_tax(taxable_vouchers, tax_details): return sum(entries) +def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details): + """ + Only applies for Taxes deducted on Advance Payments + """ + advance_tax_from_across_fiscal_year = sum( + [adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date] + ) + return advance_tax_from_across_fiscal_year + + def get_tds_amount(ldc, parties, inv, tax_details, vouchers): tds_amount = 0 invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} From 4d56c46446e6cbd0f979b3452e823e76f2abf148 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Apr 2024 10:25:07 +0530 Subject: [PATCH 13/17] test: TDS deduction across fiscal year (cherry picked from commit 2f9a144023433e8b2a0d5f4be4c98c97ab6e2cab) --- .../test_tax_withholding_category.py | 140 +++++++++++++++++- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index a5509619583..19e6f4d3188 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -1,19 +1,22 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import datetime import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.tests.utils import change_settings -from frappe.utils import today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, today +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice test_dependencies = ["Supplier Group", "Customer Group"] -class TestTaxWithholdingCategory(unittest.TestCase): +class TestTaxWithholdingCategory(FrappeTestCase): @classmethod def setUpClass(self): # create relevant supplier, etc @@ -22,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): make_pan_no_field() def tearDown(self): - cancel_invoices() + frappe.db.rollback() def test_cumulative_threshold_tds(self): frappe.db.set_value( @@ -322,8 +325,6 @@ class TestTaxWithholdingCategory(unittest.TestCase): d.cancel() def test_tds_deduction_for_po_via_payment_entry(self): - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - frappe.db.set_value( "Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS" ) @@ -490,6 +491,133 @@ class TestTaxWithholdingCategory(unittest.TestCase): pi2.cancel() pi3.cancel() + def set_previous_fy_and_tax_category(self): + test_company = "_Test Company" + category = "Cumulative Threshold TDS" + + def add_company_to_fy(fy, company): + if not [x.company for x in fy.companies if x.company == company]: + fy.append("companies", {"company": company}) + fy.save() + + # setup previous fiscal year + fiscal_year = get_fiscal_year(today(), company=test_company) + if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)): + self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0]) + add_company_to_fy(self.prev_fy, test_company) + else: + # make previous fiscal year + start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day) + end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day) + self.prev_fy = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year_start_date": start, + "year_end_date": end, + "companies": [{"company": test_company}], + } + ) + self.prev_fy.save() + + # setup tax withholding category for previous fiscal year + cat = frappe.get_doc("Tax Withholding Category", category) + cat.append( + "rates", + { + "from_date": self.prev_fy.year_start_date, + "to_date": self.prev_fy.year_end_date, + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000, + }, + ) + cat.save() + + def test_tds_across_fiscal_year(self): + """ + Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year + --||-----FY 2023-----||-----FY 2024-----||-- + --||-----Advance-----||---Inv1---Inv2---||-- + """ + self.set_previous_fy_and_tax_category() + supplier = "Test TDS Supplier" + # Cumulative threshold 30000 and tax rate 10% + category = "Cumulative Threshold TDS" + frappe.db.set_value( + "Supplier", + supplier, + { + "tax_withholding_category": category, + "pan": "ABCTY1234D", + }, + ) + po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10) + po = create_purchase_order(supplier=supplier, qty=10, rate=10000) + po.transaction_date = po_and_advance_posting_date + po.taxes = [] + po.apply_tds = False + po.tax_withholding_category = None + po.save().submit() + + # Partial advance + payment = get_payment_entry(po.doctype, po.name) + payment.posting_date = po_and_advance_posting_date + payment.paid_amount = 60000 + payment.apply_tax_withholding_amount = 1 + payment.tax_withholding_category = category + payment.references = [] + payment.taxes = [] + payment.save().submit() + + self.assertEqual(len(payment.taxes), 1) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + + # Multiple partial invoices + payment.reload() + pi1 = make_purchase_invoice(source_name=po.name) + pi1.apply_tds = True + pi1.tax_withholding_category = category + pi1.items[0].qty = 3 + pi1.items[0].rate = 10000 + advances = pi1.get_advance_entries() + pi1.append( + "advances", + { + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "advance_amount": advances[0].amount, + "allocated_amount": 30000, + }, + ) + pi1.save().submit() + pi1.reload() + payment.reload() + self.assertEqual(pi1.taxes, []) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + self.assertEqual(payment.taxes[0].allocated_amount, 3000) + + pi2 = make_purchase_invoice(source_name=po.name) + pi2.apply_tds = True + pi2.tax_withholding_category = category + pi2.items[0].qty = 3 + pi2.items[0].rate = 10000 + advances = pi2.get_advance_entries() + pi2.append( + "advances", + { + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "advance_amount": advances[0].amount, + "allocated_amount": 30000, + }, + ) + pi2.save().submit() + pi2.reload() + payment.reload() + self.assertEqual(pi2.taxes, []) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + self.assertEqual(payment.taxes[0].allocated_amount, 6000) + def cancel_invoices(): purchase_invoices = frappe.get_all( From 58b68b75971ec9a2a4460bdf0ad67020ff9554f3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Apr 2024 19:31:21 +0530 Subject: [PATCH 14/17] fix: display term name for single term invoices (cherry picked from commit 5fa4cfee041b333bf112f9a435d9f3298399f693) --- .../report/accounts_receivable/accounts_receivable.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index dcfd85afddb..25143e555d3 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -501,8 +501,9 @@ class ReceivablePayableReport: # Deduct that from paid amount pre allocation row.paid -= flt(payment_terms_details[0].total_advance) - # If no or single payment terms, no need to split the row - if len(payment_terms_details) <= 1: + # If single payment terms, no need to split the row + if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term: + self.append_payment_term(row, payment_terms_details[0], original_row) return for d in payment_terms_details: From 45c4167c867304dc59e4790368e5d02dad089d6c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 30 Apr 2024 12:24:14 +0530 Subject: [PATCH 15/17] fix: permission issue when user permission restricts on company --- .../tax_withholding_account/tax_withholding_account.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json b/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json index 06d6b088e5b..52a4f4c0eed 100644 --- a/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json +++ b/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json @@ -21,7 +21,7 @@ "fieldname": "company", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 0, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, @@ -53,7 +53,7 @@ "fieldname": "account", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 0, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, @@ -87,7 +87,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-04-13 18:44:25.055382", + "modified": "2024-04-30 10:26:48.21829", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Account", From 831828686587e42881ba0f9131a7c605569c1ba0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 30 Apr 2024 17:06:48 +0530 Subject: [PATCH 16/17] fix: validation to prevent overallocation (cherry picked from commit bf755fab550134fccc344dd220143bf0a4fb4a8c) --- erpnext/accounts/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d966074b2ae..8fc47b0b679 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -479,6 +479,11 @@ def reconcile_against_document( # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) gl_map = doc.build_gl_map() + from erpnext.accounts.general_ledger import process_debit_credit_difference + + # Make sure there is no overallocation + process_debit_credit_difference(gl_map) + create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) # Only update outstanding for newly linked vouchers From 9aa054c4004731c4f1731db93d1ce016d87d7e77 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 2 May 2024 09:38:33 +0530 Subject: [PATCH 17/17] fix: negative stock qty error for stock reconciliation (#41283) fix: negative stock qty error for stock reco --- erpnext/stock/stock_ledger.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 99930768ea5..4e624c9acb0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1490,6 +1490,10 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) else: stock_reco_qty_shift = flt(args.actual_qty) + + elif args.get("serial_no") or args.get("batch_no"): + stock_reco_qty_shift = flt(args.actual_qty) + else: # reco is being submitted last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get( @@ -1559,6 +1563,15 @@ def get_datetime_limit_condition(detail): def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code): return + + if ( + args.voucher_type == "Stock Reconciliation" + and args.actual_qty < 0 + and args.get("batch_no") + and frappe.db.get_value("Stock Reconciliation Item", args.voucher_detail_no, "qty") > 0 + ): + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): return