diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index 378a687378..a28008e9b6 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -1,71 +1,6 @@ // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.provide("erpnext.balance_sheet"); +frappe.require("assets/erpnext/js/financial_statements.js"); -erpnext.balance_sheet = frappe.query_reports["Balance Sheet"] = { - "filters": [ - { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("company"), - "reqd": 1 - }, - { - "fieldname":"fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), - "reqd": 1 - }, - { - "fieldname": "periodicity", - "label": __("Periodicity"), - "fieldtype": "Select", - "options": "Yearly\nQuarterly\nMonthly", - "default": "Yearly", - "reqd": 1 - }, - { - "fieldname": "depth", - "label": __("Depth"), - "fieldtype": "Select", - "options": "3\n4\n5", - "default": "3" - } - ], - "formatter": function(row, cell, value, columnDef, dataContext) { - if (columnDef.df.fieldname=="account") { - var link = $("") - .text(dataContext.account_name) - .attr("onclick", 'erpnext.balance_sheet.open_general_ledger("' + dataContext.account + '")'); - - var span = $("") - .css("padding-left", (cint(dataContext.indent) * 21) + "px") - .append(link); - - value = span.wrap("

").parent().html(); - - } else { - value = frappe.query_reports["Balance Sheet"].default_formatter(row, cell, value, columnDef, dataContext); - } - - if (!dataContext.parent_account) { - value = $(value).css("font-weight", "bold").wrap("

