diff --git a/erpnext/accounts/doctype/account_closing_balance/__init__.py b/erpnext/accounts/doctype/account_closing_balance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js new file mode 100644 index 00000000000..e35591474b1 --- /dev/null +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Account Closing Balance", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json new file mode 100644 index 00000000000..8dacb96197a --- /dev/null +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json @@ -0,0 +1,164 @@ +{ + "actions": [], + "creation": "2023-02-21 15:20:59.586811", + "default_view": "List", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "closing_date", + "account", + "cost_center", + "debit", + "credit", + "account_currency", + "debit_in_account_currency", + "credit_in_account_currency", + "project", + "company", + "finance_book", + "period_closing_voucher", + "is_period_closing_voucher_entry" + ], + "fields": [ + { + "fieldname": "closing_date", + "fieldtype": "Date", + "in_filter": 1, + "in_list_view": 1, + "label": "Closing Date", + "oldfieldname": "posting_date", + "oldfieldtype": "Date", + "search_index": 1 + }, + { + "fieldname": "account", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Account", + "oldfieldname": "account", + "oldfieldtype": "Link", + "options": "Account", + "search_index": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Cost Center", + "oldfieldname": "cost_center", + "oldfieldtype": "Link", + "options": "Cost Center" + }, + { + "fieldname": "debit", + "fieldtype": "Currency", + "label": "Debit Amount", + "oldfieldname": "debit", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency" + }, + { + "fieldname": "credit", + "fieldtype": "Currency", + "label": "Credit Amount", + "oldfieldname": "credit", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency" + }, + { + "fieldname": "debit_in_account_currency", + "fieldtype": "Currency", + "label": "Debit Amount in Account Currency", + "options": "account_currency" + }, + { + "fieldname": "credit_in_account_currency", + "fieldtype": "Currency", + "label": "Credit Amount in Account Currency", + "options": "account_currency" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "search_index": 1 + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "period_closing_voucher", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Period Closing Voucher", + "options": "Period Closing Voucher", + "search_index": 1 + }, + { + "default": "0", + "fieldname": "is_period_closing_voucher_entry", + "fieldtype": "Check", + "label": "Is Period Closing Voucher Entry" + } + ], + "icon": "fa fa-list", + "in_create": 1, + "links": [], + "modified": "2023-03-06 08:56:36.393237", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Account Closing Balance", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager" + }, + { + "export": 1, + "read": 1, + "report": 1, + "role": "Auditor" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py new file mode 100644 index 00000000000..7c842372de8 --- /dev/null +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -0,0 +1,127 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import cint, cstr + +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) + + +class AccountClosingBalance(Document): + pass + + +def make_closing_entries(closing_entries, voucher_name): + accounting_dimensions = get_accounting_dimensions() + company = closing_entries[0].get("company") + closing_date = closing_entries[0].get("closing_date") + + previous_closing_entries = get_previous_closing_entries( + company, closing_date, accounting_dimensions + ) + combined_entries = closing_entries + previous_closing_entries + + merged_entries = aggregate_with_last_account_closing_balance( + combined_entries, accounting_dimensions + ) + + for key, value in merged_entries.items(): + cle = frappe.new_doc("Account Closing Balance") + cle.update(value) + cle.update(value["dimensions"]) + cle.update( + { + "period_closing_voucher": voucher_name, + "closing_date": closing_date, + } + ) + cle.submit() + + +def aggregate_with_last_account_closing_balance(entries, accounting_dimensions): + merged_entries = {} + for entry in entries: + key, key_values = generate_key(entry, accounting_dimensions) + merged_entries.setdefault( + key, + { + "debit": 0, + "credit": 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + }, + ) + + merged_entries[key]["dimensions"] = key_values + merged_entries[key]["debit"] += entry.get("debit") + merged_entries[key]["credit"] += entry.get("credit") + merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency") + merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency") + + return merged_entries + + +def generate_key(entry, accounting_dimensions): + key = [ + cstr(entry.get("account")), + cstr(entry.get("account_currency")), + cstr(entry.get("cost_center")), + cstr(entry.get("project")), + cstr(entry.get("finance_book")), + cint(entry.get("is_period_closing_voucher_entry")), + ] + + key_values = { + "company": cstr(entry.get("company")), + "account": cstr(entry.get("account")), + "account_currency": cstr(entry.get("account_currency")), + "cost_center": cstr(entry.get("cost_center")), + "project": cstr(entry.get("project")), + "finance_book": cstr(entry.get("finance_book")), + "is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")), + } + for dimension in accounting_dimensions: + key.append(cstr(entry.get(dimension))) + key_values[dimension] = cstr(entry.get(dimension)) + + return tuple(key), key_values + + +def get_previous_closing_entries(company, closing_date, accounting_dimensions): + entries = [] + last_period_closing_voucher = frappe.db.get_all( + "Period Closing Voucher", + filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)}, + fields=["name"], + order_by="posting_date desc", + limit=1, + ) + + if last_period_closing_voucher: + account_closing_balance = frappe.qb.DocType("Account Closing Balance") + query = frappe.qb.from_(account_closing_balance).select( + account_closing_balance.company, + account_closing_balance.account, + account_closing_balance.account_currency, + account_closing_balance.debit, + account_closing_balance.credit, + account_closing_balance.debit_in_account_currency, + account_closing_balance.credit_in_account_currency, + account_closing_balance.cost_center, + account_closing_balance.project, + account_closing_balance.finance_book, + account_closing_balance.is_period_closing_voucher_entry, + ) + + for dimension in accounting_dimensions: + query = query.select(account_closing_balance[dimension]) + + query = query.where( + account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name + ) + entries = query.run(as_dict=1) + + return entries diff --git a/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py new file mode 100644 index 00000000000..fc42677062e --- /dev/null +++ b/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAccountClosingBalance(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index ca98bee5c1c..9d636adc57a 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -4,12 +4,13 @@ import frappe from frappe import _ -from frappe.utils import flt +from frappe.query_builder.functions import Sum +from frappe.utils import add_days, flt from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year from erpnext.controllers.accounts_controller import AccountsController @@ -20,9 +21,17 @@ class PeriodClosingVoucher(AccountsController): def on_submit(self): self.db_set("gle_processing_status", "In Progress") - self.make_gl_entries() + get_opening_entries = False + + if not frappe.db.exists( + "Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)} + ): + get_opening_entries = True + + self.make_gl_entries(get_opening_entries=get_opening_entries) def on_cancel(self): + self.validate_future_closing_vouchers() self.db_set("gle_processing_status", "In Progress") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") gle_count = frappe.db.count( @@ -42,6 +51,25 @@ class PeriodClosingVoucher(AccountsController): else: make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) + self.delete_closing_entries() + + def validate_future_closing_vouchers(self): + if frappe.db.exists( + "Period Closing Voucher", + {"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company}, + ): + frappe.throw( + _( + "You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first" + ) + ) + + def delete_closing_entries(self): + closing_balance = frappe.qb.DocType("Account Closing Balance") + frappe.qb.from_(closing_balance).delete().where( + closing_balance.period_closing_voucher == self.name + ).run() + def validate_account_head(self): closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type") @@ -56,8 +84,6 @@ class PeriodClosingVoucher(AccountsController): frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) def validate_posting_date(self): - from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year - validate_fiscal_year( self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self ) @@ -66,6 +92,8 @@ class PeriodClosingVoucher(AccountsController): self.posting_date, self.fiscal_year, company=self.company )[1] + self.check_if_previous_year_closed() + pce = frappe.db.sql( """select name from `tabPeriod Closing Voucher` where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""", @@ -78,28 +106,64 @@ class PeriodClosingVoucher(AccountsController): ) ) - def make_gl_entries(self): + def check_if_previous_year_closed(self): + last_year_closing = add_days(self.year_start_date, -1) + + previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True) + + if previous_fiscal_year and not frappe.db.exists( + "GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company} + ): + return + + if previous_fiscal_year and not frappe.db.exists( + "Period Closing Voucher", + {"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company}, + ): + frappe.throw(_("Previous Year is not closed, please close it first")) + + def make_gl_entries(self, get_opening_entries=False): gl_entries = self.get_gl_entries() + closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) if gl_entries: if len(gl_entries) > 5000: - frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long") + frappe.enqueue( + process_gl_entries, + gl_entries=gl_entries, + closing_entries=closing_entries, + voucher_name=self.name, + queue="long", + ) frappe.msgprint( _("The GL Entries will be processed in the background, it can take a few minutes."), alert=True, ) else: - process_gl_entries(gl_entries) + process_gl_entries(gl_entries, closing_entries, voucher_name=self.name) + + def get_grouped_gl_entries(self, get_opening_entries=False): + closing_entries = [] + for acc in self.get_balances_based_on_dimensions( + group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries + ): + closing_entries.append(self.get_closing_entries(acc)) + + return closing_entries def get_gl_entries(self): gl_entries = [] # pl account - for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True): + for acc in self.get_balances_based_on_dimensions( + group_by_account=True, report_type="Profit and Loss" + ): if flt(acc.bal_in_company_currency): gl_entries.append(self.get_gle_for_pl_account(acc)) # closing liability account - for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False): + for acc in self.get_balances_based_on_dimensions( + group_by_account=False, report_type="Profit and Loss" + ): if flt(acc.bal_in_company_currency): gl_entries.append(self.get_gle_for_closing_account(acc)) @@ -108,6 +172,8 @@ class PeriodClosingVoucher(AccountsController): def get_gle_for_pl_account(self, acc): gl_entry = self.get_gl_dict( { + "company": self.company, + "closing_date": self.posting_date, "account": acc.account, "cost_center": acc.cost_center, "finance_book": acc.finance_book, @@ -120,6 +186,7 @@ class PeriodClosingVoucher(AccountsController): if flt(acc.bal_in_account_currency) > 0 else 0, "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, + "is_period_closing_voucher_entry": 1, }, item=acc, ) @@ -129,6 +196,8 @@ class PeriodClosingVoucher(AccountsController): def get_gle_for_closing_account(self, acc): gl_entry = self.get_gl_dict( { + "company": self.company, + "closing_date": self.posting_date, "account": self.closing_account_head, "cost_center": acc.cost_center, "finance_book": acc.finance_book, @@ -141,12 +210,36 @@ class PeriodClosingVoucher(AccountsController): if flt(acc.bal_in_account_currency) < 0 else 0, "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, + "is_period_closing_voucher_entry": 1, }, item=acc, ) self.update_default_dimensions(gl_entry, acc) return gl_entry + def get_closing_entries(self, acc): + closing_entry = self.get_gl_dict( + { + "company": self.company, + "closing_date": self.posting_date, + "period_closing_voucher": self.name, + "account": acc.account, + "cost_center": acc.cost_center, + "finance_book": acc.finance_book, + "account_currency": acc.account_currency, + "debit_in_account_currency": flt(acc.debit_in_account_currency), + "debit": flt(acc.debit), + "credit_in_account_currency": flt(acc.credit_in_account_currency), + "credit": flt(acc.credit), + }, + item=acc, + ) + + for dimension in self.accounting_dimensions: + closing_entry.update({dimension: acc.get(dimension)}) + + return closing_entry + def update_default_dimensions(self, gl_entry, acc): if not self.accounting_dimensions: self.accounting_dimensions = get_accounting_dimensions() @@ -154,47 +247,88 @@ class PeriodClosingVoucher(AccountsController): for dimension in self.accounting_dimensions: gl_entry.update({dimension: acc.get(dimension)}) - def get_pl_balances_based_on_dimensions(self, group_by_account=False): + def get_balances_based_on_dimensions( + self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False + ): """Get balance for dimension-wise pl accounts""" - dimension_fields = ["t1.cost_center", "t1.finance_book"] + qb_dimension_fields = ["cost_center", "finance_book", "project"] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: - dimension_fields.append("t1.{0}".format(dimension)) + qb_dimension_fields.append(dimension) if group_by_account: - dimension_fields.append("t1.account") + qb_dimension_fields.append("account") - return frappe.db.sql( - """ - select - t2.account_currency, - {dimension_fields}, - sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, - sum(t1.debit) - sum(t1.credit) as bal_in_company_currency - from `tabGL Entry` t1, `tabAccount` t2 - where - t1.is_cancelled = 0 - and t1.account = t2.name - and t2.report_type = 'Profit and Loss' - and t2.docstatus < 2 - and t2.company = %s - and t1.posting_date between %s and %s - group by {dimension_fields} - """.format( - dimension_fields=", ".join(dimension_fields) - ), - (self.company, self.get("year_start_date"), self.posting_date), - as_dict=1, + account_filters = { + "company": self.company, + "is_group": 0, + } + + if report_type: + account_filters.update({"report_type": report_type}) + + accounts = frappe.get_all("Account", filters=account_filters, pluck="name") + + gl_entry = frappe.qb.DocType("GL Entry") + query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency) + + if not for_aggregation: + query = query.select( + (Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_( + "bal_in_account_currency" + ), + (Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"), + ) + else: + query = query.select( + (Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"), + (Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"), + (Sum(gl_entry.debit)).as_("debit"), + (Sum(gl_entry.credit)).as_("credit"), + ) + + for dimension in qb_dimension_fields: + query = query.select(gl_entry[dimension]) + + query = query.where( + (gl_entry.company == self.company) + & (gl_entry.is_cancelled == 0) + & (gl_entry.account.isin(accounts)) ) + if get_opening_entries: + query = query.where( + gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date) + | gl_entry.is_opening + == "Yes" + ) + else: + query = query.where( + gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date) + & gl_entry.is_opening + == "No" + ) -def process_gl_entries(gl_entries): + if for_aggregation: + query = query.where(gl_entry.voucher_type != "Period Closing Voucher") + + for dimension in qb_dimension_fields: + query = query.groupby(gl_entry[dimension]) + + return query.run(as_dict=1) + + +def process_gl_entries(gl_entries, closing_entries, voucher_name=None): + from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( + make_closing_entries, + ) from erpnext.accounts.general_ledger import make_gl_entries try: make_gl_entries(gl_entries, merge_entries=False) + make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name) frappe.db.set_value( "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" ) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index e9ed2e46940..62ae8572e4e 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -16,16 +16,17 @@ from erpnext.accounts.utils import get_fiscal_year, now class TestPeriodClosingVoucher(unittest.TestCase): def test_closing_entry(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") company = create_company() cost_center = create_cost_center("Test Cost Center 1") jv1 = make_journal_entry( + posting_date="2021-03-15", amount=400, account1="Cash - TPC", account2="Sales - TPC", cost_center=cost_center, - posting_date=now(), save=False, ) jv1.company = company @@ -33,18 +34,18 @@ class TestPeriodClosingVoucher(unittest.TestCase): jv1.submit() jv2 = make_journal_entry( + posting_date="2021-03-15", amount=600, account1="Cost of Goods Sold - TPC", account2="Cash - TPC", cost_center=cost_center, - posting_date=now(), save=False, ) jv2.company = company jv2.save() jv2.submit() - pcv = self.make_period_closing_voucher() + pcv = self.make_period_closing_voucher(posting_date="2021-03-31") surplus_account = pcv.closing_account_head expected_gle = ( @@ -65,6 +66,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): def test_cost_center_wise_posting(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") company = create_company() surplus_account = create_account() @@ -81,6 +83,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): debit_to="Debtors - TPC", currency="USD", customer="_Test Customer USD", + posting_date="2021-03-15", ) create_sales_invoice( company=company, @@ -91,9 +94,10 @@ class TestPeriodClosingVoucher(unittest.TestCase): debit_to="Debtors - TPC", currency="USD", customer="_Test Customer USD", + posting_date="2021-03-15", ) - pcv = self.make_period_closing_voucher(submit=False) + pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False) pcv.save() pcv.submit() surplus_account = pcv.closing_account_head @@ -128,12 +132,13 @@ class TestPeriodClosingVoucher(unittest.TestCase): def test_period_closing_with_finance_book_entries(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") company = create_company() surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - si = create_sales_invoice( + create_sales_invoice( company=company, income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", @@ -142,6 +147,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): debit_to="Debtors - TPC", currency="USD", customer="_Test Customer USD", + posting_date="2021-03-15", ) jv = make_journal_entry( @@ -149,14 +155,14 @@ class TestPeriodClosingVoucher(unittest.TestCase): account2="Sales - TPC", amount=400, cost_center=cost_center, - posting_date=now(), + posting_date="2021-03-15", ) jv.company = company jv.finance_book = create_finance_book().name jv.save() jv.submit() - pcv = self.make_period_closing_voucher() + pcv = self.make_period_closing_voucher(posting_date="2021-03-31") surplus_account = pcv.closing_account_head expected_gle = ( @@ -177,14 +183,130 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertSequenceEqual(pcv_gle, expected_gle) - def make_period_closing_voucher(self, submit=True): + def test_gl_entries_restrictions(self): + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") + + company = create_company() + cost_center = create_cost_center("Test Cost Center 1") + + self.make_period_closing_voucher(posting_date="2021-03-31") + + jv1 = make_journal_entry( + posting_date="2021-03-15", + amount=400, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center, + save=False, + ) + jv1.company = company + jv1.save() + + self.assertRaises(frappe.ValidationError, jv1.submit) + + def test_closing_balance_with_dimensions(self): + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") + frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'") + + company = create_company() + cost_center1 = create_cost_center("Test Cost Center 1") + cost_center2 = create_cost_center("Test Cost Center 2") + + jv1 = make_journal_entry( + posting_date="2021-03-15", + amount=400, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center1, + save=False, + ) + jv1.company = company + jv1.save() + jv1.submit() + + jv2 = make_journal_entry( + posting_date="2021-03-15", + amount=200, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center2, + save=False, + ) + jv2.company = company + jv2.save() + jv2.submit() + + pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31") + + closing_balance = frappe.db.get_value( + "Account Closing Balance", + { + "account": "Sales - TPC", + "cost_center": cost_center1, + "period_closing_voucher": pcv1.name, + "is_period_closing_voucher_entry": 0, + }, + ["credit", "credit_in_account_currency"], + as_dict=1, + ) + + self.assertEqual(closing_balance.credit, 400) + self.assertEqual(closing_balance.credit_in_account_currency, 400) + + jv3 = make_journal_entry( + posting_date="2022-03-15", + amount=300, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center2, + save=False, + ) + + jv3.company = company + jv3.save() + jv3.submit() + + pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31") + + cc1_closing_balance = frappe.db.get_value( + "Account Closing Balance", + { + "account": "Sales - TPC", + "cost_center": cost_center1, + "period_closing_voucher": pcv2.name, + "is_period_closing_voucher_entry": 0, + }, + ["credit", "credit_in_account_currency"], + as_dict=1, + ) + + cc2_closing_balance = frappe.db.get_value( + "Account Closing Balance", + { + "account": "Sales - TPC", + "cost_center": cost_center2, + "period_closing_voucher": pcv2.name, + "is_period_closing_voucher_entry": 0, + }, + ["credit", "credit_in_account_currency"], + as_dict=1, + ) + + self.assertEqual(cc1_closing_balance.credit, 400) + self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400) + self.assertEqual(cc2_closing_balance.credit, 500) + self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500) + + def make_period_closing_voucher(self, posting_date=None, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") pcv = frappe.get_doc( { "doctype": "Period Closing Voucher", - "transaction_date": today(), - "posting_date": today(), + "transaction_date": posting_date or today(), + "posting_date": posting_date or today(), "company": "Test PCV Company", "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], "cost_center": cost_center, diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 41fdb6a97f8..6b2546e8202 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -300,6 +300,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): 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) + if gl_map[0]["voucher_type"] != "Period Closing Voucher": + validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"]) for entry in gl_map: make_entry(entry, adv_adj, update_outstanding, from_repost) @@ -519,6 +522,9 @@ def make_reverse_gl_entries( ) validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + + is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) + validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: @@ -566,6 +572,28 @@ def check_freezing_date(posting_date, adv_adj=False): ) +def validate_against_pcv(is_opening, posting_date, company): + if is_opening and frappe.db.exists( + "Period Closing Voucher", {"docstatus": 1, "company": company} + ): + frappe.throw( + _("Opening Entry can not be created after Period Closing Voucher is created."), + title=_("Invalid Opening Entry"), + ) + + last_pcv_date = frappe.db.get_value( + "Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)" + ) + + if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date): + message = _("Books have been closed till the period ending on {0}").format( + formatdate(last_pcv_date) + ) + message += "
" + message += _("You cannot create/amend any accounting entries till this date.") + frappe.throw(message, title=_("Period Closed")) + + def set_as_cancel(voucher_type, voucher_no): """ Set is_cancelled=1 in all original gl entries for the voucher diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 07552e311c0..b225aac7b56 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -25,6 +25,8 @@ def execute(filters=None): company=filters.company, ) + filters.period_start_date = period_list[0]["year_start_date"] + currency = filters.presentation_currency or frappe.get_cached_value( "Company", filters.company, "default_currency" ) @@ -96,7 +98,7 @@ def execute(filters=None): chart = get_chart_data(filters, columns, asset, liability, equity) report_summary = get_report_summary( - period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters + period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) return columns, data, message, chart, report_summary @@ -174,7 +176,6 @@ def get_report_summary( liability, equity, provisional_profit_loss, - total_credit, currency, filters, consolidated=False, diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 7b60800ee2a..33da6ffe78a 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -118,7 +118,6 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): liability, equity, provisional_profit_loss, - total_credit, company_currency, filters, True, diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 8c6fe43a93d..debe655f814 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -418,46 +418,47 @@ def set_gl_entries_by_account( ignore_closing_entries=False, ): """Returns a dict like { "account": [gl entries], ... }""" + gl_entries = [] - additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) - - accounts = frappe.db.sql_list( - """select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", - (root_lft, root_rgt, company), + accounts_list = frappe.db.get_all( + "Account", + filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)}, + pluck="name", ) - if accounts: - additional_conditions += " and account in ({})".format( - ", ".join(frappe.db.escape(d) for d in accounts) - ) + ignore_opening_entries = False + if accounts_list: + # For balance sheet + if not from_date: + from_date = filters["period_start_date"] + last_period_closing_voucher = frappe.db.get_all( + "Period Closing Voucher", + filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)}, + fields=["posting_date", "name"], + order_by="posting_date desc", + limit=1, + ) + if last_period_closing_voucher: + gl_entries += get_accounting_entries( + "Account Closing Balance", + from_date, + to_date, + accounts_list, + filters, + ignore_closing_entries, + last_period_closing_voucher[0].name, + ) + from_date = add_days(last_period_closing_voucher[0].posting_date, 1) + ignore_opening_entries = True - gl_filters = { - "company": company, - "from_date": from_date, - "to_date": to_date, - "finance_book": cstr(filters.get("finance_book")), - } - - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.get_cached_value("Company", company, "default_finance_book") - - for key, value in filters.items(): - if value: - gl_filters.update({key: value}) - - gl_entries = frappe.db.sql( - """ - select posting_date, account, debit, credit, is_opening, fiscal_year, - debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` - where company=%(company)s - {additional_conditions} - and posting_date <= %(to_date)s - and is_cancelled = 0""".format( - additional_conditions=additional_conditions - ), - gl_filters, - as_dict=True, + gl_entries += get_accounting_entries( + "GL Entry", + from_date, + to_date, + accounts_list, + filters, + ignore_closing_entries, + ignore_opening_entries=ignore_opening_entries, ) if filters and filters.get("presentation_currency"): @@ -469,34 +470,82 @@ def set_gl_entries_by_account( return gl_entries_by_account -def get_additional_conditions(from_date, ignore_closing_entries, filters): - additional_conditions = [] +def get_accounting_entries( + doctype, + from_date, + to_date, + accounts, + filters, + ignore_closing_entries, + period_closing_voucher=None, + ignore_opening_entries=False, +): + gl_entry = frappe.qb.DocType(doctype) + query = ( + frappe.qb.from_(gl_entry) + .select( + gl_entry.account, + gl_entry.debit, + gl_entry.credit, + gl_entry.debit_in_account_currency, + gl_entry.credit_in_account_currency, + gl_entry.account_currency, + ) + .where(gl_entry.company == filters.company) + ) + if doctype == "GL Entry": + query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year) + query = query.where(gl_entry.is_cancelled == 0) + query = query.where(gl_entry.posting_date <= to_date) + + if ignore_opening_entries: + query = query.where(gl_entry.is_opening == "No") + else: + query = query.select(gl_entry.closing_date.as_("posting_date")) + query = query.where(gl_entry.period_closing_voucher == period_closing_voucher) + + query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters) + query = query.where(gl_entry.account.isin(accounts)) + + entries = query.run(as_dict=True) + + return entries + + +def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters): + gl_entry = frappe.qb.DocType(doctype) accounting_dimensions = get_accounting_dimensions(as_list=False) if ignore_closing_entries: - additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'") + if doctype == "GL Entry": + query = query.where(gl_entry.voucher_type != "Period Closing Voucher") + else: + query = query.where(gl_entry.is_period_closing_voucher_entry == 0) - if from_date: - additional_conditions.append("posting_date >= %(from_date)s") + if from_date and doctype == "GL Entry": + query = query.where(gl_entry.posting_date >= from_date) if filters: if filters.get("project"): if not isinstance(filters.get("project"), list): filters.project = frappe.parse_json(filters.get("project")) - additional_conditions.append("project in %(project)s") + query = query.where(gl_entry.project.isin(filters.project)) if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) - additional_conditions.append("cost_center in %(cost_center)s") + query = query.where(gl_entry.cost_center.isin(filters.cost_center)) if filters.get("include_default_book_entries"): - additional_conditions.append( - "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + query = query.where( + (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""])) + | (gl_entry.finance_book.isnull()) ) else: - additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") + query = query.where( + (gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull()) + ) if accounting_dimensions: for dimension in accounting_dimensions: @@ -505,11 +554,10 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): filters[dimension.fieldname] = get_dimension_with_children( dimension.document_type, filters.get(dimension.fieldname) ) - additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) - else: - additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) - return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + query = query.where(gl_entry[dimension.fieldname].isin(filters[dimension.fieldname])) + + return query def get_cost_centers_with_children(cost_centers): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index bc334c7060b..53611ab9196 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.utils import cstr, flt, formatdate, getdate +from frappe.query_builder.functions import Sum +from frappe.utils import add_days, cstr, flt, formatdate, getdate import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -137,45 +138,125 @@ def get_opening_balances(filters): def get_rootwise_opening_balances(filters, report_type): - additional_conditions = "" - if not filters.show_unclosed_fy_pl_balances: - additional_conditions = ( - " and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else "" - ) + gle = [] - if not flt(filters.with_period_closing_entry): - additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'" - - if filters.cost_center: - lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) - additional_conditions += """ and cost_center in (select name from `tabCost Center` - where lft >= %s and rgt <= %s)""" % ( - lft, - rgt, - ) - - if filters.project: - additional_conditions += " and project = %(project)s" - - if filters.get("include_default_book_entries"): - additional_conditions += ( - " AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" - ) - else: - additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" + last_period_closing_voucher = frappe.db.get_all( + "Period Closing Voucher", + filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)}, + fields=["posting_date", "name"], + order_by="posting_date desc", + limit=1, + ) accounting_dimensions = get_accounting_dimensions(as_list=False) - query_filters = { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "report_type": report_type, - "year_start_date": filters.year_start_date, - "project": filters.project, - "finance_book": filters.finance_book, - "company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"), - } + if last_period_closing_voucher: + gle = get_opening_balance( + "Account Closing Balance", + filters, + report_type, + accounting_dimensions, + period_closing_voucher=last_period_closing_voucher[0].name, + ) + if getdate(last_period_closing_voucher[0].posting_date) < getdate( + add_days(filters.from_date, -1) + ): + start_date = add_days(last_period_closing_voucher[0].posting_date, 1) + gle += get_opening_balance( + "GL Entry", filters, report_type, accounting_dimensions, start_date=start_date + ) + else: + gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions) + + opening = frappe._dict() + for d in gle: + opening.setdefault( + d.account, + { + "account": d.account, + "opening_debit": 0.0, + "opening_credit": 0.0, + }, + ) + opening[d.account]["opening_debit"] += flt(d.opening_debit) + opening[d.account]["opening_credit"] += flt(d.opening_credit) + + return opening + + +def get_opening_balance( + doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None +): + closing_balance = frappe.qb.DocType(doctype) + account = frappe.qb.DocType("Account") + + opening_balance = ( + frappe.qb.from_(closing_balance) + .select( + closing_balance.account, + Sum(closing_balance.debit).as_("opening_debit"), + Sum(closing_balance.credit).as_("opening_credit"), + ) + .where( + (closing_balance.company == filters.company) + & ( + closing_balance.account.isin( + frappe.qb.from_(account).select("name").where(account.report_type == report_type) + ) + ) + ) + .groupby(closing_balance.account) + ) + + if period_closing_voucher: + opening_balance = opening_balance.where( + closing_balance.period_closing_voucher == period_closing_voucher + ) + else: + if start_date: + opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) + opening_balance = opening_balance.where(closing_balance.is_opening == "No") + opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date) + + if ( + not filters.show_unclosed_fy_pl_balances + and report_type == "Profit and Loss" + and doctype == "GL Entry" + ): + opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date) + + if not flt(filters.with_period_closing_entry): + if doctype == "Account Closing Balance": + opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0) + else: + opening_balance = opening_balance.where( + closing_balance.voucher_type != "Period Closing Voucher" + ) + + if filters.cost_center: + lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) + cost_center = frappe.qb.DocType("Cost Center") + opening_balance = opening_balance.where( + closing_balance.cost_center.in_( + frappe.qb.from_(cost_center) + .select("name") + .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) + ) + ) + + if filters.project: + opening_balance = opening_balance.where(closing_balance.project == filters.project) + + if filters.get("include_default_book_entries"): + opening_balance = opening_balance.where( + (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""])) + | (closing_balance.finance_book.isnull()) + ) + else: + opening_balance = opening_balance.where( + (closing_balance.finance_book.isin([cstr(filters.finance_book), ""])) + | (closing_balance.finance_book.isnull()) + ) if accounting_dimensions: for dimension in accounting_dimensions: @@ -184,35 +265,17 @@ def get_rootwise_opening_balances(filters, report_type): filters[dimension.fieldname] = get_dimension_with_children( dimension.document_type, filters.get(dimension.fieldname) ) - additional_conditions += " and {0} in %({0})s".format(dimension.fieldname) + opening_balance = opening_balance.where( + closing_balance[dimension.fieldname].isin(filters[dimension.fieldname]) + ) else: - additional_conditions += " and {0} in %({0})s".format(dimension.fieldname) + opening_balance = opening_balance.where( + closing_balance[dimension.fieldname].isin(filters[dimension.fieldname]) + ) - query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)}) + gle = opening_balance.run(as_dict=1) - gle = frappe.db.sql( - """ - select - account, sum(debit) as opening_debit, sum(credit) as opening_credit - from `tabGL Entry` - where - company=%(company)s - {additional_conditions} - and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) - and account in (select name from `tabAccount` where report_type=%(report_type)s) - and is_cancelled = 0 - group by account""".format( - additional_conditions=additional_conditions - ), - query_filters, - as_dict=True, - ) - - opening = frappe._dict() - for d in gle: - opening.setdefault(d.account, d) - - return opening + return gle def calculate_values(accounts, gl_entries_by_account, opening_balances): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 005a2f176c4..92906c189a3 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -51,13 +51,25 @@ GL_REPOSTING_CHUNK = 100 @frappe.whitelist() def get_fiscal_year( - date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False + date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False ): - return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0] + fiscal_years = get_fiscal_years( + date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean + ) + if boolean: + return fiscal_years + else: + return fiscal_years[0] def get_fiscal_years( - transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False + transaction_date=None, + fiscal_year=None, + label="Date", + verbose=1, + company=None, + as_dict=False, + boolean=False, ): fiscal_years = frappe.cache().hget("fiscal_years", company) or [] @@ -121,8 +133,12 @@ def get_fiscal_years( if company: error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) + if boolean: + return False + if verbose == 1: frappe.msgprint(error_msg) + raise FiscalYearError(error_msg) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index dbfbcc9b385..ae4ff34c4bb 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -510,6 +510,7 @@ accounting_dimension_doctypes = [ "Subcontracting Order Item", "Subcontracting Receipt", "Subcontracting Receipt Item", + "Account Closing Balance", ] # get matching queries for Bank Reconciliation diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e2cecd98ea..0ef51a951d6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,6 +325,8 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch +erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance +erpnext.patches.v14_0.update_closing_balances # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py new file mode 100644 index 00000000000..43ad0d7cfab --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py @@ -0,0 +1,31 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + doctype = "Account Closing Balance" + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py new file mode 100644 index 00000000000..40a18516cb7 --- /dev/null +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -0,0 +1,33 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe + +from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( + make_closing_entries, +) +from erpnext.accounts.utils import get_fiscal_year + + +def execute(): + company_wise_order = {} + get_opening_entries = True + for pcv in frappe.db.get_all( + "Period Closing Voucher", + fields=["company", "posting_date", "name"], + filters={"docstatus": 1}, + order_by="posting_date", + ): + + company_wise_order.setdefault(pcv.company, []) + if pcv.posting_date not in company_wise_order[pcv.company]: + pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) + pcv_doc.year_start_date = get_fiscal_year( + pcv.posting_date, pcv.fiscal_year, company=pcv.company + )[1] + gl_entries = pcv_doc.get_gl_entries() + closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_entries) + make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) + company_wise_order[pcv.company].append(pcv.posting_date) + get_opening_entries = False