From 23e256aedf65c8ad9fb39a507772798a374a209a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 26 Feb 2024 12:53:06 +0530 Subject: [PATCH 01/33] fix: Patch to remove cancelled asset capitalization from asset --- .../asset_capitalization/asset_capitalization.py | 4 ++++ erpnext/patches.txt | 1 + ...remove_cancelled_asset_capitalization_from_asset.py | 10 ++++++++++ 3 files changed, 15 insertions(+) create mode 100644 erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 2b5d8d0ffdd..f3464e2aba1 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -83,6 +83,10 @@ class AssetCapitalization(StockController): self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() + + def on_trash(self): + frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) + super(AssetCapitalization, self).on_trash() def cancel_target_asset(self): if self.entry_type == "Capitalization" and self.target_asset: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 125158a5add..e0b8534f9fe 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -359,3 +359,4 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2 erpnext.patches.v14_0.set_maintain_stock_for_bom_item execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1) erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records +erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset diff --git a/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py new file mode 100644 index 00000000000..82f3c205582 --- /dev/null +++ b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + cancelled_asset_capitalizations = frappe.get_all( + "Asset Capitalization", + filters={"docstatus": 2}, + fields=["name", "target_asset"], + ) + for asset_capitalization in cancelled_asset_capitalizations: + frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) From c7ed854850739d39442828019d6a813c5aa064fc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 26 Feb 2024 14:49:32 +0530 Subject: [PATCH 02/33] fix: Reload document on cancel to avoid document modified error --- erpnext/assets/doctype/asset/asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7f5e4debf29..55545610100 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -57,6 +57,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() + self.reload() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() @@ -64,6 +65,7 @@ class Asset(AccountsController): self.validate_cancellation() self.cancel_movement_entries() self.cancel_capitalization() + self.reload() self.delete_depreciation_entries() self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") From d7b738ff61e0ebab62ff2c15247f90b79401e9a4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 27 Feb 2024 14:40:15 +0530 Subject: [PATCH 03/33] perf: Optimization for providional gl entries --- .../purchase_invoice/purchase_invoice.py | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cfaaf767786..35652b71c4d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -720,7 +720,7 @@ class PurchaseInvoice(BuyingController): "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) - + provisional_enpenses_booked_in_pr = False purchase_receipt_doc_map = {} for item in self.get("items"): @@ -859,34 +859,37 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value( - "Purchase Receipt Item", - item.pr_detail, - ["provisional_expense_account", "qty", "base_rate"], - ) - provisional_account = provisional_account or self.get_company_default( - "default_provisional_account" - ) - purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) + if not provisional_enpenses_booked_in_pr: + provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value( + "Purchase Receipt Item", + item.pr_detail, + ["provisional_expense_account", "qty", "base_rate"], + ) + provisional_account = provisional_account or self.get_company_default( + "default_provisional_account" + ) + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt + provision_gle_against_pr = frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "voucher_type": "Purchase Receipt", + "voucher_no": item.purchase_receipt, + "voucher_detail_no": item.pr_detail, + "account": provisional_account, + }, + ["name"], + ) + if provision_gle_against_pr: + provisional_enpenses_booked_in_pr = True - if not purchase_receipt_doc: - purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) - purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc + if provisional_enpenses_booked_in_pr: + purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "voucher_type": "Purchase Receipt", - "voucher_no": item.purchase_receipt, - "voucher_detail_no": item.pr_detail, - "account": provisional_account, - }, - "name", - ) + if not purchase_receipt_doc: + purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) + purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc - if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( item, From f204d810bba5a6fdb3137b780489907c30b650fc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 4 Mar 2024 15:46:15 +0530 Subject: [PATCH 04/33] perf: Performance optimization for validating budget --- erpnext/accounts/doctype/budget/budget.py | 6 +- .../budget_account/budget_account.json | 124 +++++------------- 2 files changed, 40 insertions(+), 90 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 6cfd15d3ec8..2f3b9a4784e 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -109,6 +109,8 @@ class Budget(Document): def validate_expense_against_budget(args, expense_amount=0): args = frappe._dict(args) + if not frappe.get_all("Budget", limit=1): + return if args.get("company") and not args.fiscal_year: args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] @@ -142,13 +144,13 @@ def validate_expense_against_budget(args, expense_amount=0): if ( args.get(budget_against) and args.account - and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) + and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") ): doctype = dimension.get("document_type") if frappe.get_cached_value("DocType", doctype, "is_tree"): - lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) condition = """and exists(select name from `tab%s` where lft<=%s and rgt>=%s and name=b.%s)""" % ( doctype, diff --git a/erpnext/accounts/doctype/budget_account/budget_account.json b/erpnext/accounts/doctype/budget_account/budget_account.json index ead07614a7f..c7d872647f1 100644 --- a/erpnext/accounts/doctype/budget_account/budget_account.json +++ b/erpnext/accounts/doctype/budget_account/budget_account.json @@ -1,94 +1,42 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-05-16 11:54:09.286135", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-05-16 11:54:09.286135", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account", + "budget_amount" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "budget_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Budget Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "budget_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Budget Amount", + "options": "Company:company:default_currency", + "reqd": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-01-02 17:02:53.339420", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budget Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2024-03-04 15:43:27.016947", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budget Account", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 8cd8b8f885419fac8297b171e90be7657357a0e3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 4 Mar 2024 15:47:09 +0530 Subject: [PATCH 05/33] perf: Cached accounting dimensions details --- .../accounting_dimension/accounting_dimension.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 8afd313322e..52dda51708f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -236,14 +236,15 @@ def get_accounting_dimensions(as_list=True, filters=None): def get_checks_for_pl_and_bs_accounts(): - dimensions = frappe.db.sql( - """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs - FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", - as_dict=1, - ) + if frappe.flags.accounting_dimensions_details is None: + frappe.flags.accounting_dimensions_details = frappe.db.sql( + """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs + FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c + WHERE p.name = c.parent""", + as_dict=1, + ) - return dimensions + return frappe.flags.accounting_dimensions_details def get_dimension_with_children(doctype, dimensions): From aa75a6014264853668acd15e996e8fd0af8e7ebe Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 4 Mar 2024 15:47:56 +0530 Subject: [PATCH 06/33] perf: Optimzed code for merging similar gl entries --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 +- erpnext/accounts/general_ledger.py | 43 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 6e07b0ec430..c0f6502910e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -127,7 +127,7 @@ class GLEntry(Document): frappe.throw(msg, title=_("Missing Cost Center")) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") + account_type = frappe.get_cached_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): if ( @@ -252,7 +252,7 @@ class GLEntry(Document): def validate_balance_type(account, adv_adj=False): if not adv_adj and account: - balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") + balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be") if balance_must_be: balance = frappe.db.sql( """select sum(debit) - sum(credit) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 38fcd976ad4..b1223b271f5 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -234,11 +234,13 @@ def get_cost_center_allocation_data(company, posting_date): def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() + merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry - same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions) + same_head = check_if_in_list(entry, merged_gl_map) if same_head: same_head.debit = flt(same_head.debit) + flt(entry.debit) same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( @@ -272,37 +274,34 @@ def merge_similar_entries(gl_map, precision=None): return merged_gl_map - -def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = [ - "voucher_detail_no", - "party", - "against_voucher", +def get_merge_properties(dimensions=None): + merge_properties = [ + "account", "cost_center", - "against_voucher_type", + "party", "party_type", + "voucher_detail_no", + "against_voucher", + "against_voucher_type", "project", "finance_book", ] - if dimensions: - account_head_fieldnames = account_head_fieldnames + dimensions + merge_properties.extend(dimensions) + return merge_properties +def get_merge_key(entry, merge_properties): + merge_key = [] + for fieldname in merge_properties: + merge_key.append(entry.get(fieldname, '')) + + return tuple(merge_key) + +def check_if_in_list(gle, gl_map): for e in gl_map: - same_head = True - if e.account != gle.account: - same_head = False - continue - - for fieldname in account_head_fieldnames: - if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)): - same_head = False - break - - if same_head: + if e.merge_key == gle.merge_key: return e - def toggle_debit_credit_if_negative(gl_map): for entry in gl_map: # toggle debit, credit if negative entry From acc0b2faf82c2b352cc2caf14ef26e6e55f34230 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 6 Mar 2024 17:43:27 +0530 Subject: [PATCH 07/33] fix: linter issues --- erpnext/accounts/general_ledger.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b1223b271f5..432abc250b1 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,7 +7,7 @@ import copy import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, formatdate, getdate, now +from frappe.utils import cint, flt, formatdate, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -274,6 +274,7 @@ def merge_similar_entries(gl_map, precision=None): return merged_gl_map + def get_merge_properties(dimensions=None): merge_properties = [ "account", @@ -290,18 +291,21 @@ def get_merge_properties(dimensions=None): merge_properties.extend(dimensions) return merge_properties + def get_merge_key(entry, merge_properties): merge_key = [] for fieldname in merge_properties: - merge_key.append(entry.get(fieldname, '')) - + merge_key.append(entry.get(fieldname, "")) + return tuple(merge_key) + def check_if_in_list(gle, gl_map): for e in gl_map: if e.merge_key == gle.merge_key: return e + def toggle_debit_credit_if_negative(gl_map): for entry in gl_map: # toggle debit, credit if negative entry From e4bd1738752896e9a50231561c8ca5f3a2ee15c5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 6 Mar 2024 20:32:47 +0530 Subject: [PATCH 08/33] perf: Cache accounting dimension filter map --- .../accounting_dimension_filter.py | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 80f736fa5bb..56f2eb71393 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -38,37 +38,40 @@ class AccountingDimensionFilter(Document): def get_dimension_filter_map(): - filters = frappe.db.sql( - """ - SELECT - a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory - FROM - `tabApplicable On Account` a, `tabAllowed Dimension` d, - `tabAccounting Dimension Filter` p - WHERE - p.name = a.parent - AND p.disabled = 0 - AND p.name = d.parent - """, - as_dict=1, - ) - - dimension_filter_map = {} - - for f in filters: - f.fieldname = scrub(f.accounting_dimension) - - build_map( - dimension_filter_map, - f.fieldname, - f.applicable_on_account, - f.dimension_value, - f.allow_or_restrict, - f.is_mandatory, + if not frappe.flags.get("dimension_filter_map"): + filters = frappe.db.sql( + """ + SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, a.is_mandatory + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p + WHERE + p.name = a.parent + AND p.disabled = 0 + AND p.name = d.parent + """, + as_dict=1, ) - return dimension_filter_map + dimension_filter_map = {} + + for f in filters: + f.fieldname = scrub(f.accounting_dimension) + + build_map( + dimension_filter_map, + f.fieldname, + f.applicable_on_account, + f.dimension_value, + f.allow_or_restrict, + f.is_mandatory, + ) + + frappe.flags.dimension_filter_map = dimension_filter_map + + return frappe.flags.dimension_filter_map def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): From 5cd9bf3bda1c63fc04b6b4db8ddfdb2f9c7b456e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 6 Mar 2024 21:04:07 +0530 Subject: [PATCH 09/33] fix: minor fixes --- .../doctype/accounting_dimension/accounting_dimension.py | 1 + .../doctype/accounting_dimension/test_accounting_dimension.py | 1 + .../accounting_dimension_filter/accounting_dimension_filter.py | 1 + 3 files changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 52dda51708f..c6c5f207a6a 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -237,6 +237,7 @@ def get_accounting_dimensions(as_list=True, filters=None): def get_checks_for_pl_and_bs_accounts(): if frappe.flags.accounting_dimensions_details is None: + # nosemgrep frappe.flags.accounting_dimensions_details = frappe.db.sql( """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 25ef2ea5c2c..6de3215cc59 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -78,6 +78,7 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() + frappe.flags.accounting_dimensions_details = None def create_dimension(): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 56f2eb71393..65262764294 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -39,6 +39,7 @@ class AccountingDimensionFilter(Document): def get_dimension_filter_map(): if not frappe.flags.get("dimension_filter_map"): + # nosemgrep filters = frappe.db.sql( """ SELECT From 05385e4acb0f3af7e9cd19c752df8d7287f73401 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 6 Mar 2024 22:33:36 +0530 Subject: [PATCH 10/33] perf: skip unnecessary validation while transaction cancellation --- .../accounting_dimension/test_accounting_dimension.py | 1 + .../test_accounting_dimension_filter.py | 2 ++ .../doctype/payment_ledger_entry/payment_ledger_entry.py | 9 +++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 6de3215cc59..2ceaffc4e25 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -79,6 +79,7 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None def create_dimension(): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index f13f2f9f279..3dc87bb4e38 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase): def tearDown(self): disable_dimension_filter() disable_dimension() + frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None for si in self.invoice_list: si.load_from_db() diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index bcbcb670faf..bd438ee7606 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -132,11 +132,12 @@ class PaymentLedgerEntry(Document): def on_update(self): adv_adj = self.flags.adv_adj if not self.flags.from_repost: - self.validate_account_details() - self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() - validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) + if not self.delinked: + self.validate_account_details() + self.validate_dimensions_for_pl_and_bs() + self.validate_allowed_dimensions() + validate_balance_type(self.account, adv_adj) # update outstanding amount if ( From 5fcf8c588a2e365ce8670fa3e1d05e4dee359316 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 2 Mar 2024 19:06:48 +0530 Subject: [PATCH 11/33] fix: Book depreciation until the asset disposal date and removed unwanted commits --- erpnext/assets/doctype/asset/asset.py | 4 ++-- erpnext/assets/doctype/asset/depreciation.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7f5e4debf29..23cb4d4d515 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -351,7 +351,7 @@ class Asset(AccountsController): if len(self.get("finance_books")) > 1 and any(start): self.sort_depreciation_schedule() - + def _make_depreciation_schedule( self, finance_book, start, date_of_disposal, value_after_depreciation=None ): @@ -428,7 +428,7 @@ class Asset(AccountsController): schedule_date = get_last_day(schedule_date) # if asset is being sold - if date_of_disposal: + if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal): from_date = self.get_from_date_for_disposal(finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 22bd6b1d6ff..a0c4439ee76 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -157,7 +157,6 @@ def get_depreciation_cost_center_and_depreciation_series_for_company(): return res - @frappe.whitelist() def make_depreciation_entry( asset_name, @@ -210,9 +209,7 @@ def make_depreciation_entry( debit_account, accounting_dimensions, ) - frappe.db.commit() except Exception as e: - frappe.db.rollback() depreciation_posting_error = e asset.set_status() @@ -467,13 +464,14 @@ def restore_asset(asset_name): def depreciate_asset(asset, date): if not asset.calculate_depreciation: return - + asset.flags.ignore_validate_update_after_submit = True asset.prepare_depreciation_data(date_of_disposal=date) asset.save() make_depreciation_entry(asset.name, date) - + + asset.reload() cancel_depreciation_entries(asset, date) From 668a93128b12f0c1d0379779fa312841c5428675 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Mar 2024 12:02:23 +0530 Subject: [PATCH 12/33] fix: linter issues --- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/assets/doctype/asset/depreciation.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 23cb4d4d515..051d2d83a93 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -351,7 +351,7 @@ class Asset(AccountsController): if len(self.get("finance_books")) > 1 and any(start): self.sort_depreciation_schedule() - + def _make_depreciation_schedule( self, finance_book, start, date_of_disposal, value_after_depreciation=None ): diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a0c4439ee76..7953ed2fe2b 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -157,6 +157,7 @@ def get_depreciation_cost_center_and_depreciation_series_for_company(): return res + @frappe.whitelist() def make_depreciation_entry( asset_name, @@ -464,13 +465,13 @@ def restore_asset(asset_name): def depreciate_asset(asset, date): if not asset.calculate_depreciation: return - + asset.flags.ignore_validate_update_after_submit = True asset.prepare_depreciation_data(date_of_disposal=date) asset.save() make_depreciation_entry(asset.name, date) - + asset.reload() cancel_depreciation_entries(asset, date) From ac0dd5083fd958edd03e779617c183eb7a8d8c4f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Mar 2024 12:10:55 +0530 Subject: [PATCH 13/33] fix: linter issues --- .../asset_capitalization/asset_capitalization.py | 2 +- ...e_cancelled_asset_capitalization_from_asset.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index f3464e2aba1..76a660d6a9a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -83,7 +83,7 @@ class AssetCapitalization(StockController): self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() - + def on_trash(self): frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) super(AssetCapitalization, self).on_trash() diff --git a/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py index 82f3c205582..cb39a9280e4 100644 --- a/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py +++ b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py @@ -1,10 +1,11 @@ import frappe + def execute(): - cancelled_asset_capitalizations = frappe.get_all( - "Asset Capitalization", - filters={"docstatus": 2}, - fields=["name", "target_asset"], - ) - for asset_capitalization in cancelled_asset_capitalizations: - frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) + cancelled_asset_capitalizations = frappe.get_all( + "Asset Capitalization", + filters={"docstatus": 2}, + fields=["name", "target_asset"], + ) + for asset_capitalization in cancelled_asset_capitalizations: + frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) From 34c3e75d5c9fe57a16ac573a3f8567488b06a794 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Mar 2024 12:16:41 +0530 Subject: [PATCH 14/33] fix: convert str to date --- erpnext/assets/doctype/asset/asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 55545610100..8b821448c37 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1011,7 +1011,9 @@ class Asset(AccountsController): fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() if ( - purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate() + purchase_document + and self.purchase_receipt_amount + and getdate(self.available_for_use_date) <= getdate() ): gl_entries.append( From 4d6b56e55b80d6353d337993908494fc06f5f9a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Mar 2024 14:02:05 +0530 Subject: [PATCH 15/33] refactor: checkbox to toggle always standalone credit note (cherry picked from commit 2cefe2a20eddf96929d4112db1a082645a5beeef) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- .../doctype/sales_invoice/sales_invoice.json | 13 ++ .../doctype/sales_invoice/sales_invoice.py | 192 +++++++++++++++++- 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 83b7da94110..493c1f2318f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -25,6 +25,7 @@ "is_consolidated", "is_return", "return_against", + "update_outstanding_for_self", "update_billed_amount_in_sales_order", "update_billed_amount_in_delivery_note", "is_debit_note", @@ -2161,6 +2162,14 @@ "fieldtype": "Check", "label": "Don't Create Loyalty Points", "no_copy": 1 + }, + { + "default": "1", + "depends_on": "eval: doc.is_return && doc.return_against", + "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", + "fieldname": "update_outstanding_for_self", + "fieldtype": "Check", + "label": "Update Outstanding for Self" } ], "icon": "fa fa-file-text", @@ -2173,7 +2182,11 @@ "link_fieldname": "consolidated_invoice" } ], +<<<<<<< HEAD "modified": "2024-01-02 17:25:46.027523", +======= + "modified": "2024-03-11 14:20:34.874192", +>>>>>>> 2cefe2a20e (refactor: checkbox to toggle always standalone credit note) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 730c47569fa..82e49e040ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,6 +49,194 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class SalesInvoice(SellingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( + SalesInvoiceAdvance, + ) + from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( + SalesInvoicePayment, + ) + from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( + SalesInvoiceTimesheet, + ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.selling.doctype.sales_team.sales_team import SalesTeam + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + account_for_change_amount: DF.Link | None + additional_discount_account: DF.Link | None + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advances: DF.Table[SalesInvoiceAdvance] + against_income_account: DF.SmallText | None + allocate_advances_automatically: DF.Check + amended_from: DF.Link | None + amount_eligible_for_commission: DF.Currency + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_change_amount: DF.Currency + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.SmallText | None + base_net_total: DF.Currency + base_paid_amount: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + base_write_off_amount: DF.Currency + campaign: DF.Link | None + cash_bank_account: DF.Link | None + change_amount: DF.Currency + commission_rate: DF.Float + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + company_tax_id: DF.Data | None + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + customer: DF.Link | None + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.SmallText | None + debit_to: DF.Link + disable_rounded_total: DF.Check + discount_amount: DF.Currency + dispatch_address: DF.SmallText | None + dispatch_address_name: DF.Link | None + dont_create_loyalty_points: DF.Check + due_date: DF.Date | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_default_payment_terms_template: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.SmallText | None + incoterm: DF.Link | None + inter_company_invoice_reference: DF.Link | None + is_cash_or_non_trade_discount: DF.Check + is_consolidated: DF.Check + is_debit_note: DF.Check + is_discounted: DF.Check + is_internal_customer: DF.Check + is_opening: DF.Literal["No", "Yes"] + is_pos: DF.Check + is_return: DF.Check + items: DF.Table[SalesInvoiceItem] + language: DF.Data | None + letter_head: DF.Link | None + loyalty_amount: DF.Currency + loyalty_points: DF.Int + loyalty_program: DF.Link | None + loyalty_redemption_account: DF.Link | None + loyalty_redemption_cost_center: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["ACC-SINV-.YYYY.-", "ACC-SINV-RET-.YYYY.-"] + net_total: DF.Currency + only_include_allocated_payments: DF.Check + other_charges_calculation: DF.LongText | None + outstanding_amount: DF.Currency + packed_items: DF.Table[PackedItem] + paid_amount: DF.Currency + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + payments: DF.Table[SalesInvoicePayment] + plc_conversion_rate: DF.Float + po_date: DF.Date | None + po_no: DF.Data | None + pos_profile: DF.Link | None + posting_date: DF.Date + posting_time: DF.Time | None + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + redeem_loyalty_points: DF.Check + remarks: DF.SmallText | None + repost_required: DF.Check + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + sales_partner: DF.Link | None + sales_team: DF.Table[SalesTeam] + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + set_posting_time: DF.Check + set_target_warehouse: DF.Link | None + set_warehouse: DF.Link | None + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + source: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "Return", + "Credit Note Issued", + "Submitted", + "Paid", + "Partly Paid", + "Unpaid", + "Unpaid and Discounted", + "Partly Paid and Discounted", + "Overdue and Discounted", + "Overdue", + "Cancelled", + "Internal Transfer", + ] + subscription: DF.Link | None + tax_category: DF.Link | None + tax_id: DF.Data | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + timesheets: DF.Table[SalesInvoiceTimesheet] + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_advance: DF.Currency + total_billing_amount: DF.Currency + total_billing_hours: DF.Float + total_commission: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + unrealized_profit_loss_account: DF.Link | None + update_billed_amount_in_delivery_note: DF.Check + update_billed_amount_in_sales_order: DF.Check + update_outstanding_for_self: DF.Check + update_stock: DF.Check + use_company_roundoff_cost_center: DF.Check + write_off_account: DF.Link | None + write_off_amount: DF.Currency + write_off_cost_center: DF.Link | None + write_off_outstanding_amount_automatically: DF.Check + # end: auto-generated types + +>>>>>>> 2cefe2a20e (refactor: checkbox to toggle always standalone credit note) def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) self.status_updater = [ @@ -1072,7 +1260,9 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.name, + "against_voucher": self.name + if self.is_return and self.return_against and self.update_outstanding_for_self + else self.return_against, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, From 1ddb162ef1beef447189fe49eec423baae29f395 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Mar 2024 14:09:51 +0530 Subject: [PATCH 16/33] chore: update popup message (cherry picked from commit adf13a19c468ca39162f9acea47fe5e45a36bd97) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0795ab0f747..95444886e26 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -203,15 +203,19 @@ class AccountsController(TransactionBase): ) if self.get("is_return") and self.get("return_against") and not self.get("is_pos"): - # if self.get("is_return") and self.get("return_against"): document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" frappe.msgprint( _( - "{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}." + "{0} will be treated as a standalone {0}. If you want {1}'s outstanding to be updated, uncheck {2} checkbox.