").parent().html(); - } - - return value; - }, - "open_general_ledger": function(account) { - if (!account) return; - - frappe.route_options = { - "account": account, - "company": frappe.query_report.filters_by_name.company.get_value() - }; - frappe.set_route("query-report", "General Ledger"); - } -} +frappe.query_reports["Balance Sheet"] = erpnext.financial_statements; diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 3bf424c89b..dd6abfd01c 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -2,188 +2,22 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import babel.dates import frappe -from frappe.utils import (cstr, flt, cint, - getdate, get_first_day, get_last_day, add_months, add_days, now_datetime) -from frappe import _ +from erpnext.accounts.report.financial_statements import (process_filters, get_period_list, get_columns, get_data) + +print_path = "accounts/report/financial_statements.html" def execute(filters=None): - company = filters.company - fiscal_year = filters.fiscal_year - depth = cint(filters.depth) or 3 - start_date, end_date = frappe.db.get_value("Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]) - period_list = get_period_list(start_date, end_date, filters.get("periodicity") or "Yearly", fiscal_year) - - out = [] - for (root_type, balance_must_be) in (("Asset", "Debit"), ("Liability", "Credit"), ("Equity", "Credit")): - data = [] - accounts, account_gl_entries = get_accounts_and_gl_entries(root_type, company, end_date) - if accounts: - accounts, accounts_map = filter_accounts(accounts, depth=depth) - - for d in accounts: - for account_name in ([d.name] + (d.invisible_children or [])): - for each in account_gl_entries.get(account_name, []): - for period_start_date, period_end_date, period_key, period_label in period_list: - each.posting_date = getdate(each.posting_date) - - # check if posting date is within the period - if ((not period_start_date or (each.posting_date >= period_start_date)) - and (each.posting_date <= period_end_date)): - - d[period_key] = d.get(period_key, 0.0) + flt(each.debit) - flt(each.credit) - - for d in reversed(accounts): - if d.parent_account: - for period_start_date, period_end_date, period_key, period_label in period_list: - accounts_map[d.parent_account][period_key] = accounts_map[d.parent_account].get(period_key, 0.0) + d.get(period_key, 0.0) - - for i, d in enumerate(accounts): - has_value = False - row = {"account_name": d["account_name"], "account": d["name"], "indent": d["indent"], "parent_account": d["parent_account"]} - for period_start_date, period_end_date, period_key, period_label in period_list: - if d.get(period_key): - d[period_key] *= (1 if balance_must_be=="Debit" else -1) - - row[period_key] = d.get(period_key, 0.0) - if row[period_key]: - has_value = True - - if has_value: - data.append(row) - - if data: - row = {"account_name": _("Total ({0})").format(balance_must_be), "account": None} - for period_start_date, period_end_date, period_key, period_label in period_list: - if period_key in data[0]: - row[period_key] = data[0].get(period_key, 0.0) - data[0][period_key] = "" - - data.append(row) - - # blank row after Total - data.append({}) - - out.extend(data) - - columns = [{"fieldname": "account", "label": _("Account"), "fieldtype": "Link", "options": "Account", "width": 300}] - for period_start_date, period_end_date, period_key, period_label in period_list: - columns.append({"fieldname": period_key, "label": period_label, "fieldtype": "Currency", "width": 150}) - - return columns, out - -def get_accounts_and_gl_entries(root_type, company, end_date): - # root lft, rgt - root_account = frappe.db.sql("""select lft, rgt from `tabAccount` - where company=%s and root_type=%s order by lft limit 1""", - (company, root_type), as_dict=True) - - if not root_account: - return None, None - - lft, rgt = root_account[0].lft, root_account[0].rgt - - accounts = frappe.db.sql("""select * from `tabAccount` - where company=%(company)s and lft >= %(lft)s and rgt <= %(rgt)s order by lft""", - { "company": company, "lft": lft, "rgt": rgt }, as_dict=True) - - gl_entries = frappe.db.sql("""select * from `tabGL Entry` - where company=%(company)s - and posting_date <= %(end_date)s - and account in (select name from `tabAccount` - where lft >= %(lft)s and rgt <= %(rgt)s)""", - { - "company": company, - "end_date": end_date, - "lft": lft, - "rgt": rgt - }, - as_dict=True) - - account_gl_entries = {} - for entry in gl_entries: - account_gl_entries.setdefault(entry.account, []).append(entry) - - return accounts, account_gl_entries - -def filter_accounts(accounts, depth): - parent_children_map = {} - accounts_map = {} - for d in accounts: - accounts_map[d.name] = d - parent_children_map.setdefault(d.parent_account or None, []).append(d) + process_filters(filters) + period_list = get_period_list(filters.fiscal_year, filters.periodicity, from_beginning=True) data = [] - def add_to_data(parent, level): - if level < depth: - for child in (parent_children_map.get(parent) or []): - child.indent = level - data.append(child) - add_to_data(child.name, level + 1) + for (root_type, balance_must_be) in (("Asset", "Debit"), ("Liability", "Credit"), ("Equity", "Credit")): + result = get_data(filters.company, root_type, balance_must_be, period_list, filters.depth) + data.extend(result or []) - else: - # include all children at level lower than the depth - parent_account = accounts_map[parent] - parent_account["invisible_children"] = [] - for d in accounts: - if d.lft > parent_account.lft and d.rgt < parent_account.rgt: - parent_account["invisible_children"].append(d.name) + columns = get_columns(period_list) - add_to_data(None, 0) + return columns, data - return data, accounts_map -def get_period_list(start_date, end_date, periodicity, fiscal_year): - """Get a list of tuples that represents (period_start_date, period_end_date, period_key) - Periodicity can be (Yearly, Quarterly, Monthly)""" - - start_date = getdate(start_date) - end_date = getdate(end_date) - today = now_datetime().date() - - if periodicity == "Yearly": - period_list = [(None, end_date, fiscal_year, fiscal_year)] - else: - months_to_add = { - "Half-yearly": 6, - "Quarterly": 3, - "Monthly": 1 - }[periodicity] - - period_list = [] - - # start with first day, so as to avoid year start dates like 2-April if every they occur - next_date = get_first_day(start_date) - - for i in xrange(12 / months_to_add): - next_date = add_months(next_date, months_to_add) - - if next_date == get_first_day(next_date): - # if first day, get the last day of previous month - next_date = add_days(next_date, -1) - else: - # get the last day of the month - next_date = get_last_day(next_date) - - # checking in the middle of the fiscal year? don't show future periods - if next_date > today: - break - - elif next_date <= end_date: - key = next_date.strftime("%b_%Y").lower() - label = babel.dates.format_date(next_date, "MMM YYYY", locale=(frappe.local.lang or "").replace("-", "_")) - period_list.append((None, next_date, key, label)) - - # if it ends before a full year - if next_date == end_date: - break - - else: - # if it ends before a full year - key = end_date.strftime("%b_%Y").lower() - label = babel.dates.format_date(end_date, "MMM YYYY", locale=(frappe.local.lang or "").replace("-", "_")) - period_list.append((None, end_date, key, label)) - break - - return period_list diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.html b/erpnext/accounts/report/financial_statements.html similarity index 56% rename from erpnext/accounts/report/balance_sheet/balance_sheet.html rename to erpnext/accounts/report/financial_statements.html index a6a33f594e..403e67e5bb 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.html +++ b/erpnext/accounts/report/financial_statements.html @@ -1,20 +1,27 @@ +{% + if (report.columns.length > 6) { + frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); + } +%} + -

