diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 8afd313322e..c6c5f207a6a 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -236,14 +236,16 @@ 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: + # 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 + WHERE p.name = c.parent""", + as_dict=1, + ) - return dimensions + return frappe.flags.accounting_dimensions_details def get_dimension_with_children(doctype, dimensions): diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 25ef2ea5c2c..2ceaffc4e25 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -78,6 +78,8 @@ 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/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 80f736fa5bb..65262764294 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,41 @@ 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"): + # nosemgrep + 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): 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/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 6cfd15d3ec8..42b6b44690b 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] @@ -116,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") @@ -142,13 +149,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 diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 6e07b0ec430..939d8410900 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 ( @@ -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( @@ -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) @@ -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" ) 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 ( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 813c70806f4..88dd0113192 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,21 @@ "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate", "read_only": 1 + }, + { + "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" } ], "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..4719876f4ec 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) @@ -663,6 +676,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( @@ -676,7 +693,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, @@ -720,8 +737,9 @@ class PurchaseInvoice(BuyingController): "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) - - 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,44 +876,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: - 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 purchase_receipt_doc: - purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) - purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc - - # 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 expense_booked_in_pr: - # 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( @@ -992,6 +973,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", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 83b7da94110..1e8a912030a 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,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-01-02 17:25:46.027523", + "modified": "2024-03-11 14:20:34.874192", "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..2a86d0daf45 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1059,6 +1059,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( @@ -1072,7 +1076,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, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 38fcd976ad4..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 ( @@ -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( @@ -273,33 +275,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 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) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6ff81be0ab7..de49139adc1 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,82 @@ 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, do_not_submit=True) + si.set_posting_time = True + si.posting_date = add_days(today(), -1) + si.save().submit() + + 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.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, si.name], + [0, 0, 100.0, 0.0, -100.0, cr_note.name], + ] + self.assertEqual(len(report[1]), 2) + 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): """ Payments made against Purchase Order will show up as outstanding amount diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7f5e4debf29..7755f3e0b64 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") @@ -428,7 +430,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, @@ -1009,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( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 22bd6b1d6ff..7953ed2fe2b 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -210,9 +210,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() @@ -474,6 +472,7 @@ def depreciate_asset(asset, date): make_depreciation_entry(asset.name, date) + asset.reload() cancel_depreciation_entries(asset, date) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 2b5d8d0ffdd..76a660d6a9a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -84,6 +84,10 @@ class AssetCapitalization(StockController): 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: asset_doc = frappe.get_doc("Asset", self.target_asset) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0795ab0f747..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", ) @@ -203,17 +204,18 @@ 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}." - ).format( - document_type, - get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), - get_link_to_form(self.doctype, self.get("return_against")), + 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", "Payment Reconciliation"), + ) ) - ) 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)): @@ -1605,8 +1607,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 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 90650a640ad..9b3cf5d7bc7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -354,9 +354,11 @@ 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 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..cb39a9280e4 --- /dev/null +++ b/erpnext/patches/v14_0/remove_cancelled_asset_capitalization_from_asset.py @@ -0,0 +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) 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() 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 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", 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)