Or you can use {3} tool to reconcile against {1} later." ).format( document_type, +<<<<<<< HEAD get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), +======= +>>>>>>> adf13a19c4 (chore: update popup message) get_link_to_form(self.doctype, self.get("return_against")), + frappe.bold("Update Outstanding for Self"), + get_link_to_form("Payment Reconciliation"), ) ) From 6d5e5954406678b90bf3ea4f827d5315909e6934 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Mar 2024 14:41:36 +0530 Subject: [PATCH 17/33] refactor: checkbox in purchase invoice (cherry picked from commit 767f2157e6f2977b73399f1d77fecb2e87d3471d) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py --- .../purchase_invoice/purchase_invoice.json | 21 ++- .../purchase_invoice/purchase_invoice.py | 175 ++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 813c70806f4..1133d93d358 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -22,6 +22,7 @@ "is_paid", "is_return", "return_against", + "update_outstanding_for_self", "update_billed_amount_in_purchase_order", "update_billed_amount_in_purchase_receipt", "apply_tds", @@ -1604,13 +1605,31 @@ "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate", "read_only": 1 +<<<<<<< HEAD +======= + }, + { + "fetch_from": "supplier.supplier_group", + "fieldname": "supplier_group", + "fieldtype": "Link", + "label": "Supplier Group", + "options": "Supplier Group" + }, + { + "default": "1", + "depends_on": "eval: doc.is_return && doc.return_against", + "description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", + "fieldname": "update_outstanding_for_self", + "fieldtype": "Check", + "label": "Update Outstanding for Self" +>>>>>>> 767f2157e6 (refactor: checkbox in purchase invoice) } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-02-25 11:20:28.366808", + "modified": "2024-03-11 14:46:30.298184", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cfaaf767786..7e64cc21f41 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -54,6 +54,181 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class PurchaseInvoice(BuyingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import ( + PurchaseInvoiceAdvance, + ) + from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( + PurchaseInvoiceItem, + ) + from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( + PurchaseTaxesandCharges, + ) + from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import ( + TaxWithheldVouchers, + ) + from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( + PurchaseReceiptItemSupplied, + ) + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advance_tax: DF.Table[AdvanceTax] + advances: DF.Table[PurchaseInvoiceAdvance] + against_expense_account: DF.SmallText | None + allocate_advances_automatically: DF.Check + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + apply_tds: DF.Check + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_paid_amount: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_tax_withholding_net_total: DF.Currency + base_taxes_and_charges_added: DF.Currency + base_taxes_and_charges_deducted: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + base_write_off_amount: DF.Currency + bill_date: DF.Date | None + bill_no: DF.Data | None + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + buying_price_list: DF.Link | None + cash_bank_account: DF.Link | None + clearance_date: DF.Date | None + company: DF.Link | None + contact_display: DF.SmallText | None + contact_email: DF.SmallText | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + credit_to: DF.Link + currency: DF.Link | None + disable_rounded_total: DF.Check + discount_amount: DF.Currency + due_date: DF.Date | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + hold_comment: DF.SmallText | None + ignore_default_payment_terms_template: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + inter_company_invoice_reference: DF.Link | None + is_internal_supplier: DF.Check + is_old_subcontracting_flow: DF.Check + is_opening: DF.Literal["No", "Yes"] + is_paid: DF.Check + is_return: DF.Check + is_subcontracted: DF.Check + items: DF.Table[PurchaseInvoiceItem] + language: DF.Data | None + letter_head: DF.Link | None + mode_of_payment: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["ACC-PINV-.YYYY.-", "ACC-PINV-RET-.YYYY.-"] + net_total: DF.Currency + on_hold: DF.Check + only_include_allocated_payments: DF.Check + other_charges_calculation: DF.LongText | None + outstanding_amount: DF.Currency + paid_amount: DF.Currency + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + per_received: DF.Percent + plc_conversion_rate: DF.Float + posting_date: DF.Date + posting_time: DF.Time | None + price_list_currency: DF.Link | None + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + rejected_warehouse: DF.Link | None + release_date: DF.Date | None + remarks: DF.SmallText | None + repost_required: DF.Check + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + set_from_warehouse: DF.Link | None + set_posting_time: DF.Check + set_warehouse: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + shipping_rule: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "Return", + "Debit Note Issued", + "Submitted", + "Paid", + "Partly Paid", + "Unpaid", + "Overdue", + "Cancelled", + "Internal Transfer", + ] + subscription: DF.Link | None + supplied_items: DF.Table[PurchaseReceiptItemSupplied] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_group: DF.Link | None + supplier_name: DF.Data | None + supplier_warehouse: DF.Link | None + tax_category: DF.Link | None + tax_id: DF.ReadOnly | None + tax_withheld_vouchers: DF.Table[TaxWithheldVouchers] + tax_withholding_category: DF.Link | None + tax_withholding_net_total: DF.Currency + taxes: DF.Table[PurchaseTaxesandCharges] + taxes_and_charges: DF.Link | None + taxes_and_charges_added: DF.Currency + taxes_and_charges_deducted: DF.Currency + tc_name: DF.Link | None + terms: DF.TextEditor | None + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_advance: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + unrealized_profit_loss_account: DF.Link | None + update_billed_amount_in_purchase_order: DF.Check + update_billed_amount_in_purchase_receipt: DF.Check + update_outstanding_for_self: DF.Check + update_stock: DF.Check + use_company_roundoff_cost_center: DF.Check + use_transaction_date_exchange_rate: DF.Check + write_off_account: DF.Link | None + write_off_amount: DF.Currency + write_off_cost_center: DF.Link | None + # end: auto-generated types + +>>>>>>> 767f2157e6 (refactor: checkbox in purchase invoice) def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) self.status_updater = [ From d1e3b2ab6e2a15101adc03cbb706e8746da91ce7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Mar 2024 14:49:28 +0530 Subject: [PATCH 18/33] refactor: post ledger entries based on toggle (cherry picked from commit 77aac6f5716cf852b1f4781589e6c6c560bf4b43) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 +++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7e64cc21f41..a67d9b014c1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -838,6 +838,10 @@ class PurchaseInvoice(BuyingController): ) if grand_total and not self.is_internal_transfer(): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( @@ -851,7 +855,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.name, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "project": self.project, "cost_center": self.cost_center, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 82e49e040ca..bcccc788208 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1247,6 +1247,10 @@ class SalesInvoice(SellingController): ) if grand_total and not self.is_internal_transfer(): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( @@ -1260,9 +1264,7 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.name - if self.is_return and self.return_against and self.update_outstanding_for_self - else self.return_against, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, From adbc20736e27c998883c62bd6d36be4df3d9f47b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 12 Mar 2024 16:05:50 +0530 Subject: [PATCH 19/33] chore: patch for updating flag in Cr/Dr notes (cherry picked from commit 849f47889498bb8c6cbf237dad711fbc0475ef93) --- erpnext/patches.txt | 1 + .../v14_0/update_flag_for_return_invoices.py | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 erpnext/patches/v14_0/update_flag_for_return_invoices.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 90650a640ad..a8de34aab96 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -354,6 +354,7 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency" erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool +erpnext.patches.v14_0.update_flag_for_return_invoices # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 diff --git a/erpnext/patches/v14_0/update_flag_for_return_invoices.py b/erpnext/patches/v14_0/update_flag_for_return_invoices.py new file mode 100644 index 00000000000..feb43beacf8 --- /dev/null +++ b/erpnext/patches/v14_0/update_flag_for_return_invoices.py @@ -0,0 +1,62 @@ +from frappe import qb + + +def execute(): + # Set "update_outstanding_for_self" flag in Credit/Debit Notes + # Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves. + + gle = qb.DocType("GL Entry") + + # Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport + # https://github.com/frappe/erpnext/pull/39497 + creation_date = "2024-01-25" + + si = qb.DocType("Sales Invoice") + if cr_notes := ( + qb.from_(si) + .select(si.name) + .where( + (si.creation.gte(creation_date)) + & (si.docstatus == 1) + & (si.is_return == True) + & (si.return_against.notnull()) + ) + .run() + ): + cr_notes = [x[0] for x in cr_notes] + if docs_that_require_update := ( + qb.from_(gle) + .select(gle.voucher_no) + .distinct() + .where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher)) + .run() + ): + docs_that_require_update = [x[0] for x in docs_that_require_update] + qb.update(si).set(si.update_outstanding_for_self, True).where( + si.name.isin(docs_that_require_update) + ).run() + + pi = qb.DocType("Purchase Invoice") + if dr_notes := ( + qb.from_(pi) + .select(pi.name) + .where( + (pi.creation.gte(creation_date)) + & (pi.docstatus == 1) + & (pi.is_return == True) + & (pi.return_against.notnull()) + ) + .run() + ): + dr_notes = [x[0] for x in dr_notes] + if docs_that_require_update := ( + qb.from_(gle) + .select(gle.voucher_no) + .distinct() + .where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher)) + .run() + ): + docs_that_require_update = [x[0] for x in docs_that_require_update] + qb.update(pi).set(pi.update_outstanding_for_self, True).where( + pi.name.isin(docs_that_require_update) + ).run() From 34053ec59070b9e091e518c6a20751ec37e2365b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 09:55:23 +0530 Subject: [PATCH 20/33] refactor: make AR/AP report aware of always standalone cr/dr notes (cherry picked from commit 4d0c2d8e8226e4ba093d6d19f975743a1023aaa1) --- .../report/accounts_receivable/accounts_receivable.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88e26863cf1..c3ebb018ae9 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -692,7 +692,12 @@ class ReceivablePayableReport(object): def get_return_entries(self): doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" - filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} + filters = { + "is_return": 1, + "docstatus": 1, + "company": self.filters.company, + "update_outstanding_for_self": 0, + } or_filters = {} for party_type in self.party_type: party_field = scrub(party_type) From b7464938304f3a05fe022bedc2f639b9945b8fae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 10:14:21 +0530 Subject: [PATCH 21/33] test: cr note flag to update self (cherry picked from commit ce3b1f09f51f28a8e3733020c70c0e381a2dc8c3) --- .../test_accounts_receivable.py | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6ff81be0ab7..a0f8af5d419 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -62,7 +62,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): pe.insert() pe.submit() - def create_credit_note(self, docname): + def create_credit_note(self, docname, do_not_submit=False): credit_note = create_sales_invoice( company=self.company, customer=self.customer, @@ -72,6 +72,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): cost_center=self.cost_center, is_return=1, return_against=docname, + do_not_submit=do_not_submit, ) return credit_note @@ -149,7 +150,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note - self.create_credit_note(si.name) + cr_note = self.create_credit_note(si.name, do_not_submit=True) + cr_note.update_outstanding_for_self = False + cr_note.save().submit() report = execute(filters) expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to] @@ -167,6 +170,68 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ], ) + def test_cr_note_flag_to_update_self(self): + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_remarks": True, + } + + # check invoice grand total and invoiced column's value for 3 payment terms + si = self.create_sales_invoice(no_payment_schedule=True) + name = si.name + + report = execute(filters) + + expected_data = [100, 100, "No Remarks"] + + self.assertEqual(len(report[1]), 1) + row = report[1][0] + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + + # check invoice grand total, invoiced, paid and outstanding column's value after payment + self.create_payment_entry(si.name) + report = execute(filters) + + expected_data_after_payment = [100, 100, 40, 60] + self.assertEqual(len(report[1]), 1) + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], + ) + + # check invoice grand total, invoiced, paid and outstanding column's value after credit note + cr_note = self.create_credit_note(si.name, do_not_submit=True) + cr_note.posting_date = add_days(today(), 1) + cr_note.update_outstanding_for_self = True + cr_note.save().submit() + report = execute(filters) + + expected_data_after_credit_note = [ + [100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to], + [0, 0, 100.0, 0.0, -100.0, self.debit_to], + ] + self.assertEqual(len(report[1]), 2) + for i in range(2): + row = report[1][i - 1] + # row = report[1][0] + self.assertEqual( + expected_data_after_credit_note[i - 1], + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.party_account, + ], + ) + def test_payment_againt_po_in_receivable_report(self): """ Payments made against Purchase Order will show up as outstanding amount From ae6d1c14cb31907e61a5f30a238a900a4973d2d7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 11:03:56 +0530 Subject: [PATCH 22/33] chore: better popup message (cherry picked from commit 445d2acf5071f2376bee992b5eb6aff71910f562) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 95444886e26..78f1ec1f549 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -203,6 +203,7 @@ class AccountsController(TransactionBase): ) if self.get("is_return") and self.get("return_against") and not self.get("is_pos"): +<<<<<<< HEAD document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" frappe.msgprint( _( @@ -216,8 +217,20 @@ class AccountsController(TransactionBase): get_link_to_form(self.doctype, self.get("return_against")), frappe.bold("Update Outstanding for Self"), get_link_to_form("Payment Reconciliation"), +======= + if self.get("update_outstanding_for_self"): + document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + frappe.msgprint( + _( + "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox.

