diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 22ddc2ffae3..283e9d2f4ef 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -58,6 +58,7 @@ class Account(NestedSet): self.validate_balance_must_be_debit_or_credit() self.validate_account_currency() self.validate_root_company_and_sync_account_to_children() + self.validate_receivable_payable_account_type() def validate_parent(self): """Fetch Parent Details and validate parent account""" @@ -114,6 +115,24 @@ class Account(NestedSet): "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" ) + def validate_receivable_payable_account_type(self): + doc_before_save = self.get_doc_before_save() + receivable_payable_types = ["Receivable", "Payable"] + if ( + doc_before_save + and doc_before_save.account_type in receivable_payable_types + and doc_before_save.account_type != self.account_type + ): + # check for ledger entries + if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1): + msg = _( + "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report" + ).format( + frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type + ) + frappe.msgprint(msg) + self.add_comment("Comment", msg) + def validate_root_details(self): # does not exists parent if frappe.db.exists("Account", self.name): diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 41d79479ca5..b1d53dc1e70 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -13,6 +13,7 @@ "account_type", "account_subtype", "column_break_7", + "disabled", "is_default", "is_company_account", "company", @@ -199,10 +200,16 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Branch Code" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "links": [], - "modified": "2022-05-04 15:49:42.620630", + "modified": "2024-02-02 17:50:09.768835", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index addcf62e5b6..ece27e77ee9 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import ( load_address_and_contact, ) from frappe.model.document import Document +from frappe.utils import comma_and, get_link_to_form class BankAccount(Document): @@ -25,6 +26,17 @@ class BankAccount(Document): def validate(self): self.validate_company() self.validate_iban() + self.validate_account() + + def validate_account(self): + if self.account: + if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) def validate_company(self): if self.is_company_account and not self.company: diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index eb0dc74825d..b35ed01cd9a 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase): frappe.db.delete(dt) make_pos_profile() - add_transactions() - add_vouchers() + + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + gl_account = create_gl_account("_Test Bank " + uniq_identifier) + bank_account = create_bank_account( + gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier + ) + + add_transactions(bank_account=bank_account) + add_vouchers(gl_account=gl_account) # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): @@ -213,7 +221,9 @@ class TestBankTransaction(FrappeTestCase): self.assertEqual(linked_payments[0][2], repayment_entry.name) -def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): +def create_bank_account( + bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account" +): try: frappe.get_doc( { @@ -225,21 +235,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - frappe.get_doc( + bank_account = frappe.get_doc( { "doctype": "Bank Account", - "account_name": "Checking Account", + "account_name": bank_account_name, "bank": bank_name, - "account": account_name, + "account": gl_account, } ).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass + return bank_account.name -def add_transactions(): - create_bank_account() +def create_gl_account(gl_account_name="_Test Bank - _TC"): + gl_account = frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 0, + "account_name": gl_account_name, + } + ).insert() + return gl_account.name + + +def add_transactions(bank_account="_Test Bank - _TC"): doc = frappe.get_doc( { "doctype": "Bank Transaction", @@ -247,7 +271,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1200, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -259,7 +283,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1700, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -271,7 +295,7 @@ def add_transactions(): "date": "2018-10-26", "withdrawal": 690, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -283,7 +307,7 @@ def add_transactions(): "date": "2018-10-27", "deposit": 3900, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -295,13 +319,13 @@ def add_transactions(): "date": "2018-10-27", "withdrawal": 109080, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() -def add_vouchers(): +def add_vouchers(gl_account="_Test Bank - _TC"): try: frappe.get_doc( { @@ -317,7 +341,7 @@ def add_vouchers(): pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" pe.insert() @@ -336,14 +360,14 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Oct 18" pe.reference_date = "2018-10-24" pe.insert() pe.submit() pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Nov 18" pe.reference_date = "2018-11-01" pe.insert() @@ -374,10 +398,10 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1) - pi.cash_bank_account = "_Test Bank - _TC" + pi.cash_bank_account = gl_account pi.insert() pi.submit() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.paid_amount = 690 @@ -386,7 +410,7 @@ def add_vouchers(): pe.submit() si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900) - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.insert() @@ -409,16 +433,12 @@ def add_vouchers(): if not frappe.db.get_value( "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} ): - mode_of_payment.append( - "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} - ) + mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account}) mode_of_payment.save() si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 - si.append( - "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} - ) + si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080}) si.insert() si.submit() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3a564825b55..6e07b0ec430 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,16 +13,9 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( - get_dimension_filter_map, -) from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.exceptions import ( - InvalidAccountCurrency, - InvalidAccountDimensionError, - MandatoryAccountDimensionError, -) +from erpnext.exceptions import InvalidAccountCurrency exclude_from_linked_with = True @@ -54,7 +47,6 @@ class GLEntry(Document): if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher": self.validate_account_details(adv_adj) 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) @@ -164,42 +156,6 @@ class GLEntry(Document): ) ) - def validate_allowed_dimensions(self): - dimension_filter_map = get_dimension_filter_map() - for key, value in dimension_filter_map.items(): - dimension = key[0] - account = key[1] - - if self.account == account: - if value["is_mandatory"] and not self.get(dimension): - frappe.throw( - _("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account) - ), - MandatoryAccountDimensionError, - ) - - if value["allow_or_restrict"] == "Allow": - if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - else: - if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - def check_pl_account(self): if ( self.is_opening == "Yes" diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 0dcb1794b9a..60f288e1f07 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -4,9 +4,13 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import getdate -from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import ( + create_bank_account, + create_gl_account, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_payment_entry, make_payment_order, @@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -class TestPaymentOrder(unittest.TestCase): +class TestPaymentOrder(FrappeTestCase): def setUp(self): - create_bank_account() + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + self.gl_account = create_gl_account("_Test Bank " + uniq_identifier) + self.bank_account = create_bank_account( + gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier + ) def tearDown(self): - for bt in frappe.get_all("Payment Order"): - doc = frappe.get_doc("Payment Order", bt.name) - doc.cancel() - doc.delete() + frappe.db.rollback() def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry( - "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC" + "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account ) payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_date = getdate() - payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.party_bank_account = self.bank_account payment_entry.insert() payment_entry.submit() - doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + doc = create_payment_order_against_payment_entry( + payment_entry, "Payment Entry", self.bank_account + ) reference_doc = doc.get("references")[0] self.assertEqual(reference_doc.reference_name, payment_entry.name) self.assertEqual(reference_doc.reference_doctype, "Payment Entry") @@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase): self.assertEqual(reference_doc.amount, 250) -def create_payment_order_against_payment_entry(ref_doc, order_type): +def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account): payment_order = frappe.get_doc( dict( doctype="Payment Order", company="_Test Company", payment_order_type=order_type, - company_bank_account="Checking Account - Citi Bank", + company_bank_account=bank_account, ) ) doc = make_payment_order(ref_doc.name, payment_order) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index d4b4b37b4ee..222c9628018 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -64,18 +64,6 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" - err_journals = None - if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: - err_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "company": doc.company, - "docstatus": 1, - "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), - }, - as_list=True, - ) - for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -88,8 +76,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) - if err_journals: - filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if doc.ignore_exchange_rate_revaluation_journals: + filters.update({"ignore_err": True}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 134ddddf9e0..38fcd976ad4 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -13,9 +13,13 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( + get_dimension_filter_map, +) from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError def make_gl_entries( @@ -354,6 +358,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): process_debit_credit_difference(gl_map) + dimension_filter_map = get_dimension_filter_map() if gl_map: check_freezing_date(gl_map[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_map) @@ -361,6 +366,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"]) for entry in gl_map: + validate_allowed_dimensions(entry, dimension_filter_map) make_entry(entry, adv_adj, update_outstanding, from_repost) @@ -672,3 +678,39 @@ def set_as_cancel(voucher_type, voucher_no): where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no), ) + + +def validate_allowed_dimensions(gl_entry, dimension_filter_map): + for key, value in dimension_filter_map.items(): + dimension = key[0] + account = key[1] + + if gl_entry.account == account: + if value["is_mandatory"] and not gl_entry.get(dimension): + frappe.throw( + _("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account) + ), + MandatoryAccountDimensionError, + ) + + if value["allow_or_restrict"] == "Allow": + if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) + else: + if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f0ac3d0ffdb..2b5abcb7240 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -193,8 +193,14 @@ frappe.query_reports["General Ledger"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check" + }, + { + "fieldname": "ignore_err", + "label": __("Ignore Exchange Rate Revaluation Journals"), + "fieldtype": "Check" } + ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 95397452b01..269d25b99db 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -231,6 +231,19 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") + if filters.get("ignore_err"): + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index a8c362e78c1..75f94309bcc 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import flt, today from erpnext.accounts.report.general_ledger.general_ledger import execute @@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase): self.assertEqual(data[2]["credit"], 900) self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["credit"], 100) + + def test_ignore_exchange_rate_journals_filter(self): + # create a new account with USD currency + account_name = "Test Debtors USD" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Accounts Receivable - _TC", + "account_type": "Receivable", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "party_type": "Customer", + "party": "_Test Customer USD", + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + accounts = revaluation.get_accounts_data() + revaluation.extend("accounts", accounts) + row = revaluation.accounts[0] + row.new_exchange_rate = 83 + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + revaluation.set_total_gain_loss() + revaluation = revaluation.save().submit() + + # post journal entry for Revaluation doc + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_for_revaluation() + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # With ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": True, + } + ) + ) + self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data])) + + # Without ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": False, + } + ) + ) + self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 8131104b825..9d4846056ea 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -317,6 +317,7 @@ class PurchaseOrder(BuyingController): self.update_requested_qty() self.update_ordered_qty() self.update_reserved_qty_for_subcontract() + self.update_blanket_order() self.notify_update() clear_doctype_notifications(self) @@ -456,6 +457,7 @@ class PurchaseOrder(BuyingController): ) +@frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index b0bbc5d0c71..739a989c79e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -814,6 +814,30 @@ class TestPurchaseOrder(FrappeTestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_blanket_order_on_po_close_and_open(self): + # Step - 1: Create Blanket Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) + + # Step - 2: Create Purchase Order + po = create_purchase_order( + item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name + ) + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + + # Step - 3: Close Purchase Order + po.update_status("Closed") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 0) + + # Step - 4: Re-Open Purchase Order + po.update_status("Re-open") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1016,6 +1040,7 @@ def create_purchase_order(**args): "schedule_date": add_days(nowdate(), 1), "include_exploded_items": args.get("include_exploded_items", 1), "against_blanket_order": args.against_blanket_order, + "against_blanket": args.against_blanket, "material_request": args.material_request, "material_request_item": args.material_request_item, }, diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9035c3327..feb1a9b8828 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -546,7 +546,6 @@ "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", - "no_copy": 1, "options": "Blanket Order" }, { @@ -554,7 +553,6 @@ "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", - "no_copy": 1, "print_hide": 1, "read_only": 1 }, @@ -918,7 +916,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-24 19:07:34.921094", + "modified": "2024-02-05 11:23:24.859435", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a4597da8e27..a3f48141aac 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -671,7 +671,7 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 06ea8336bd6..303548cca42 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -679,17 +679,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - query = """select `tabWarehouse`.name, + warehouse_field = "name" + meta = frappe.get_meta("Warehouse") + if meta.get("show_title_field_in_link") and meta.get("title_field"): + searchfield = meta.get("title_field") + warehouse_field = meta.get("title_field") + + query = """select `tabWarehouse`.`{warehouse_field}`, CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty from `tabWarehouse` left join `tabBin` on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by ifnull(`tabBin`.actual_qty, 0) desc + order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc limit {page_len} offset {start} """.format( + warehouse_field=warehouse_field, bin_conditions=get_filters_cond( doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True ), diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d55aeeacc08..c9b12f4f723 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -586,7 +586,7 @@ class SellingController(StockController): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item) + ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item) ) def set_customer_address(self): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 48bde9dbc98..d5bc7647647 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -746,14 +746,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]; if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_selling_terms); } let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order", "Material Request", "Purchase Receipt"]; // Purchase Invoice is excluded as per issue #3345 if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_buying_terms); } diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index b7fcadb1eca..74f68ea6922 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -171,6 +171,7 @@ class Customer(TransactionBase): if self.flags.is_new_doc: self.link_lead_address_and_contact() + self.copy_communication() self.update_customer_groups() @@ -224,6 +225,17 @@ class Customer(TransactionBase): linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) + def copy_communication(self): + if not self.lead_name or not frappe.db.get_single_value( + "CRM Settings", "carry_forward_communication_and_comments" + ): + return + + from erpnext.crm.utils import copy_comments, link_communications + + copy_comments("Lead", self.lead_name, self) + link_communications("Lead", self.lead_name, self) + def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): frappe.throw( diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 3511cec2fd7..c73dc65f8a8 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -122,6 +122,13 @@ class LandedCostVoucher(Document): self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): + if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1: + frappe.throw( + _( + "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher." + ) + ) + based_on = self.distribute_charges_based_on.lower() if based_on != "distribute manually": diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index b84ccf770b2..b0a0158abf4 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -417,6 +417,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): postprocess, ) + doclist.set_onload("load_after_mapping", False) return doclist diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d44bb4d26d8..274073b638e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1200,16 +1200,16 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + based_on_field = None # Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": - based_on_field = "amount" - else: + if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) total_item_cost = 0 - for item in landed_cost_voucher_doc.items: - total_item_cost += item.get(based_on_field) + if based_on_field: + for item in landed_cost_voucher_doc.items: + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8afe23a1d7a..1d7e4da26d5 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1734,6 +1734,48 @@ class TestStockEntry(FrappeTestCase): self.assertFalse(doc.is_enqueue_action()) frappe.flags.in_test = True + def test_auto_reorder_level(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 001", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["qty", "stock_uom", "stock_qty"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(mri.stock_uom, "Kg") + self.assertEqual(mri.stock_qty, 10) + self.assertEqual(mri.qty, 2) + + for mr in mr_list: + mr.cancel() + mr.delete() + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 10d3ef4b7a9..37a6a0d6892 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -87,7 +87,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru get_party_item_code(args, item, out) - set_valuation_rate(out, args) + if args.get("doctype") in ["Sales Order", "Quotation"]: + set_valuation_rate(out, args) update_party_blanket_order(args, out) @@ -303,7 +304,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of and not item.taxes: + if ( + item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}) + ): item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index e172cecb236..de4242893c6 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -34,73 +34,157 @@ def _reorder_item(): erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] ) - items_to_consider = frappe.db.sql_list( - """select name from `tabItem` item - where is_stock_item=1 and has_variants=0 - and disabled=0 - and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) - and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name) - or (variant_of is not null and variant_of != '' - and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) - )""", - {"today": nowdate()}, - ) + items_to_consider = get_items_for_reorder() if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request( - item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None - ): - if warehouse not in warehouse_company: + def add_to_material_request(**kwargs): + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + if kwargs.warehouse not in warehouse_company: # a disabled warehouse return - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) + reorder_level = flt(kwargs.reorder_level) + reorder_qty = flt(kwargs.reorder_qty) # projected_qty will be 0 if Bin does not exist - if warehouse_group: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group)) + if kwargs.warehouse_group: + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group) + ) else: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse)) + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse) + ) if (reorder_level or reorder_qty) and projected_qty < reorder_level: deficiency = reorder_level - projected_qty if deficiency > reorder_qty: reorder_qty = deficiency - company = warehouse_company.get(warehouse) or default_company + company = warehouse_company.get(kwargs.warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append( - {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + material_requests[kwargs.material_request_type].setdefault(company, []).append( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "reorder_qty": reorder_qty, + "item_details": kwargs.item_details, + } ) - for item_code in items_to_consider: - item = frappe.get_doc("Item", item_code) + for item_code, reorder_levels in items_to_consider.items(): + for d in reorder_levels: + if d.has_variants: + continue - if item.variant_of and not item.get("reorder_levels"): - item.update_template_tables() - - if item.get("reorder_levels"): - for d in item.get("reorder_levels"): - add_to_material_request( - item_code, - d.warehouse, - d.warehouse_reorder_level, - d.warehouse_reorder_qty, - d.material_request_type, - warehouse_group=d.warehouse_group, - ) + add_to_material_request( + item_code=item_code, + warehouse=d.warehouse, + reorder_level=d.warehouse_reorder_level, + reorder_qty=d.warehouse_reorder_qty, + material_request_type=d.material_request_type, + warehouse_group=d.warehouse_group, + item_details=frappe._dict( + { + "item_code": item_code, + "name": item_code, + "item_name": d.item_name, + "item_group": d.item_group, + "brand": d.brand, + "description": d.description, + "stock_uom": d.stock_uom, + "purchase_uom": d.purchase_uom, + } + ), + ) if material_requests: return create_material_request(material_requests) +def get_items_for_reorder() -> dict[str, list]: + reorder_table = frappe.qb.DocType("Item Reorder") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(reorder_table) + .inner_join(item_table) + .on(reorder_table.parent == item_table.name) + .select( + reorder_table.warehouse, + reorder_table.warehouse_group, + reorder_table.material_request_type, + reorder_table.warehouse_reorder_level, + reorder_table.warehouse_reorder_qty, + item_table.name, + item_table.stock_uom, + item_table.purchase_uom, + item_table.description, + item_table.item_name, + item_table.item_group, + item_table.brand, + item_table.variant_of, + item_table.has_variants, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + ) + ) + + data = query.run(as_dict=True) + itemwise_reorder = frappe._dict({}) + for d in data: + itemwise_reorder.setdefault(d.name, []).append(d) + + itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder) + + return itemwise_reorder + + +def get_reorder_levels_for_variants(itemwise_reorder): + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.variant_of, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + & (item_table.variant_of.notnull()) + ) + ) + + variants_item = query.run(as_dict=True) + for row in variants_item: + if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of): + itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, [])) + + return itemwise_reorder + + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} + items_to_consider = list(items_to_consider.keys()) for item_code, warehouse, projected_qty in frappe.db.sql( """select item_code, warehouse, projected_qty @@ -164,7 +248,7 @@ def create_material_request(material_requests): for d in items: d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) + item = d.get("item_details") uom = item.stock_uom conversion_factor = 1.0 @@ -190,6 +274,7 @@ def create_material_request(material_requests): "item_code": d.item_code, "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), "qty": qty, + "conversion_factor": conversion_factor, "uom": uom, "stock_uom": item.stock_uom, "warehouse": d.warehouse, diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7a5a8615d0c..32a2b302d7b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -90,8 +90,7 @@ class StockBalanceReport(object): self.opening_data.setdefault(group_by_key, entry) def prepare_new_data(self): - if not self.sle_entries: - return + self.item_warehouse_map = self.get_item_warehouse_map() if self.filters.get("show_stock_ageing_data"): self.filters["show_warehouse_wise_stock"] = True @@ -99,7 +98,7 @@ class StockBalanceReport(object): _func = itemgetter(1) - self.item_warehouse_map = self.get_item_warehouse_map() + del self.sle_entries variant_values = {} if self.filters.get("show_variant_attributes"): @@ -139,15 +138,22 @@ class StockBalanceReport(object): item_warehouse_map = {} self.opening_vouchers = self.get_opening_vouchers() - for entry in self.sle_entries: - group_by_key = self.get_group_by_key(entry) - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) + if self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True) - self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + with frappe.db.unbuffered_cursor(): + if not self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) - if self.opening_data.get(group_by_key): - del self.opening_data[group_by_key] + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] for group_by_key, entry in self.opening_data.items(): if group_by_key not in item_warehouse_map: @@ -236,7 +242,8 @@ class StockBalanceReport(object): .where( (table.docstatus == 1) & (table.company == self.filters.company) - & ((table.to_date <= self.from_date)) + & (table.to_date <= self.from_date) + & (table.status == "Completed") ) .orderby(table.to_date, order=Order.desc) .limit(1) @@ -289,7 +296,7 @@ class StockBalanceReport(object): if self.filters.get("company"): query = query.where(sle.company == self.filters.get("company")) - self.sle_entries = query.run(as_dict=True) + self.sle_query = query def apply_inventory_dimensions_filters(self, query, sle) -> str: inventory_dimension_fields = self.get_inventory_dimension_fields()