From 41236ed0e5a73ed3c767cd9812ddaf7cc6b51e90 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 12 Dec 2018 05:12:20 +0500 Subject: [PATCH] feat: Customer Ledger Summary report --- .../customer_ledger_summary/__init__.py | 0 .../customer_ledger_summary.js | 97 +++++++ .../customer_ledger_summary.json | 26 ++ .../customer_ledger_summary.py | 264 ++++++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 erpnext/accounts/report/customer_ledger_summary/__init__.py create mode 100644 erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js create mode 100644 erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.json create mode 100644 erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py diff --git a/erpnext/accounts/report/customer_ledger_summary/__init__.py b/erpnext/accounts/report/customer_ledger_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js new file mode 100644 index 0000000000..105977b6e4 --- /dev/null +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js @@ -0,0 +1,97 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Customer Ledger Summary"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1, + "width": "60px" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1, + "width": "60px" + }, + { + "fieldname":"finance_book", + "label": __("Finance Book"), + "fieldtype": "Link", + "options": "Finance Book" + }, + { + "fieldname":"party", + "label": __("Customer"), + "fieldtype": "Link", + "options": "Customer", + on_change: () => { + var customer = frappe.query_report.get_filter_value('customer'); + if (customer) { + frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "credit_limit", "payment_terms"], function(value) { + frappe.query_report.set_filter_value('tax_id', value["tax_id"]); + frappe.query_report.set_filter_value('customer_name', value["customer_name"]); + }); + } else { + frappe.query_report.set_filter_value('tax_id', ""); + frappe.query_report.set_filter_value('customer_name', ""); + } + } + }, + { + "fieldname":"customer_group", + "label": __("Customer Group"), + "fieldtype": "Link", + "options": "Customer Group" + }, + { + "fieldname":"payment_terms_template", + "label": __("Payment Terms Template"), + "fieldtype": "Link", + "options": "Payment Terms Template" + }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory" + }, + { + "fieldname":"sales_partner", + "label": __("Sales Partner"), + "fieldtype": "Link", + "options": "Sales Partner" + }, + { + "fieldname":"sales_person", + "label": __("Sales Person"), + "fieldtype": "Link", + "options": "Sales Person" + }, + { + "fieldname":"tax_id", + "label": __("Tax Id"), + "fieldtype": "Data", + "hidden": 1 + }, + { + "fieldname":"customer_name", + "label": __("Customer Name"), + "fieldtype": "Data", + "hidden": 1 + } + ] +}; diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.json b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.json new file mode 100644 index 0000000000..91e4e197d3 --- /dev/null +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 1, + "creation": "2018-12-11 00:58:19.078506", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-12-11 00:59:21.708343", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Customer Ledger Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "Customer Ledger Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py new file mode 100644 index 0000000000..0b590db49d --- /dev/null +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -0,0 +1,264 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _, scrub +from frappe.utils import getdate, nowdate, flt, cint + +class PartyLedgerSummaryReport(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.filters.from_date = getdate(self.filters.from_date or nowdate()) + self.filters.to_date = getdate(self.filters.to_date or nowdate()) + + def run(self, args): + if self.filters.from_date > self.filters.to_date: + frappe.throw(_("From Date must be before To Date")) + + self.filters.party_type = args.get("party_type") + self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + + columns = self.get_columns() + data = self.get_data() + return columns, data + + def get_columns(self): + columns = [{ + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 200 + }] + + if self.party_naming_by == "Naming Series": + columns.append({ + "label": _(self.filters.party_type + "Name"), + "fieldtype": "Data", + "fieldname": "party_name", + "width": 110 + }) + + credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" + columns += [ + { + "label": _("Opening Balance"), + "fieldname": "opening_balance", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Invoiced Amount"), + "fieldname": "invoiced_amount", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _(credit_or_debit_note), + "fieldname": "return_amount", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Write Off Amount"), + "fieldname": "write_off_amount", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Closing Balance"), + "fieldname": "closing_balance", + "fieldtype": "Currency", + "width": 120 + } + ] + + return columns + + def get_data(self): + if not self.filters.get("company"): + self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') + + credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" + invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" + reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" + + self.get_gl_entries() + self.get_return_invoices() + self.get_party_write_off_amounts() + + self.party_data = frappe._dict({}) + for gle in self.gl_entries: + self.party_data.setdefault(gle.party, frappe._dict({ + "party": gle.party, + "party_name": "", # TODO add party_name + "opening_balance": 0, + "invoiced_amount": 0, + "paid_amount": -self.party_write_off_amounts.get(gle.party, 0), + "return_amount": 0, + "write_off_amount": self.party_write_off_amounts.get(gle.party, 0), + "closing_balance": 0 + })) + + amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) + self.party_data[gle.party].closing_balance += amount + + if gle.posting_date < self.filters.from_date: + self.party_data[gle.party].opening_balance += amount + else: + if amount > 0: + self.party_data[gle.party].invoiced_amount += amount + elif gle.voucher_no in self.return_invoices: + self.party_data[gle.party].return_amount -= amount + else: + self.party_data[gle.party].paid_amount -= amount + + return [d for d in self.party_data.values() if d.opening_balance or d.invoiced_amount or d.paid_amount + or d.return_amount or d.write_off_amount or d.closing_amount] + + def get_gl_entries(self): + conditions = self.prepare_conditions() + + self.gl_entries = frappe.db.sql(""" + select + posting_date, party, voucher_type, voucher_no, against_voucher_type, against_voucher, debit, credit + from + `tabGL Entry` + where + docstatus < 2 and party_type=%(party_type)s and ifnull(party, '') != '' and posting_date <= %(to_date)s + {0} + order by posting_date""".format(conditions), self.filters, as_dict=True) + + def prepare_conditions(self): + conditions = [""] + + if self.filters.company: + conditions.append("company=%(company)s") + + self.filters.company_finance_book = erpnext.get_default_finance_book(self.filters.company) + + if not self.filters.finance_book or (self.filters.finance_book == self.filters.company_finance_book): + conditions.append("ifnull(finance_book,'') in (%(company_finance_book)s, '')") + elif self.filters.finance_book: + conditions.append("ifnull(finance_book,'') = %(finance_book)s") + + if self.filters.get("party"): + conditions.append("party=%(party)s") + + if self.filters.party_type == "Customer": + if self.filters.get("customer_group"): + lft, rgt = frappe.db.get_value("Customer Group", + self.filters.get("customer_group"), ["lft", "rgt"]) + + conditions.append("""party in (select name from tabCustomer + where exists(select name from `tabCustomer Group` where lft >= {0} and rgt <= {1} + and name=tabCustomer.customer_group))""".format(lft, rgt)) + + if self.filters.get("territory"): + lft, rgt = frappe.db.get_value("Territory", + self.filters.get("territory"), ["lft", "rgt"]) + + conditions.append("""party in (select name from tabCustomer + where exists(select name from `tabTerritory` where lft >= {0} and rgt <= {1} + and name=tabCustomer.territory))""".format(lft, rgt)) + + if self.filters.get("payment_terms_template"): + conditions.append("party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)") + + if self.filters.get("sales_partner"): + conditions.append("party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)") + + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) + + conditions.append("""party in (select parent from `tabSales Team` + where parenttype = 'Customer' and exists(select name from `tabSales Person` + where lft >= {0} and rgt <= {1} and name=`tabSales Team`.sales_person))""".format(lft, rgt)) + + if self.filters.party_type == "Supplier": + if self.filters.get("supplier_group"): + conditions.append("""party in (select name from tabSupplier + where supplier_group=%(supplier_group)s)""") + + return " and ".join(conditions) + + def get_return_invoices(self): + doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice" + self.return_invoices = [d.name for d in frappe.get_all(doctype, filters={"is_return": 1, "docstatus": 1, + "posting_date": ["between", [self.filters.from_date, self.filters.to_date]]})] + + def get_party_write_off_amounts(self): + conditions = self.prepare_conditions() + income_or_expense = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" + reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" + round_off_account = frappe.get_cached_value('Company', self.filters.company, "round_off_account") + + gl_entries = frappe.db.sql(""" + select + posting_date, account, party, voucher_type, voucher_no, debit, credit + from + `tabGL Entry` + where + docstatus < 2 + and (voucher_type, voucher_no) in ( + select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc + where acc.name = gle.account and acc.account_type = '{income_or_expense}' + and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 + ) and (voucher_type, voucher_no) in ( + select voucher_type, voucher_no from `tabGL Entry` gle + where gle.party_type=%(party_type)s and ifnull(party, '') != '' + and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions} + ) + order by posting_date + """.format(conditions=conditions, income_or_expense=income_or_expense), self.filters, as_dict=True) + + self.party_write_off_amounts = {} + write_off_voucher_entries = {} + for gle in gl_entries: + write_off_voucher_entries.setdefault((gle.voucher_type, gle.voucher_no), []) + write_off_voucher_entries[(gle.voucher_type, gle.voucher_no)].append(gle) + + for voucher, voucher_gl_entries in write_off_voucher_entries.iteritems(): + parties = {} + write_offs = {} + has_irrelevant_entry = False + + for gle in voucher_gl_entries: + if gle.account == round_off_account: + continue + elif gle.party: + parties.setdefault(gle.party, 0) + parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr) + elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense: + write_offs.setdefault(gle.account, 0) + write_offs[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) + else: + has_irrelevant_entry = True + + if parties and write_offs: + if len(parties) == 1: + for account, amount in write_offs.iteritems(): + party = parties.keys()[0] + self.party_write_off_amounts.setdefault(party, 0) + self.party_write_off_amounts[party] += amount + elif len(write_offs) == 1 and not has_irrelevant_entry: + for party, amount in parties.iteritems(): + self.party_write_off_amounts.setdefault(party, 0) + self.party_write_off_amounts[party] += amount + +def execute(filters=None): + args = { + "party_type": "Customer", + "naming_by": ["Selling Settings", "cust_master_name"], + } + return PartyLedgerSummaryReport(filters).run(args) \ No newline at end of file