{%= __("Balance Sheet") %}

+

{%= __(report.report_name) %}

+

{%= filters.company %}

{%= filters.fiscal_year %}


- + {% for(var i=2, l=report.columns.length; i{%= report.columns[i].label %} {% } %} @@ -24,12 +31,12 @@ {% for(var j=0, k=data.length; j {% for(var i=2, l=report.columns.length; i diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py new file mode 100644 index 0000000000..3490146eec --- /dev/null +++ b/erpnext/accounts/report/financial_statements.py @@ -0,0 +1,252 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, _dict +from frappe.utils import (cstr, flt, cint, + getdate, get_first_day, get_last_day, add_months, add_days, now_datetime, localize_date) + +def process_filters(filters): + filters.depth = cint(filters.depth) or 3 + if not filters.periodicity: + filters.periodicity = "Yearly" + +def get_period_list(fiscal_year, periodicity, from_beginning=False): + """Get a list of dict {"to_date": to_date, "key": key, "label": label} + Periodicity can be (Yearly, Quarterly, Monthly)""" + + start_date, end_date = frappe.db.get_value("Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]) + start_date = getdate(start_date) + end_date = getdate(end_date) + today = now_datetime().date() + + if periodicity == "Yearly": + period_list = [_dict({"to_date": end_date, "key": fiscal_year, "label": fiscal_year})] + else: + months_to_add = { + "Half-yearly": 6, + "Quarterly": 3, + "Monthly": 1 + }[periodicity] + + period_list = [] + + # start with first day, so as to avoid year to_dates like 2-April if ever they occur + to_date = get_first_day(start_date) + + for i in xrange(12 / months_to_add): + to_date = add_months(to_date, months_to_add) + + if to_date == get_first_day(to_date): + # if to_date is the first day, get the last day of previous month + to_date = add_days(to_date, -1) + else: + # to_date should be the last day of the new to_date's month + to_date = get_last_day(to_date) + + if to_date > today: + # checking in the middle of the currenct fiscal year? don't show future periods + key = today.strftime("%b_%Y").lower() + label = localize_date(today, "MMM YYYY") + period_list.append(_dict({"to_date": today, "key": key, "label": label})) + break + + elif to_date <= end_date: + # the normal case + key = to_date.strftime("%b_%Y").lower() + label = localize_date(to_date, "MMM YYYY") + period_list.append(_dict({"to_date": to_date, "key": key, "label": label})) + + # if it ends before a full year + if to_date == end_date: + break + + else: + # if a fiscal year ends before a 12 month period + key = end_date.strftime("%b_%Y").lower() + label = localize_date(end_date, "MMM YYYY") + period_list.append(_dict({"to_date": end_date, "key": key, "label": label})) + break + + # common processing + for opts in period_list: + opts["key"] = opts["key"].replace(" ", "_").replace("-", "_") + + if from_beginning: + # set start date as None for all fiscal periods, used in case of Balance Sheet + opts["from_date"] = None + else: + opts["from_date"] = start_date + + return period_list + +def get_data(company, root_type, balance_must_be, period_list, depth, ignore_opening_and_closing_entries=False): + accounts = get_accounts(company, root_type) + if not accounts: + return None + + accounts, accounts_by_name = filter_accounts(accounts, depth) + gl_entries_by_account = get_gl_entries(company, root_type, period_list[0]["from_date"], period_list[-1]["to_date"], + accounts[0].lft, accounts[0].rgt, ignore_opening_and_closing_entries=ignore_opening_and_closing_entries) + + calculate_values(accounts, gl_entries_by_account, period_list) + accumulate_values_into_parents(accounts, accounts_by_name, period_list) + out = prepare_data(accounts, balance_must_be, period_list) + + if out: + add_total_row(out, balance_must_be, period_list) + + return out + +def calculate_values(accounts, gl_entries_by_account, period_list): + for d in accounts: + for name in ([d.name] + (d.collapsed_children or [])): + for entry in gl_entries_by_account.get(name, []): + for period in period_list: + entry.posting_date = getdate(entry.posting_date) + + # check if posting date is within the period + if entry.posting_date <= period.to_date: + d[period.key] = d.get(period.key, 0.0) + flt(entry.debit) - flt(entry.credit) + + +def accumulate_values_into_parents(accounts, accounts_by_name, period_list): + """accumulate children's values in parent accounts""" + for d in reversed(accounts): + if d.parent_account: + for period in period_list: + accounts_by_name[d.parent_account][period.key] = accounts_by_name[d.parent_account].get(period.key, 0.0) + \ + d.get(period.key, 0.0) + +def prepare_data(accounts, balance_must_be, period_list): + out = [] + for d in accounts: + # add to output + has_value = False + row = { + "account_name": d.account_name, + "account": d.name, + "parent_account": d.parent_account, + "indent": flt(d.indent) + } + for period in period_list: + if d.get(period.key): + # change sign based on Debit or Credit, since calculation is done using (debit - credit) + d[period.key] *= (1 if balance_must_be=="Debit" else -1) + has_value = True + + row[period.key] = flt(d.get(period.key, 0.0), 3) + + if has_value: + out.append(row) + + return out + +def add_total_row(out, balance_must_be, period_list): + row = { + "account_name": _("Total ({0})").format(balance_must_be), + "account": None + } + for period in period_list: + row[period.key] = out[0].get(period.key, 0.0) + out[0][period.key] = "" + + out.append(row) + + # blank row after Total + out.append({}) + +def get_accounts(company, root_type): + # root lft, rgt + root_account = frappe.db.sql("""select lft, rgt from `tabAccount` + where company=%s and root_type=%s order by lft limit 1""", + (company, root_type), as_dict=True) + + if not root_account: + return None + + lft, rgt = root_account[0].lft, root_account[0].rgt + + accounts = frappe.db.sql("""select * from `tabAccount` + where company=%(company)s and lft >= %(lft)s and rgt <= %(rgt)s order by lft""", + { "company": company, "lft": lft, "rgt": rgt }, as_dict=True) + + return accounts + +def filter_accounts(accounts, depth): + parent_children_map = {} + accounts_by_name = {} + for d in accounts: + accounts_by_name[d.name] = d + parent_children_map.setdefault(d.parent_account or None, []).append(d) + + filtered_accounts = [] + def add_to_list(parent, level): + if level < depth: + for child in (parent_children_map.get(parent) or []): + child.indent = level + filtered_accounts.append(child) + add_to_list(child.name, level + 1) + + else: + # include all children at level lower than the depth + parent_account = accounts_by_name[parent] + parent_account["collapsed_children"] = [] + for d in accounts: + if d.lft > parent_account.lft and d.rgt < parent_account.rgt: + parent_account["collapsed_children"].append(d.name) + + add_to_list(None, 0) + + return filtered_accounts, accounts_by_name + +def get_gl_entries(company, root_type, from_date, to_date, root_lft, root_rgt, ignore_opening_and_closing_entries=False): + """Returns a dict like { "account": [gl entries], ... }""" + additional_conditions = [] + + if ignore_opening_and_closing_entries: + additional_conditions.append("and ifnull(is_opening, 'No')='No' and ifnull(voucher_type, '')!='Period Closing Voucher'") + + if from_date: + additional_conditions.append("and posting_date >= %(from_date)s") + + gl_entries = frappe.db.sql("""select * from `tabGL Entry` + where company=%(company)s + {additional_conditions} + and posting_date <= %(to_date)s + and account in (select name from `tabAccount` + where lft >= %(lft)s and rgt <= %(rgt)s) + order by account, posting_date""".format(additional_conditions="\n".join(additional_conditions)), + { + "company": company, + "from_date": from_date, + "to_date": to_date, + "lft": root_lft, + "rgt": root_rgt + }, + as_dict=True) + + gl_entries_by_account = {} + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) + + return gl_entries_by_account + +def get_columns(period_list): + columns = [{ + "fieldname": "account", + "label": _("Account"), + "fieldtype": "Link", + "options": "Account", + "width": 300 + }] + for period in period_list: + columns.append({ + "fieldname": period.key, + "label": period.label, + "fieldtype": "Currency", + "width": 150 + }) + + return columns diff --git a/erpnext/accounts/report/profit_and_loss_statement/__init__.py b/erpnext/accounts/report/profit_and_loss_statement/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js new file mode 100644 index 0000000000..d047fea5fb --- /dev/null +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -0,0 +1,6 @@ +// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.require("assets/erpnext/js/financial_statements.js"); + +frappe.query_reports["Profit and Loss Statement"] = erpnext.financial_statements; diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json new file mode 100644 index 0000000000..a7608d8cae --- /dev/null +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json @@ -0,0 +1,17 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2014-07-18 11:43:33.173207", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "is_standard": "Yes", + "modified": "2014-07-18 11:43:33.173207", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Profit and Loss Statement", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "Profit and Loss Statement", + "report_type": "Script Report" +} \ No newline at end of file diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py new file mode 100644 index 0000000000..556883661b --- /dev/null +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -0,0 +1,42 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt +from erpnext.accounts.report.financial_statements import (process_filters, get_period_list, get_columns, get_data) + +print_path = "accounts/report/financial_statements.html" + +def execute(filters=None): + process_filters(filters) + period_list = get_period_list(filters.fiscal_year, filters.periodicity) + + data = [] + income = get_data(filters.company, "Income", "Credit", period_list, filters.depth, + ignore_opening_and_closing_entries=True) + expense = get_data(filters.company, "Expense", "Debit", period_list, filters.depth, + ignore_opening_and_closing_entries=True) + net_total = get_net_total(income, expense, period_list) + + data.extend(income or []) + data.extend(expense or []) + if net_total: + data.append(net_total) + + columns = get_columns(period_list) + + return columns, data + +def get_net_total(income, expense, period_list): + if income and expense: + net_total = { + "account_name": _("Net Profit / Loss"), + "account": None + } + + for period in period_list: + net_total[period.key] = flt(income[-2][period.key] - expense[-2][period.key], 3) + + return net_total diff --git a/erpnext/config/accounts.py b/erpnext/config/accounts.py index 0e3e2d4ae5..722bf77f83 100644 --- a/erpnext/config/accounts.py +++ b/erpnext/config/accounts.py @@ -201,6 +201,12 @@ def get_data(): "doctype": "GL Entry", "is_query_report": True }, + { + "type": "report", + "name": "Profit and Loss Statement", + "doctype": "GL Entry", + "is_query_report": True + }, { "type": "page", "name": "financial-analytics", diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js new file mode 100644 index 0000000000..dbe41ff345 --- /dev/null +++ b/erpnext/public/js/financial_statements.js @@ -0,0 +1,68 @@ +frappe.provide("erpnext.financial_statements"); + +erpnext.financial_statements = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("company"), + "reqd": 1 + }, + { + "fieldname":"fiscal_year", + "label": __("Fiscal Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1 + }, + { + "fieldname": "periodicity", + "label": __("Periodicity"), + "fieldtype": "Select", + "options": "Yearly\nHalf-yearly\nQuarterly\nMonthly", + "default": "Yearly", + "reqd": 1 + }, + { + "fieldname": "depth", + "label": __("Depth"), + "fieldtype": "Select", + "options": "3\n4\n5", + "default": "3" + } + ], + "formatter": function(row, cell, value, columnDef, dataContext) { + if (columnDef.df.fieldname=="account") { + var link = $("") + .text(dataContext.account_name) + .attr("onclick", 'erpnext.financial_statements.open_general_ledger("' + dataContext.account + '")'); + + var span = $("") + .css("padding-left", (cint(dataContext.indent) * 21) + "px") + .append(link); + + value = span.wrap("

").parent().html(); + + } else { + value = erpnext.financial_statements.default_formatter(row, cell, value, columnDef, dataContext); + } + + if (!dataContext.parent_account) { + value = $(value).css("font-weight", "bold").wrap("

").parent().html(); + } + + return value; + }, + "open_general_ledger": function(account) { + if (!account) return; + + frappe.route_options = { + "account": account, + "company": frappe.query_report.filters_by_name.company.get_value() + }; + frappe.set_route("query-report", "General Ledger"); + } +};
- {%= row.account_name %} + {%= row.account_name %}