From 7312f22f35c66e51587120aa827ad0f144a927a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 29 May 2022 21:33:08 +0530 Subject: [PATCH] refactor: update voucher outstanding from payment ledger Outstanding amount is updated from payment ledger, only for receivable/payable accounts. For remaining account types, update happens from GL Entry. --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 24 ++-- .../payment_ledger_entry.py | 127 ++++++++++++++++++ erpnext/accounts/general_ledger.py | 11 +- erpnext/accounts/utils.py | 30 +++++ 4 files changed, 181 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index e5fa57df7fd..9f716568cc0 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,16 +58,20 @@ class GLEntry(Document): validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) - # Update outstanding amt on against voucher - if ( - self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] - and self.against_voucher - and self.flags.update_outstanding == "Yes" - and not frappe.flags.is_reverse_depr_entry - ): - update_outstanding_amt( - self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher - ) + if frappe.db.get_value("Account", self.account, "account_type") not in [ + "Receivable", + "Payable", + ]: + # Update outstanding amt on against voucher + if ( + self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] + and self.against_voucher + and self.flags.update_outstanding == "Yes" + and not frappe.flags.is_reverse_depr_entry + ): + update_outstanding_amt( + self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher + ) def check_mandatory(self): mandatory = ["account", "voucher_type", "voucher_no", "company"] 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 43e19f4ae7d..52df9234e27 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -6,6 +6,19 @@ import frappe from frappe import _ from frappe.model.document import Document +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.doctype.gl_entry.gl_entry import ( + validate_balance_type, + validate_frozen_account, +) +from erpnext.accounts.utils import update_voucher_outstanding +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError + class PaymentLedgerEntry(Document): def validate_account(self): @@ -18,5 +31,119 @@ class PaymentLedgerEntry(Document): if not valid_account: frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type)) + def validate_account_details(self): + """Account must be ledger, active and not freezed""" + + ret = frappe.db.sql( + """select is_group, docstatus, company + from tabAccount where name=%s""", + self.account, + as_dict=1, + )[0] + + if ret.is_group == 1: + frappe.throw( + _( + """{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions""" + ).format(self.voucher_type, self.voucher_no, self.account) + ) + + if ret.docstatus == 2: + frappe.throw( + _("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account) + ) + + if ret.company != self.company: + frappe.throw( + _("{0} {1}: Account {2} does not belong to Company {3}").format( + self.voucher_type, self.voucher_no, self.account, self.company + ) + ) + + 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 validate_dimensions_for_pl_and_bs(self): + account_type = frappe.db.get_value("Account", self.account, "report_type") + + for dimension in get_checks_for_pl_and_bs_accounts(): + if ( + account_type == "Profit and Loss" + and self.company == dimension.company + and dimension.mandatory_for_pl + and not dimension.disabled + ): + if not self.get(dimension.fieldname): + frappe.throw( + _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format( + dimension.label, self.account + ) + ) + + if ( + account_type == "Balance Sheet" + and self.company == dimension.company + and dimension.mandatory_for_bs + and not dimension.disabled + ): + if not self.get(dimension.fieldname): + frappe.throw( + _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format( + dimension.label, self.account + ) + ) + def validate(self): self.validate_account() + + 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) + + # update outstanding amount + if ( + self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] + and self.flags.update_outstanding == "Yes" + and not frappe.flags.is_reverse_depr_entry + ): + update_voucher_outstanding( + self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party + ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b0513f16a59..81468047058 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -35,7 +35,13 @@ def make_gl_entries( validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) if gl_map and len(gl_map) > 1: - create_payment_ledger_entry(gl_map) + create_payment_ledger_entry( + gl_map, + cancel=0, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + from_repost=from_repost, + ) save_entries(gl_map, adv_adj, update_outstanding, from_repost) # Post GL Map proccess there may no be any GL Entries elif gl_map: @@ -482,6 +488,9 @@ def make_reverse_gl_entries( if gl_entries: create_payment_ledger_entry(gl_entries, cancel=1) + create_payment_ledger_entry( + gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding + ) validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 42a748e1aa1..8daff9d1936 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1433,6 +1433,36 @@ def create_payment_ledger_entry( ple.submit() +def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): + ple = frappe.qb.DocType("Payment Ledger Entry") + vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})] + common_filter = [] + if account: + common_filter.append(ple.account == account) + + if party_type: + common_filter.append(ple.party_type == party_type) + + if party: + common_filter.append(ple.party == party) + + ple_query = QueryPaymentLedger() + + # on cancellation outstanding can be an empty list + voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter) + if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding: + outstanding = voucher_outstanding[0] + ref_doc = frappe.get_doc(voucher_type, voucher_no) + + # Didn't use db_set for optimisation purpose + ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] + frappe.db.set_value( + voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] + ) + + ref_doc.set_status(update=True) + + def delink_original_entry(pl_entry): if pl_entry: ple = qb.DocType("Payment Ledger Entry")