diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index bc76ee0a114..6c44c07a508 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -14,9 +14,16 @@ erpnext.utils.add_dimensions("Cash Flow", 10); frappe.query_reports["Cash Flow"]["filters"].splice(8, 1); -frappe.query_reports["Cash Flow"]["filters"].push({ - fieldname: "include_default_book_entries", - label: __("Include Default FB Entries"), - fieldtype: "Check", - default: 1, -}); +frappe.query_reports["Cash Flow"]["filters"].push( + { + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, + }, + { + fieldname: "show_opening_and_closing_balance", + label: __("Show Opening and Closing Balance"), + fieldtype: "Check", + } +); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index c647c5b2e4f..0ee0cb14edb 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -2,9 +2,13 @@ # For license information, please see license.txt +from datetime import timedelta + import frappe from frappe import _ -from frappe.utils import cstr +from frappe.query_builder import DocType +from frappe.utils import cstr, flt +from pypika import Order from erpnext.accounts.report.financial_statements import ( get_columns, @@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import ( get_data, get_filtered_list_for_consolidated_report, get_period_list, + set_gl_entries_by_account, ) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_net_profit_loss, @@ -119,10 +124,20 @@ def execute(filters=None): filters, ) - add_total_row_account( + net_change_in_cash = add_total_row_account( data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters ) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True) + + if filters.show_opening_and_closing_balance: + show_opening_and_closing_balance(data, period_list, company_currency, net_change_in_cash, filters) + + columns = get_columns( + filters.periodicity, + period_list, + filters.accumulated_values, + filters.company, + True, + ) chart = get_chart_data(columns, data, company_currency) @@ -255,6 +270,137 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data, out.append(total_row) out.append({}) + return total_row + + +def show_opening_and_closing_balance(out, period_list, currency, net_change_in_cash, filters): + opening_balance = { + "section_name": "Opening", + "section": "Opening", + "currency": currency, + } + closing_balance = { + "section_name": "Closing (Opening + Total)", + "section": "Closing (Opening + Total)", + "currency": currency, + } + + opening_amount = get_opening_balance(filters.company, period_list, filters) or 0.0 + running_total = opening_amount + + for i, period in enumerate(period_list): + key = period["key"] + change = net_change_in_cash.get(key, 0.0) + + opening_balance[key] = opening_amount if i == 0 else running_total + running_total += change + closing_balance[key] = running_total + + opening_balance["total"] = opening_balance[period_list[0]["key"]] + closing_balance["total"] = closing_balance[period_list[-1]["key"]] + + out.extend([opening_balance, net_change_in_cash, closing_balance, {}]) + + +def get_opening_balance(company, period_list, filters): + from copy import deepcopy + + cash_value = {} + account_types = get_cash_flow_accounts() + net_profit_loss = 0.0 + + local_filters = deepcopy(filters) + local_filters.start_date, local_filters.end_date = get_opening_range_using_fiscal_year( + company, period_list + ) + + for section in account_types: + section_name = section.get("section_name") + cash_value.setdefault(section_name, 0.0) + + if section_name == "Operations": + net_profit_loss += get_net_income(company, period_list, local_filters) + + for account in section.get("account_types", []): + account_type = account.get("account_type") + local_filters.account_type = account_type + + amount = get_account_type_based_gl_data(company, local_filters) or 0.0 + + if account_type == "Depreciation": + cash_value[section_name] += amount * -1 + else: + cash_value[section_name] += amount + + return sum(cash_value.values()) + net_profit_loss + + +def get_net_income(company, period_list, filters): + gl_entries_by_account_for_income, gl_entries_by_account_for_expense = {}, {} + income, expense = 0.0, 0.0 + from_date, to_date = get_opening_range_using_fiscal_year(company, period_list) + + for root_type in ["Income", "Expense"]: + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): + set_gl_entries_by_account( + company, + from_date, + to_date, + filters, + gl_entries_by_account_for_income + if root_type == "Income" + else gl_entries_by_account_for_expense, + root.lft, + root.rgt, + root_type=root_type, + ignore_closing_entries=True, + ) + + for entries in gl_entries_by_account_for_income.values(): + for entry in entries: + if entry.posting_date <= to_date: + amount = (entry.debit - entry.credit) * -1 + income = flt((income + amount), 2) + + for entries in gl_entries_by_account_for_expense.values(): + for entry in entries: + if entry.posting_date <= to_date: + amount = entry.debit - entry.credit + expense = flt((expense + amount), 2) + + return income - expense + + +def get_opening_range_using_fiscal_year(company, period_list): + first_from_date = period_list[0]["from_date"] + previous_day = first_from_date - timedelta(days=1) + + # Get the earliest fiscal year for the company + + FiscalYear = DocType("Fiscal Year") + FiscalYearCompany = DocType("Fiscal Year Company") + + earliest_fy = ( + frappe.qb.from_(FiscalYear) + .join(FiscalYearCompany) + .on(FiscalYearCompany.parent == FiscalYear.name) + .select(FiscalYear.year_start_date) + .where(FiscalYearCompany.company == company) + .orderby(FiscalYear.year_start_date, order=Order.asc) + .limit(1) + ).run(as_dict=True) + + if not earliest_fy: + frappe.throw(_("Not able to find the earliest Fiscal Year for the given company.")) + + company_start_date = earliest_fy[0]["year_start_date"] + return company_start_date, previous_day + def get_report_summary(summary_data, currency): report_summary = [] @@ -276,7 +422,7 @@ def get_chart_data(columns, data, currency): for section in data if section.get("parent_section") is None and section.get("currency") ] - datasets = datasets[:-1] + datasets = datasets[:-2] chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}