Or you can use {3} tool to reconcile against {1} later." + ).format( + frappe.bold(document_type), + get_link_to_form(self.doctype, self.get("return_against")), + frappe.bold("Update Outstanding for Self"), + get_link_to_form("Payment Reconciliation"), + ) +>>>>>>> 445d2acf50 (chore: better popup message) ) - ) pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): From 92ccef0ebc82b81bd0e383876fc072b65f59c206 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 12:01:31 +0530 Subject: [PATCH 23/33] chore: resolve conflicts --- .../purchase_invoice/purchase_invoice.json | 10 - .../purchase_invoice/purchase_invoice.py | 175 ---------------- .../doctype/sales_invoice/sales_invoice.json | 4 - .../doctype/sales_invoice/sales_invoice.py | 188 ------------------ erpnext/controllers/accounts_controller.py | 16 -- 5 files changed, 393 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 1133d93d358..88dd0113192 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1605,15 +1605,6 @@ "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate", "read_only": 1 -<<<<<<< HEAD -======= - }, - { - "fetch_from": "supplier.supplier_group", - "fieldname": "supplier_group", - "fieldtype": "Link", - "label": "Supplier Group", - "options": "Supplier Group" }, { "default": "1", @@ -1622,7 +1613,6 @@ "fieldname": "update_outstanding_for_self", "fieldtype": "Check", "label": "Update Outstanding for Self" ->>>>>>> 767f2157e6 (refactor: checkbox in purchase invoice) } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a67d9b014c1..55ffd0eaeee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -54,181 +54,6 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class PurchaseInvoice(BuyingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import ( - PurchaseInvoiceAdvance, - ) - from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( - PurchaseInvoiceItem, - ) - from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( - PurchaseTaxesandCharges, - ) - from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import ( - TaxWithheldVouchers, - ) - from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( - PurchaseReceiptItemSupplied, - ) - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advance_tax: DF.Table[AdvanceTax] - advances: DF.Table[PurchaseInvoiceAdvance] - against_expense_account: DF.SmallText | None - allocate_advances_automatically: DF.Check - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - apply_tds: DF.Check - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_paid_amount: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_tax_withholding_net_total: DF.Currency - base_taxes_and_charges_added: DF.Currency - base_taxes_and_charges_deducted: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - base_write_off_amount: DF.Currency - bill_date: DF.Date | None - bill_no: DF.Data | None - billing_address: DF.Link | None - billing_address_display: DF.SmallText | None - buying_price_list: DF.Link | None - cash_bank_account: DF.Link | None - clearance_date: DF.Date | None - company: DF.Link | None - contact_display: DF.SmallText | None - contact_email: DF.SmallText | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - credit_to: DF.Link - currency: DF.Link | None - disable_rounded_total: DF.Check - discount_amount: DF.Currency - due_date: DF.Date | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - hold_comment: DF.SmallText | None - ignore_default_payment_terms_template: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - inter_company_invoice_reference: DF.Link | None - is_internal_supplier: DF.Check - is_old_subcontracting_flow: DF.Check - is_opening: DF.Literal["No", "Yes"] - is_paid: DF.Check - is_return: DF.Check - is_subcontracted: DF.Check - items: DF.Table[PurchaseInvoiceItem] - language: DF.Data | None - letter_head: DF.Link | None - mode_of_payment: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["ACC-PINV-.YYYY.-", "ACC-PINV-RET-.YYYY.-"] - net_total: DF.Currency - on_hold: DF.Check - only_include_allocated_payments: DF.Check - other_charges_calculation: DF.LongText | None - outstanding_amount: DF.Currency - paid_amount: DF.Currency - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - per_received: DF.Percent - plc_conversion_rate: DF.Float - posting_date: DF.Date - posting_time: DF.Time | None - price_list_currency: DF.Link | None - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - rejected_warehouse: DF.Link | None - release_date: DF.Date | None - remarks: DF.SmallText | None - repost_required: DF.Check - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - set_from_warehouse: DF.Link | None - set_posting_time: DF.Check - set_warehouse: DF.Link | None - shipping_address: DF.Link | None - shipping_address_display: DF.SmallText | None - shipping_rule: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "Return", - "Debit Note Issued", - "Submitted", - "Paid", - "Partly Paid", - "Unpaid", - "Overdue", - "Cancelled", - "Internal Transfer", - ] - subscription: DF.Link | None - supplied_items: DF.Table[PurchaseReceiptItemSupplied] - supplier: DF.Link - supplier_address: DF.Link | None - supplier_group: DF.Link | None - supplier_name: DF.Data | None - supplier_warehouse: DF.Link | None - tax_category: DF.Link | None - tax_id: DF.ReadOnly | None - tax_withheld_vouchers: DF.Table[TaxWithheldVouchers] - tax_withholding_category: DF.Link | None - tax_withholding_net_total: DF.Currency - taxes: DF.Table[PurchaseTaxesandCharges] - taxes_and_charges: DF.Link | None - taxes_and_charges_added: DF.Currency - taxes_and_charges_deducted: DF.Currency - tc_name: DF.Link | None - terms: DF.TextEditor | None - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_advance: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - unrealized_profit_loss_account: DF.Link | None - update_billed_amount_in_purchase_order: DF.Check - update_billed_amount_in_purchase_receipt: DF.Check - update_outstanding_for_self: DF.Check - update_stock: DF.Check - use_company_roundoff_cost_center: DF.Check - use_transaction_date_exchange_rate: DF.Check - write_off_account: DF.Link | None - write_off_amount: DF.Currency - write_off_cost_center: DF.Link | None - # end: auto-generated types - ->>>>>>> 767f2157e6 (refactor: checkbox in purchase invoice) def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 493c1f2318f..1e8a912030a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2182,11 +2182,7 @@ "link_fieldname": "consolidated_invoice" } ], -<<<<<<< HEAD - "modified": "2024-01-02 17:25:46.027523", -======= "modified": "2024-03-11 14:20:34.874192", ->>>>>>> 2cefe2a20e (refactor: checkbox to toggle always standalone credit note) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bcccc788208..2a86d0daf45 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,194 +49,6 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class SalesInvoice(SellingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) - from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) - from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( - SalesInvoiceTimesheet, - ) - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.selling.doctype.sales_team.sales_team import SalesTeam - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - account_for_change_amount: DF.Link | None - additional_discount_account: DF.Link | None - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advances: DF.Table[SalesInvoiceAdvance] - against_income_account: DF.SmallText | None - allocate_advances_automatically: DF.Check - amended_from: DF.Link | None - amount_eligible_for_commission: DF.Currency - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_change_amount: DF.Currency - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.SmallText | None - base_net_total: DF.Currency - base_paid_amount: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - base_write_off_amount: DF.Currency - campaign: DF.Link | None - cash_bank_account: DF.Link | None - change_amount: DF.Currency - commission_rate: DF.Float - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - company_tax_id: DF.Data | None - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - customer: DF.Link | None - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.SmallText | None - debit_to: DF.Link - disable_rounded_total: DF.Check - discount_amount: DF.Currency - dispatch_address: DF.SmallText | None - dispatch_address_name: DF.Link | None - dont_create_loyalty_points: DF.Check - due_date: DF.Date | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_default_payment_terms_template: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.SmallText | None - incoterm: DF.Link | None - inter_company_invoice_reference: DF.Link | None - is_cash_or_non_trade_discount: DF.Check - is_consolidated: DF.Check - is_debit_note: DF.Check - is_discounted: DF.Check - is_internal_customer: DF.Check - is_opening: DF.Literal["No", "Yes"] - is_pos: DF.Check - is_return: DF.Check - items: DF.Table[SalesInvoiceItem] - language: DF.Data | None - letter_head: DF.Link | None - loyalty_amount: DF.Currency - loyalty_points: DF.Int - loyalty_program: DF.Link | None - loyalty_redemption_account: DF.Link | None - loyalty_redemption_cost_center: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["ACC-SINV-.YYYY.-", "ACC-SINV-RET-.YYYY.-"] - net_total: DF.Currency - only_include_allocated_payments: DF.Check - other_charges_calculation: DF.LongText | None - outstanding_amount: DF.Currency - packed_items: DF.Table[PackedItem] - paid_amount: DF.Currency - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - payments: DF.Table[SalesInvoicePayment] - plc_conversion_rate: DF.Float - po_date: DF.Date | None - po_no: DF.Data | None - pos_profile: DF.Link | None - posting_date: DF.Date - posting_time: DF.Time | None - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - redeem_loyalty_points: DF.Check - remarks: DF.SmallText | None - repost_required: DF.Check - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - sales_partner: DF.Link | None - sales_team: DF.Table[SalesTeam] - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - set_posting_time: DF.Check - set_target_warehouse: DF.Link | None - set_warehouse: DF.Link | None - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "Return", - "Credit Note Issued", - "Submitted", - "Paid", - "Partly Paid", - "Unpaid", - "Unpaid and Discounted", - "Partly Paid and Discounted", - "Overdue and Discounted", - "Overdue", - "Cancelled", - "Internal Transfer", - ] - subscription: DF.Link | None - tax_category: DF.Link | None - tax_id: DF.Data | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - timesheets: DF.Table[SalesInvoiceTimesheet] - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_advance: DF.Currency - total_billing_amount: DF.Currency - total_billing_hours: DF.Float - total_commission: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - unrealized_profit_loss_account: DF.Link | None - update_billed_amount_in_delivery_note: DF.Check - update_billed_amount_in_sales_order: DF.Check - update_outstanding_for_self: DF.Check - update_stock: DF.Check - use_company_roundoff_cost_center: DF.Check - write_off_account: DF.Link | None - write_off_amount: DF.Currency - write_off_cost_center: DF.Link | None - write_off_outstanding_amount_automatically: DF.Check - # end: auto-generated types - ->>>>>>> 2cefe2a20e (refactor: checkbox to toggle always standalone credit note) def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78f1ec1f549..d53cb956368 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -203,21 +203,6 @@ class AccountsController(TransactionBase): ) if self.get("is_return") and self.get("return_against") and not self.get("is_pos"): -<<<<<<< HEAD - document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" - frappe.msgprint( - _( - "{0} will be treated as a standalone {0}. If you want {1}'s outstanding to be updated, uncheck {2} checkbox.

Or you can use {3} tool to reconcile against {1} later." - ).format( - document_type, -<<<<<<< HEAD - get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), -======= ->>>>>>> adf13a19c4 (chore: update popup message) - get_link_to_form(self.doctype, self.get("return_against")), - frappe.bold("Update Outstanding for Self"), - get_link_to_form("Payment Reconciliation"), -======= if self.get("update_outstanding_for_self"): document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" frappe.msgprint( @@ -229,7 +214,6 @@ class AccountsController(TransactionBase): frappe.bold("Update Outstanding for Self"), get_link_to_form("Payment Reconciliation"), ) ->>>>>>> 445d2acf50 (chore: better popup message) ) pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" From 49c74369a58c313f114a8a9d99bb3b316febe4cf Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 12:54:13 +0530 Subject: [PATCH 24/33] perf: refactored handling provisional gl entries for non-stock items --- .../purchase_invoice/purchase_invoice.py | 170 ++++++++++-------- 1 file changed, 98 insertions(+), 72 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 35652b71c4d..df64e829dfe 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, throw +from frappe import _, qb, throw from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate @@ -575,13 +575,12 @@ class PurchaseInvoice(BuyingController): self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): - if not gl_entries: - gl_entries = self.get_gl_entries() + update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" + if self.docstatus == 1: + if not gl_entries: + gl_entries = self.get_gl_entries() - if gl_entries: - update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" - - if self.docstatus == 1: + if gl_entries: make_gl_entries( gl_entries, update_outstanding=update_outstanding, @@ -589,29 +588,43 @@ class PurchaseInvoice(BuyingController): from_repost=from_repost, ) self.make_exchange_gain_loss_journal() - elif self.docstatus == 2: - provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - if provisional_entries: - for entry in provisional_entries: - frappe.db.set_value( - "GL Entry", - {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no}, - "is_cancelled", - 1, - ) - - 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, - ) - - elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: + elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + self.cancel_provisional_entries() + + self.update_supplier_outstanding(update_outstanding) + + def cancel_provisional_entries(self): + rows = set() + purchase_receipts = set() + for d in self.items: + if d.purchase_receipt: + purchase_receipts.add(d.purchase_receipt) + rows.add(d.name) + + if rows: + # cancel gl entries + gle = qb.DocType("GL Entry") + gle_update_query = ( + qb.update(gle) + .set(gle.is_cancelled, 1) + .where( + (gle.voucher_type == "Purchase Receipt") + & (gle.voucher_no.isin(purchase_receipts)) + & (gle.voucher_detail_no.isin(rows)) + ) + ) + gle_update_query.run() + + 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, + ) def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) @@ -720,8 +733,9 @@ class PurchaseInvoice(BuyingController): "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) - provisional_enpenses_booked_in_pr = False - purchase_receipt_doc_map = {} + self.provisional_enpenses_booked_in_pr = False + if provisional_accounting_for_non_stock_items: + self.get_provisional_accounts() for item in self.get("items"): if flt(item.base_net_amount): @@ -858,47 +872,7 @@ class PurchaseInvoice(BuyingController): dummy, amount = self.get_amount_and_base_amount(item, None) if provisional_accounting_for_non_stock_items: - if item.purchase_receipt: - if not provisional_enpenses_booked_in_pr: - provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value( - "Purchase Receipt Item", - item.pr_detail, - ["provisional_expense_account", "qty", "base_rate"], - ) - provisional_account = provisional_account or self.get_company_default( - "default_provisional_account" - ) - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - provision_gle_against_pr = frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "voucher_type": "Purchase Receipt", - "voucher_no": item.purchase_receipt, - "voucher_detail_no": item.pr_detail, - "account": provisional_account, - }, - ["name"], - ) - if provision_gle_against_pr: - provisional_enpenses_booked_in_pr = True - - if provisional_enpenses_booked_in_pr: - purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) - - if not purchase_receipt_doc: - purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) - purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc - - # Intentionally passing purchase invoice item to handle partial billing - purchase_receipt_doc.add_provisional_gl_entry( - item, - gl_entries, - self.posting_date, - provisional_account, - reverse=1, - item_amount=(min(item.qty, pr_qty) * pr_base_rate), - ) + self.make_provisional_gl_entry(gl_entries, item) if not self.is_internal_transfer(): gl_entries.append( @@ -995,6 +969,58 @@ class PurchaseInvoice(BuyingController): if item.is_fixed_asset and item.landed_cost_voucher_amount: self.update_gross_purchase_amount_for_linked_assets(item) + def get_provisional_accounts(self): + self.provisional_accounts = frappe._dict() + linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt]) + pr_items = frappe.get_all( + "Purchase Receipt Item", + filters={"parent": ("in", linked_purchase_receipts)}, + fields=["name", "provisional_expense_account", "qty", "base_rate"], + ) + default_provisional_account = self.get_company_default("default_provisional_account") + for item in pr_items: + self.provisional_accounts[item.name] = { + "provisional_account": item.provisional_expense_account or default_provisional_account, + "qty": item.qty, + "base_rate": item.base_rate, + } + + def make_provisional_gl_entry(self, gl_entries, item): + if item.purchase_receipt: + if not self.provisional_enpenses_booked_in_pr: + pr_item = self.provisional_accounts.get(item.pr_detail, {}) + provisional_account = pr_item.get("provisional_account") + pr_qty = pr_item.get("qty") + pr_base_rate = pr_item.get("base_rate") + + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt + provision_gle_against_pr = frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "voucher_type": "Purchase Receipt", + "voucher_no": item.purchase_receipt, + "voucher_detail_no": item.pr_detail, + "account": provisional_account, + }, + ["name"], + ) + if provision_gle_against_pr: + self.provisional_enpenses_booked_in_pr = True + + if self.provisional_enpenses_booked_in_pr: + purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt) + + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry( + item, + gl_entries, + self.posting_date, + provisional_account, + reverse=1, + item_amount=(min(item.qty, pr_qty) * pr_base_rate), + ) + def update_gross_purchase_amount_for_linked_assets(self, item): assets = frappe.db.get_all( "Asset", From c15b2d5490975a2eb69ee0819611bc46fcd0dd3e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 12:56:28 +0530 Subject: [PATCH 25/33] perf: validate expense against budget only if budget exists --- erpnext/accounts/doctype/budget/budget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 2f3b9a4784e..42b6b44690b 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -118,6 +118,11 @@ def validate_expense_against_budget(args, expense_amount=0): "Company", args.get("company"), "exception_budget_approver_role" ) + if not frappe.get_cached_value( + "Budget", {"fiscal_year": args.fiscal_year, "company": args.company} + ): # nosec + return + if not args.account: args.account = args.get("expense_account") From 6ff9e6ee848e68c1d84bc17ad682b376214c5d59 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 12:57:33 +0530 Subject: [PATCH 26/33] perf: Get bin details only for stock items --- erpnext/stock/get_item_details.py | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 37a6a0d6892..831fcac93ce 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -104,22 +104,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if ( - args.get("doctype") == "Material Request" - and args.get("material_request_type") == "Material Transfer" - ): - out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) - - elif out.get("warehouse"): - if doc and doc.get("doctype") == "Purchase Order": - # calculate company_total_stock only for po - bin_details = get_bin_details( - args.item_code, out.warehouse, args.company, include_child_warehouses=True - ) - else: - bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) - - out.update(bin_details) + if item.is_stock_item: + update_bin_details(args, out, doc) # update args with out, if key or value not exists for key, value in out.items(): @@ -202,6 +188,24 @@ def set_valuation_rate(out, args): out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) +def update_bin_details(args, out, doc): + if ( + args.get("doctype") == "Material Request" + and args.get("material_request_type") == "Material Transfer" + ): + out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) + + elif out.get("warehouse"): + company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None + + # calculate company_total_stock only for po + bin_details = get_bin_details( + args.item_code, out.warehouse, company, include_child_warehouses=True + ) + + out.update(bin_details) + + def process_args(args): if isinstance(args, str): args = json.loads(args) From d279e23623f13f93c6f7c870fef0c2335e6d2b15 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 12:58:13 +0530 Subject: [PATCH 27/33] fix: added index for price_list column in Item Price --- erpnext/stock/doctype/item_price/item_price.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index f4d9bb0742d..e83730fa23d 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -104,7 +104,8 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "bold": 1, @@ -220,7 +221,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 08:26:04.041861", + "modified": "2024-03-13 12:23:39.630290", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", From 8d682fa8840ef69a1d8a78e4bb418668f251585b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 13:00:10 +0530 Subject: [PATCH 28/33] perf: Caching in checking allowance for qty and amount --- erpnext/controllers/accounts_controller.py | 4 ++-- erpnext/controllers/status_updater.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0795ab0f747..cb342c4ead6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1605,8 +1605,8 @@ class AccountsController(TransactionBase): item_allowance = {} global_qty_allowance, global_amount_allowance = None, None - role_allowed_to_over_bill = frappe.db.get_single_value( - "Accounts Settings", "role_allowed_to_over_bill" + role_allowed_to_over_bill = frappe.get_cached_value( + "Accounts Settings", None, "role_allowed_to_over_bill" ) user_roles = frappe.get_roles() diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index d09001c8fc1..04d0dd36775 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -573,6 +573,7 @@ class StatusUpdater(Document): ref_doc.set_status(update=True) +@frappe.request_cache def get_allowance_for( item_code, item_allowance=None, @@ -602,20 +603,20 @@ def get_allowance_for( global_amount_allowance, ) - qty_allowance, over_billing_allowance = frappe.db.get_value( + qty_allowance, over_billing_allowance = frappe.get_cached_value( "Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"] ) if qty_or_amount == "qty" and not qty_allowance: if global_qty_allowance == None: global_qty_allowance = flt( - frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance") ) qty_allowance = global_qty_allowance elif qty_or_amount == "amount" and not over_billing_allowance: if global_amount_allowance == None: global_amount_allowance = flt( - frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance") ) over_billing_allowance = global_amount_allowance From b07769d8d7890580a9c47598bd166347ca8c1aec Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Mar 2024 13:02:51 +0530 Subject: [PATCH 29/33] perf: Caching in gl entry --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c0f6502910e..939d8410900 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -159,7 +159,7 @@ class GLEntry(Document): def check_pl_account(self): if ( self.is_opening == "Yes" - and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss" + and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss" and not self.is_cancelled ): frappe.throw( @@ -279,7 +279,7 @@ def update_outstanding_amt( party_condition = "" if against_voucher_type == "Sales Invoice": - party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to") + party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to") account_condition = "and account in ({0}, {1})".format( frappe.db.escape(account), frappe.db.escape(party_account) ) @@ -347,7 +347,7 @@ def update_outstanding_amt( def validate_frozen_account(account, adv_adj=None): frozen_account = frappe.get_cached_value("Account", account, "freeze_account") if frozen_account == "Yes" and not adv_adj: - frozen_accounts_modifier = frappe.db.get_value( + frozen_accounts_modifier = frappe.get_cached_value( "Accounts Settings", None, "frozen_accounts_modifier" ) From 5d178d2e2406f76fcfb342612ba79c119045464e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 13:16:56 +0530 Subject: [PATCH 30/33] refactor: pass 'name' to get_link_to_form --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d53cb956368..8ec6f9f2cc4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -212,7 +212,7 @@ class AccountsController(TransactionBase): frappe.bold(document_type), get_link_to_form(self.doctype, self.get("return_against")), frappe.bold("Update Outstanding for Self"), - get_link_to_form("Payment Reconciliation"), + get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), ) ) From fe54590af7953a5cd96875e2d738e4dc61b4af39 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 21:55:26 +0530 Subject: [PATCH 31/33] fix: incorrect gross profit on the quotation (backport #40438) (#40441) fix: incorrect gross profit on the quotation (#40438) (cherry picked from commit f4a945aee405a3151bd47bf3a91886c3e7448516) Co-authored-by: rohitwaghchaure --- erpnext/controllers/accounts_controller.py | 1 + .../doctype/quotation/test_quotation.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8fd146dca9f..8055b8180de 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -88,6 +88,7 @@ force_item_fields = ( "weight_per_unit", "weight_uom", "total_weight", + "valuation_rate", ) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 4dba030c20e..28f9a7d4f83 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase): self.assertTrue(sales_order.get("payment_schedule")) + def test_gross_profit(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.get_item_details import insert_item_price + + item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1}) + item_code = item_doc.name + make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC") + + selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) + insert_item_price( + frappe._dict( + { + "item_code": item_code, + "price_list": selling_price_list, + "price_list_rate": 300, + "rate": 300, + "conversion_factor": 1, + "discount_amount": 0.0, + "currency": frappe.db.get_value("Price List", selling_price_list, "currency"), + "uom": item_doc.stock_uom, + } + ) + ) + + quotation = make_quotation( + item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list + ) + self.assertEqual(quotation.items[0].valuation_rate, 100) + self.assertEqual(quotation.items[0].gross_profit, 200) + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def test_maintain_rate_in_sales_cycle_is_enforced(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order From 41bda583e86be7686c1a68e90a50de9da8f2534d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Mar 2024 21:24:24 +0530 Subject: [PATCH 32/33] fix: flaky Accounts Receivable test case (cherry picked from commit 40bce240bde508801d07b9c38ededea5b4f297c8) --- .../report/accounts_receivable/test_accounts_receivable.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index a0f8af5d419..0a743423d52 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -207,7 +207,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): # check invoice grand total, invoiced, paid and outstanding column's value after credit note cr_note = self.create_credit_note(si.name, do_not_submit=True) - cr_note.posting_date = add_days(today(), 1) cr_note.update_outstanding_for_self = True cr_note.save().submit() report = execute(filters) @@ -218,10 +217,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): ] self.assertEqual(len(report[1]), 2) for i in range(2): - row = report[1][i - 1] - # row = report[1][0] + row = report[1][i] self.assertEqual( - expected_data_after_credit_note[i - 1], + expected_data_after_credit_note[i], [ row.invoice_grand_total, row.invoiced, From 5fe200bc4d2923b474008d682ecc126caa947be8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Mar 2024 08:54:41 +0530 Subject: [PATCH 33/33] fix(test): manually filter rows and assert --- .../test_accounts_receivable.py | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0a743423d52..de49139adc1 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -182,8 +182,10 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - si = self.create_sales_invoice(no_payment_schedule=True) - name = si.name + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.set_posting_time = True + si.posting_date = add_days(today(), -1) + si.save().submit() report = execute(filters) @@ -212,23 +214,37 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): report = execute(filters) expected_data_after_credit_note = [ - [100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to], - [0, 0, 100.0, 0.0, -100.0, self.debit_to], + [100.0, 100.0, 40.0, 0.0, 60.0, si.name], + [0, 0, 100.0, 0.0, -100.0, cr_note.name], ] self.assertEqual(len(report[1]), 2) - for i in range(2): - row = report[1][i] - self.assertEqual( - expected_data_after_credit_note[i], - [ - row.invoice_grand_total, - row.invoiced, - row.paid, - row.credit_note, - row.outstanding, - row.party_account, - ], - ) + si_row = [ + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.voucher_no, + ] + for row in report[1] + if row.voucher_no == si.name + ][0] + + cr_note_row = [ + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.voucher_no, + ] + for row in report[1] + if row.voucher_no == cr_note.name + ][0] + self.assertEqual(expected_data_after_credit_note[0], si_row) + self.assertEqual(expected_data_after_credit_note[1], cr_note_row) def test_payment_againt_po_in_receivable_report(self): """