297 lines
7.9 KiB
Python
297 lines
7.9 KiB
Python
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||
|
# For license information, please see license.txt
|
||
|
|
||
|
import frappe
|
||
|
from frappe import _
|
||
|
from frappe.utils import add_days, flt
|
||
|
|
||
|
from erpnext.accounts.report.financial_statements import get_data, get_period_list
|
||
|
from erpnext.accounts.utils import get_balance_on, get_fiscal_year
|
||
|
|
||
|
|
||
|
def execute(filters=None):
|
||
|
filters["filter_based_on"] = "Fiscal Year"
|
||
|
columns, data = [], []
|
||
|
|
||
|
setup_filters(filters)
|
||
|
|
||
|
period_list = get_period_list(
|
||
|
filters.from_fiscal_year,
|
||
|
filters.to_fiscal_year,
|
||
|
filters.period_start_date,
|
||
|
filters.period_end_date,
|
||
|
filters.filter_based_on,
|
||
|
filters.periodicity,
|
||
|
company=filters.company,
|
||
|
)
|
||
|
|
||
|
columns, years = get_columns(period_list)
|
||
|
data = get_ratios_data(filters, period_list, years)
|
||
|
|
||
|
return columns, data
|
||
|
|
||
|
|
||
|
def setup_filters(filters):
|
||
|
if not filters.get("period_start_date"):
|
||
|
period_start_date = get_fiscal_year(fiscal_year=filters.from_fiscal_year)[1]
|
||
|
filters["period_start_date"] = period_start_date
|
||
|
|
||
|
if not filters.get("period_end_date"):
|
||
|
period_end_date = get_fiscal_year(fiscal_year=filters.to_fiscal_year)[2]
|
||
|
filters["period_end_date"] = period_end_date
|
||
|
|
||
|
|
||
|
def get_columns(period_list):
|
||
|
years = []
|
||
|
columns = [
|
||
|
{
|
||
|
"label": _("Ratios"),
|
||
|
"fieldname": "ratio",
|
||
|
"fieldtype": "Data",
|
||
|
"width": 200,
|
||
|
},
|
||
|
]
|
||
|
|
||
|
for period in period_list:
|
||
|
columns.append(
|
||
|
{
|
||
|
"fieldname": period.key,
|
||
|
"label": period.label,
|
||
|
"fieldtype": "Float",
|
||
|
"width": 150,
|
||
|
}
|
||
|
)
|
||
|
years.append(period.key)
|
||
|
|
||
|
return columns, years
|
||
|
|
||
|
|
||
|
def get_ratios_data(filters, period_list, years):
|
||
|
|
||
|
data = []
|
||
|
assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
|
||
|
|
||
|
current_asset, total_asset = {}, {}
|
||
|
current_liability, total_liability = {}, {}
|
||
|
net_sales, total_income = {}, {}
|
||
|
cogs, total_expense = {}, {}
|
||
|
quick_asset = {}
|
||
|
direct_expense = {}
|
||
|
|
||
|
for year in years:
|
||
|
total_quick_asset = 0
|
||
|
total_net_sales = 0
|
||
|
total_cogs = 0
|
||
|
|
||
|
for d in [
|
||
|
[
|
||
|
current_asset,
|
||
|
total_asset,
|
||
|
"Current Asset",
|
||
|
year,
|
||
|
assets,
|
||
|
"Asset",
|
||
|
quick_asset,
|
||
|
total_quick_asset,
|
||
|
],
|
||
|
[
|
||
|
current_liability,
|
||
|
total_liability,
|
||
|
"Current Liability",
|
||
|
year,
|
||
|
liabilities,
|
||
|
"Liability",
|
||
|
{},
|
||
|
0,
|
||
|
],
|
||
|
[cogs, total_expense, "Cost of Goods Sold", year, expense, "Expense", {}, total_cogs],
|
||
|
[direct_expense, direct_expense, "Direct Expense", year, expense, "Expense", {}, 0],
|
||
|
[net_sales, total_income, "Direct Income", year, income, "Income", {}, total_net_sales],
|
||
|
]:
|
||
|
update_balances(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7])
|
||
|
add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset)
|
||
|
add_solvency_ratios(
|
||
|
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||
|
)
|
||
|
add_turnover_ratios(
|
||
|
data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
|
||
|
)
|
||
|
|
||
|
return data
|
||
|
|
||
|
|
||
|
def get_gl_data(filters, period_list, years):
|
||
|
data = {}
|
||
|
|
||
|
for d in [
|
||
|
["Asset", "Debit"],
|
||
|
["Liability", "Credit"],
|
||
|
["Income", "Credit"],
|
||
|
["Expense", "Debit"],
|
||
|
]:
|
||
|
data[frappe.scrub(d[0])] = get_data(
|
||
|
filters.company,
|
||
|
d[0],
|
||
|
d[1],
|
||
|
period_list,
|
||
|
only_current_fiscal_year=False,
|
||
|
filters=filters,
|
||
|
)
|
||
|
|
||
|
assets, liabilities, income, expense = (
|
||
|
data.get("asset"),
|
||
|
data.get("liability"),
|
||
|
data.get("income"),
|
||
|
data.get("expense"),
|
||
|
)
|
||
|
|
||
|
return assets, liabilities, income, expense
|
||
|
|
||
|
|
||
|
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||
|
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||
|
data.append({"ratio": "Liquidity Ratios"})
|
||
|
|
||
|
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||
|
|
||
|
for d in ratio_data:
|
||
|
row = {
|
||
|
"ratio": d[0],
|
||
|
}
|
||
|
for year in years:
|
||
|
row[year] = calculate_ratio(d[1].get(year, 0), current_liability.get(year, 0), precision)
|
||
|
|
||
|
data.append(row)
|
||
|
|
||
|
|
||
|
def add_solvency_ratios(
|
||
|
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||
|
):
|
||
|
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||
|
data.append({"ratio": "Solvency Ratios"})
|
||
|
|
||
|
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||
|
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||
|
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||
|
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||
|
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||
|
|
||
|
for year in years:
|
||
|
profit_after_tax = total_income[year] + total_expense[year]
|
||
|
share_holder_fund = total_asset[year] - total_liability[year]
|
||
|
|
||
|
debt_equity_ratio[year] = calculate_ratio(
|
||
|
total_liability.get(year), share_holder_fund, precision
|
||
|
)
|
||
|
return_on_equity_ratio[year] = calculate_ratio(profit_after_tax, share_holder_fund, precision)
|
||
|
|
||
|
net_profit_ratio[year] = calculate_ratio(profit_after_tax, net_sales.get(year), precision)
|
||
|
gross_profit_ratio[year] = calculate_ratio(
|
||
|
net_sales.get(year, 0) - cogs.get(year, 0), net_sales.get(year), precision
|
||
|
)
|
||
|
return_on_asset_ratio[year] = calculate_ratio(profit_after_tax, total_asset.get(year), precision)
|
||
|
|
||
|
data.append(debt_equity_ratio)
|
||
|
data.append(gross_profit_ratio)
|
||
|
data.append(net_profit_ratio)
|
||
|
data.append(return_on_asset_ratio)
|
||
|
data.append(return_on_equity_ratio)
|
||
|
|
||
|
|
||
|
def add_turnover_ratios(
|
||
|
data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
|
||
|
):
|
||
|
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||
|
data.append({"ratio": "Turnover Ratios"})
|
||
|
|
||
|
avg_data = {}
|
||
|
for d in ["Receivable", "Payable", "Stock"]:
|
||
|
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
|
||
|
|
||
|
avg_debtors, avg_creditors, avg_stock = (
|
||
|
avg_data.get("receivable"),
|
||
|
avg_data.get("payable"),
|
||
|
avg_data.get("stock"),
|
||
|
)
|
||
|
|
||
|
ratio_data = [
|
||
|
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||
|
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||
|
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||
|
["Inventory Turnover Ratio", cogs, avg_stock],
|
||
|
]
|
||
|
for ratio in ratio_data:
|
||
|
row = {
|
||
|
"ratio": ratio[0],
|
||
|
}
|
||
|
for year in years:
|
||
|
row[year] = calculate_ratio(ratio[1].get(year, 0), ratio[2].get(year, 0), precision)
|
||
|
|
||
|
data.append(row)
|
||
|
|
||
|
|
||
|
def update_balances(
|
||
|
ratio_dict,
|
||
|
total_dict,
|
||
|
account_type,
|
||
|
year,
|
||
|
root_type_data,
|
||
|
root_type,
|
||
|
net_dict=None,
|
||
|
total_net=0,
|
||
|
):
|
||
|
|
||
|
for entry in root_type_data:
|
||
|
if not entry.get("parent_account") and entry.get("is_group"):
|
||
|
total_dict[year] = entry[year]
|
||
|
if account_type == "Direct Expense":
|
||
|
total_dict[year] = entry[year] * -1
|
||
|
|
||
|
if root_type in ("Asset", "Liability"):
|
||
|
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||
|
ratio_dict[year] = entry.get(year)
|
||
|
if entry.get("account_type") in ["Bank", "Cash", "Receivable"] and not entry.get("is_group"):
|
||
|
total_net += entry.get(year)
|
||
|
net_dict[year] = total_net
|
||
|
|
||
|
elif root_type == "Income":
|
||
|
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||
|
total_net += entry.get(year)
|
||
|
ratio_dict[year] = total_net
|
||
|
elif root_type == "Expense" and account_type == "Cost of Goods Sold":
|
||
|
if entry.get("account_type") == account_type:
|
||
|
total_net += entry.get(year)
|
||
|
ratio_dict[year] = total_net
|
||
|
else:
|
||
|
if entry.get("account_type") == account_type and entry.get("is_group"):
|
||
|
ratio_dict[year] = entry.get(year)
|
||
|
|
||
|
|
||
|
def avg_ratio_balance(account_type, period_list, precision, filters):
|
||
|
avg_ratio = {}
|
||
|
for period in period_list:
|
||
|
opening_date = add_days(period["from_date"], -1)
|
||
|
closing_date = period["to_date"]
|
||
|
|
||
|
closing_balance = get_balance_on(
|
||
|
date=closing_date,
|
||
|
company=filters.company,
|
||
|
account_type=account_type,
|
||
|
)
|
||
|
opening_balance = get_balance_on(
|
||
|
date=opening_date,
|
||
|
company=filters.company,
|
||
|
account_type=account_type,
|
||
|
)
|
||
|
avg_ratio[period["key"]] = flt(
|
||
|
(flt(closing_balance) + flt(opening_balance)) / 2, precision=precision
|
||
|
)
|
||
|
|
||
|
return avg_ratio
|
||
|
|
||
|
|
||
|
def calculate_ratio(value, denominator, precision):
|
||
|
if flt(denominator):
|
||
|
return flt(flt(value) / denominator, precision)
|
||
|
return 0
|