diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index f6968a1c3b..09174281bf 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -104,7 +104,7 @@ def get_label(periodicity, from_date, to_date): return label -def get_data(company, root_type, balance_must_be, period_list, +def get_data(company, root_type, balance_must_be, period_list, filters=None, accumulated_values=1, only_current_fiscal_year=True, ignore_closing_entries=False, ignore_accumulated_values_for_fy=False): accounts = get_accounts(company, root_type) @@ -122,7 +122,7 @@ def get_data(company, root_type, balance_must_be, period_list, set_gl_entries_by_account(company, period_list[0]["year_start_date"] if only_current_fiscal_year else None, period_list[-1]["to_date"], - root.lft, root.rgt, + root.lft, root.rgt, filters, gl_entries_by_account, ignore_closing_entries=ignore_closing_entries) calculate_values(accounts_by_name, gl_entries_by_account, period_list, accumulated_values, ignore_accumulated_values_for_fy) @@ -288,16 +288,11 @@ def sort_root_accounts(roots): roots.sort(compare_roots) -def set_gl_entries_by_account(company, from_date, to_date, root_lft, root_rgt, gl_entries_by_account, +def set_gl_entries_by_account(company, from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, ignore_closing_entries=False): """Returns a dict like { "account": [gl entries], ... }""" - additional_conditions = [] - if ignore_closing_entries: - additional_conditions.append("and ifnull(voucher_type, '')!='Period Closing Voucher'") - - if from_date: - additional_conditions.append("and posting_date >= %(from_date)s") + additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening from `tabGL Entry` where company=%(company)s @@ -305,7 +300,7 @@ def set_gl_entries_by_account(company, from_date, to_date, root_lft, root_rgt, g 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)), + order by account, posting_date""".format(additional_conditions=additional_conditions), { "company": company, "from_date": from_date, @@ -320,6 +315,22 @@ def set_gl_entries_by_account(company, from_date, to_date, root_lft, root_rgt, g return gl_entries_by_account +def get_additional_conditions(from_date, ignore_closing_entries, filters): + additional_conditions = [] + + if ignore_closing_entries: + additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'") + + if from_date: + additional_conditions.append("posting_date >= %(from_date)s") + + if filters: + for key in ['cost_center', 'project']: + if filters.get(key): + additional_conditions.append("%s = '%s'"%(key, filters.get(key))) + + return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + def get_columns(periodicity, period_list, accumulated_values=1, company=None): columns = [{ "fieldname": "account", 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 index 0f7a6b6ef3..bcac2dffa4 100644 --- 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 @@ -5,9 +5,23 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements); - frappe.query_reports["Profit and Loss Statement"]["filters"].push({ - "fieldname": "accumulated_values", - "label": __("Accumulated Values"), - "fieldtype": "Check" - }); + frappe.query_reports["Profit and Loss Statement"]["filters"].push( + { + "fieldname":"cost_center", + "label": __("Cost Center"), + "fieldtype": "Link", + "options": "Cost Center" + }, + { + "fieldname":"project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" + }, + { + "fieldname": "accumulated_values", + "label": __("Accumulated Values"), + "fieldtype": "Check" + } + ); }); \ 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 index 3629d4de76..6b7f49041a 100644 --- 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 @@ -10,9 +10,9 @@ from erpnext.accounts.report.financial_statements import (get_period_list, get_c def execute(filters=None): period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.periodicity) - income = get_data(filters.company, "Income", "Credit", period_list, + income = get_data(filters.company, "Income", "Credit", period_list, filters = filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) - expense = get_data(filters.company, "Expense", "Debit", period_list, + expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company) diff --git a/erpnext/accounts/report/profitability_analysis/__init__.py b/erpnext/accounts/report/profitability_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js new file mode 100644 index 0000000000..48924b5d91 --- /dev/null +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -0,0 +1,106 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.require("assets/erpnext/js/financial_statements.js", function() { + frappe.query_reports["Profitability Analysis"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname": "based_on", + "label": __("Baed On"), + "fieldtype": "Select", + "options": "Cost Center\nProject", + "default": "Cost Center", + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "label": __("Fiscal Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1, + "on_change": function(query_report) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; + } + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + query_report.filters_by_name.from_date.set_input(fy.year_start_date); + query_report.filters_by_name.to_date.set_input(fy.year_end_date); + query_report.trigger_refresh(); + }); + } + }, + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.defaults.get_user_default("year_start_date"), + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.defaults.get_user_default("year_end_date"), + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" + } + ], + "formatter": function(row, cell, value, columnDef, dataContext, default_formatter) { + if (columnDef.df.fieldname=="account") { + value = dataContext.account_name; + + columnDef.df.link_onclick = + "frappe.query_reports['Profitability Analysis'].open_profit_and_loss_statement(" + JSON.stringify(dataContext) + ")"; + columnDef.df.is_tree = true; + } + + value = default_formatter(row, cell, value, columnDef, dataContext); + + if (!dataContext.parent_account && dataContext.based_on != 'project') { + var $value = $(value).css("font-weight", "bold"); + if (dataContext.warn_if_negative && dataContext[columnDef.df.fieldname] < 0) { + $value.addClass("text-danger"); + } + + value = $value.wrap("

").parent().html(); + } + + return value; + }, + "open_profit_and_loss_statement": function(data) { + if (!data.account) return; + + frappe.route_options = { + "company": frappe.query_report.filters_by_name.company.get_value(), + "from_fiscal_year": data.fiscal_year, + "to_fiscal_year": data.fiscal_year + }; + + if(data.based_on == 'cost_center'){ + frappe.route_options["cost_center"] = data.account + } else { + frappe.route_options["project"] = data.account + } + + frappe.set_route("query-report", "Profit and Loss Statement"); + }, + "tree": true, + "name_field": "account", + "parent_field": "parent_account", + "initial_depth": 3 + } +}); + diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.json b/erpnext/accounts/report/profitability_analysis/profitability_analysis.json new file mode 100644 index 0000000000..d9c00c6f41 --- /dev/null +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.json @@ -0,0 +1,18 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2016-08-24 05:47:09.931202", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2016-08-24 05:47:09.931202", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Profitability Analysis", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "Profitability Analysis", + "report_type": "Script Report" +} \ No newline at end of file diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py new file mode 100644 index 0000000000..4f9fd15f66 --- /dev/null +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -0,0 +1,188 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, getdate, formatdate, cstr +from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows +from erpnext.accounts.report.trial_balance.trial_balance import validate_filters + +value_fields = ("income", "expense", "gross_profit_loss") + +def execute(filters=None): + based_on = filters.based_on.replace(' ', '_').lower() + validate_filters(filters) + accounts = get_accounts_data(based_on, filters.company) + data = get_data(accounts, filters, based_on) + columns = get_columns(filters) + return columns, data + +def get_accounts_data(based_on, company): + if based_on == 'cost_center': + return frappe.db.sql("""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt + from `tabCost Center` where company=%s order by lft""", company, as_dict=True) + else: + return frappe.get_all('Project', fields = ["name"], filters = {'company': company}) + +def get_data(accounts, filters, based_on): + if not accounts: + return None + + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) + + gl_entries_by_account = {} + + set_gl_entries_by_account(filters.company, filters.from_date, + filters.to_date, based_on, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) + + total_row = calculate_values(accounts, gl_entries_by_account, filters) + accumulate_values_into_parents(accounts, accounts_by_name) + + data = prepare_data(accounts, filters, total_row, parent_children_map, based_on) + data = filter_out_zero_value_rows(data, parent_children_map, + show_zero_values=filters.get("show_zero_values")) + + return data + +def calculate_values(accounts, gl_entries_by_account, filters): + init = { + "income": 0.0, + "expense": 0.0, + "gross_profit_loss": 0.0 + } + + total_row = { + "cost_center": None, + "account_name": "'" + _("Total") + "'", + "warn_if_negative": True, + "income": 0.0, + "expense": 0.0, + "gross_profit_loss": 0.0 + } + + for d in accounts: + d.update(init.copy()) + + # add opening + + for entry in gl_entries_by_account.get(d.name, []): + if cstr(entry.is_opening) != "Yes": + if entry.type == 'Income': + d["income"] += flt(entry.credit) - flt(entry.debit) + if entry.type == 'Expense': + d["expense"] += flt(entry.debit) - flt(entry.credit) + + d["gross_profit_loss"] = d.get("income") - d.get("expense") + + total_row["income"] += d["income"] + total_row["expense"] += d["expense"] + + total_row["gross_profit_loss"] = total_row.get("income") - total_row.get("expense") + + return total_row + +def accumulate_values_into_parents(accounts, accounts_by_name): + for d in reversed(accounts): + if d.parent_account: + for key in value_fields: + accounts_by_name[d.parent_account][key] += d[key] + +def prepare_data(accounts, filters, total_row, parent_children_map, based_on): + data = [] + company_currency = frappe.db.get_value("Company", filters.company, "default_currency") + + for d in accounts: + has_value = False + row = { + "account_name": d.account_name or d.name, + "account": d.name, + "parent_account": d.parent_account, + "indent": d.indent, + "fiscal_year": filters.fiscal_year, + "currency": company_currency, + "based_on": based_on + } + + for key in value_fields: + row[key] = flt(d.get(key, 0.0), 3) + + if abs(row[key]) >= 0.005: + # ignore zero values + has_value = True + + row["has_value"] = has_value + data.append(row) + + data.extend([{},total_row]) + + return data + +def get_columns(filters): + return [ + { + "fieldname": "account", + "label": _(filters.based_on), + "fieldtype": "Link", + "options": filters.based_on, + "width": 300 + }, + { + "fieldname": "income", + "label": _("Income"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "expense", + "label": _("Expense"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "gross_profit_loss", + "label": _("Gross Profit / Loss"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1 + } + ] + +def set_gl_entries_by_account(company, from_date, to_date, based_on, gl_entries_by_account, + ignore_closing_entries=False): + """Returns a dict like { "account": [gl entries], ... }""" + additional_conditions = [] + + if ignore_closing_entries: + additional_conditions.append("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 posting_date, {based_on} as based_on, debit, credit, + is_opening, (select root_type from `tabAccount` where name = account) as type + from `tabGL Entry` where company=%(company)s + {additional_conditions} + and posting_date <= %(to_date)s + and {based_on} is not null + order by {based_on}, posting_date""".format(additional_conditions="\n".join(additional_conditions), based_on= based_on), + { + "company": company, + "from_date": from_date, + "to_date": to_date + }, + as_dict=True) + + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.based_on, []).append(entry) + + return gl_entries_by_account \ No newline at end of file diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index a51e9b02eb..5401902443 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -65,7 +65,7 @@ def get_data(filters): gl_entries_by_account = {} set_gl_entries_by_account(filters.company, filters.from_date, - filters.to_date, min_lft, max_rgt, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) + filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) opening_balances = get_opening_balances(filters)