Merge pull request #31107 from ruthra-kumar/refactor_ar/ap_report

refactor: Accounts Receivable/Payable Report will use Payment Ledger
This commit is contained in:
Deepesh Garg 2022-06-13 12:57:25 +05:30 committed by GitHub
commit b12b7b51ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 166 additions and 193 deletions

View File

@ -172,11 +172,6 @@ frappe.query_reports["Accounts Receivable"] = {
"label": __("Show Sales Person"), "label": __("Show Sales Person"),
"fieldtype": "Check", "fieldtype": "Check",
}, },
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{ {
"fieldname": "tax_id", "fieldname": "tax_id",
"label": __("Tax Id"), "label": __("Tax Id"),

View File

@ -5,7 +5,9 @@
from collections import OrderedDict from collections import OrderedDict
import frappe import frappe
from frappe import _, scrub from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date
from frappe.utils import cint, cstr, flt, getdate, nowdate from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -41,6 +43,8 @@ def execute(filters=None):
class ReceivablePayableReport(object): class ReceivablePayableReport(object):
def __init__(self, filters=None): def __init__(self, filters=None):
self.filters = frappe._dict(filters or {}) self.filters = frappe._dict(filters or {})
self.qb_selection_filter = []
self.ple = qb.DocType("Payment Ledger Entry")
self.filters.report_date = getdate(self.filters.report_date or nowdate()) self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = ( self.age_as_on = (
getdate(nowdate()) getdate(nowdate())
@ -78,7 +82,7 @@ class ReceivablePayableReport(object):
self.skip_total_row = 1 self.skip_total_row = 1
def get_data(self): def get_data(self):
self.get_gl_entries() self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person() self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict() self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
@ -96,25 +100,25 @@ class ReceivablePayableReport(object):
self.get_return_entries() self.get_return_entries()
self.data = [] self.data = []
for gle in self.gl_entries:
self.update_voucher_balance(gle) for ple in self.ple_entries:
self.update_voucher_balance(ple)
self.build_data() self.build_data()
def init_voucher_balance(self): def init_voucher_balance(self):
# build all keys, since we want to exclude vouchers beyond the report date # build all keys, since we want to exclude vouchers beyond the report date
for gle in self.gl_entries: for ple in self.ple_entries:
# get the balance object for voucher_type # get the balance object for voucher_type
key = (gle.voucher_type, gle.voucher_no, gle.party) key = (ple.voucher_type, ple.voucher_no, ple.party)
if not key in self.voucher_balance: if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict( self.voucher_balance[key] = frappe._dict(
voucher_type=gle.voucher_type, voucher_type=ple.voucher_type,
voucher_no=gle.voucher_no, voucher_no=ple.voucher_no,
party=gle.party, party=ple.party,
party_account=gle.account, party_account=ple.account,
posting_date=gle.posting_date, posting_date=ple.posting_date,
account_currency=gle.account_currency, account_currency=ple.account_currency,
remarks=gle.remarks if self.filters.get("show_remarks") else None,
invoiced=0.0, invoiced=0.0,
paid=0.0, paid=0.0,
credit_note=0.0, credit_note=0.0,
@ -124,23 +128,22 @@ class ReceivablePayableReport(object):
credit_note_in_account_currency=0.0, credit_note_in_account_currency=0.0,
outstanding_in_account_currency=0.0, outstanding_in_account_currency=0.0,
) )
self.get_invoices(gle)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
self.init_subtotal_row(gle.party) self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
self.init_subtotal_row("Total") self.init_subtotal_row("Total")
def get_invoices(self, gle): def get_invoices(self, ple):
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):
if gle.voucher_no in self.sales_person_records.get( if ple.voucher_no in self.sales_person_records.get(
"Sales Invoice", [] "Sales Invoice", []
) or gle.party in self.sales_person_records.get("Customer", []): ) or ple.party in self.sales_person_records.get("Customer", []):
self.invoices.add(gle.voucher_no) self.invoices.add(ple.voucher_no)
else: else:
self.invoices.add(gle.voucher_no) self.invoices.add(ple.voucher_no)
def init_subtotal_row(self, party): def init_subtotal_row(self, party):
if not self.total_row_map.get(party): if not self.total_row_map.get(party):
@ -162,39 +165,49 @@ class ReceivablePayableReport(object):
"range5", "range5",
] ]
def update_voucher_balance(self, gle): def get_voucher_balance(self, ple):
if self.filters.get("sales_person"):
if not (
ple.party in self.sales_person_records.get("Customer", [])
or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", [])
):
return
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
row = self.voucher_balance.get(key)
return row
def update_voucher_balance(self, ple):
# get the row where this balance needs to be updated # get the row where this balance needs to be updated
# if its a payment, it will return the linked invoice or will be considered as advance # if its a payment, it will return the linked invoice or will be considered as advance
row = self.get_voucher_balance(gle) row = self.get_voucher_balance(ple)
if not row: if not row:
return return
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0: amount = ple.amount
if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher: amount_in_account_currency = ple.amount_in_account_currency
# debit against sales / purchase invoice
row.paid -= gle_balance # update voucher
row.paid_in_account_currency -= gle_balance_in_account_currency if ple.amount > 0:
if (
ple.voucher_type in ["Journal Entry", "Payment Entry"]
and ple.voucher_no != ple.against_voucher_no
):
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
else: else:
# invoice row.invoiced += amount
row.invoiced += gle_balance row.invoiced_in_account_currency += amount_in_account_currency
row.invoiced_in_account_currency += gle_balance_in_account_currency
else: else:
# payment or credit note for receivables if self.is_invoice(ple):
if self.is_invoice(gle): row.credit_note -= amount
# stand alone debit / credit note row.credit_note_in_account_currency -= amount_in_account_currency
row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
else: else:
# advance / unlinked payment or other adjustment row.paid -= amount
row.paid -= gle_balance row.paid_in_account_currency -= amount_in_account_currency
row.paid_in_account_currency -= gle_balance_in_account_currency
if gle.cost_center: if ple.cost_center:
row.cost_center = str(gle.cost_center) row.cost_center = str(ple.cost_center)
def update_sub_total_row(self, row, party): def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party) total_row = self.total_row_map.get(party)
@ -210,39 +223,6 @@ class ReceivablePayableReport(object):
self.data.append({}) self.data.append({})
self.update_sub_total_row(sub_total_row, "Total") self.update_sub_total_row(sub_total_row, "Total")
def get_voucher_balance(self, gle):
if self.filters.get("sales_person"):
against_voucher = gle.against_voucher or gle.voucher_no
if not (
gle.party in self.sales_person_records.get("Customer", [])
or against_voucher in self.sales_person_records.get("Sales Invoice", [])
):
return
voucher_balance = None
if gle.against_voucher:
# find invoice
against_voucher = gle.against_voucher
# If payment is made against credit note
# and credit note is made against a Sales Invoice
# then consider the payment against original sales invoice.
if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
if gle.against_voucher in self.return_entries:
return_against = self.return_entries.get(gle.against_voucher)
if return_against:
against_voucher = return_against
voucher_balance = self.voucher_balance.get(
(gle.against_voucher_type, against_voucher, gle.party)
)
if not voucher_balance:
# no invoice, this is an invoice / stand-alone payment / credit note
voucher_balance = self.voucher_balance.get((gle.voucher_type, gle.voucher_no, gle.party))
return voucher_balance
def build_data(self): def build_data(self):
# set outstanding for all the accumulated balances # set outstanding for all the accumulated balances
# as we can use this to filter out invoices without outstanding # as we can use this to filter out invoices without outstanding
@ -260,6 +240,7 @@ class ReceivablePayableReport(object):
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
): ):
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms: if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -669,48 +650,53 @@ class ReceivablePayableReport(object):
index = 4 index = 4
row["range" + str(index + 1)] = row.outstanding row["range" + str(index + 1)] = row.outstanding
def get_gl_entries(self): def get_ple_entries(self):
# get all the GL entries filtered by the given filters # get all the GL entries filtered by the given filters
conditions, values = self.prepare_conditions() self.prepare_conditions()
order_by = self.get_order_by_condition()
if self.filters.show_future_payments: if self.filters.show_future_payments:
values.insert(2, self.filters.report_date) self.qb_selection_filter.append(
(
date_condition = """AND (posting_date <= %s self.ple.posting_date.lte(self.filters.report_date)
OR (against_voucher IS NULL AND DATE(creation) <= %s))""" | (
(self.ple.voucher_no == self.ple.against_voucher_no)
& (Date(self.ple.creation).lte(self.filters.report_date))
)
)
)
else: else:
date_condition = "AND posting_date <=%s" self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date))
if self.filters.get(scrub(self.party_type)): ple = qb.DocType("Payment Ledger Entry")
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit" query = (
else: qb.from_(ple)
select_fields = "debit, credit" .select(
ple.account,
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" ple.voucher_type,
ple.voucher_no,
remarks = ", remarks" if self.filters.get("show_remarks") else "" ple.against_voucher_type,
ple.against_voucher_no,
self.gl_entries = frappe.db.sql( ple.party_type,
""" ple.cost_center,
select ple.party,
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, ple.posting_date,
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} ple.due_date,
from ple.account_currency.as_("currency"),
`tabGL Entry` ple.amount,
where ple.amount_in_account_currency,
docstatus < 2 )
and is_cancelled = 0 .where(ple.delinked == 0)
and party_type=%s .where(Criterion.all(self.qb_selection_filter))
and (party is not null and party != '')
{2} {3} {4}""".format(
select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks
),
values,
as_dict=True,
) )
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
query = query.orderby(self.ple.posting_date, self.ple.party)
self.ple_entries = query.run(as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self): def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
@ -731,23 +717,21 @@ class ReceivablePayableReport(object):
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
def prepare_conditions(self): def prepare_conditions(self):
conditions = [""] self.qb_selection_filter = []
values = [self.party_type, self.filters.report_date]
party_type_field = scrub(self.party_type) party_type_field = scrub(self.party_type)
self.add_common_filters(conditions, values, party_type_field) self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer": if party_type_field == "customer":
self.add_customer_filters(conditions, values) self.add_customer_filters()
elif party_type_field == "supplier": elif party_type_field == "supplier":
self.add_supplier_filters(conditions, values) self.add_supplier_filters()
if self.filters.cost_center: if self.filters.cost_center:
self.get_cost_center_conditions(conditions) self.get_cost_center_conditions()
self.add_accounting_dimensions_filters(conditions, values) self.add_accounting_dimensions_filters()
return " and ".join(conditions), values
def get_cost_center_conditions(self, conditions): def get_cost_center_conditions(self, conditions):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
@ -755,32 +739,20 @@ class ReceivablePayableReport(object):
center.name center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
] ]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
cost_center_string = '", "'.join(cost_center_list) def add_common_filters(self, party_type_field):
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
def get_order_by_condition(self):
if self.filters.get("group_by_party"):
return "order by party, posting_date"
else:
return "order by posting_date, party"
def add_common_filters(self, conditions, values, party_type_field):
if self.filters.company: if self.filters.company:
conditions.append("company=%s") self.qb_selection_filter.append(self.ple.company == self.filters.company)
values.append(self.filters.company)
if self.filters.finance_book: if self.filters.finance_book:
conditions.append("ifnull(finance_book, '') in (%s, '')") self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
values.append(self.filters.finance_book)
if self.filters.get(party_type_field): if self.filters.get(party_type_field):
conditions.append("party=%s") self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
values.append(self.filters.get(party_type_field))
if self.filters.party_account: if self.filters.party_account:
conditions.append("account =%s") self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
values.append(self.filters.party_account)
else: else:
# get GL with "receivable" or "payable" account_type # get GL with "receivable" or "payable" account_type
account_type = "Receivable" if self.party_type == "Customer" else "Payable" account_type = "Receivable" if self.party_type == "Customer" else "Payable"
@ -792,46 +764,68 @@ class ReceivablePayableReport(object):
] ]
if accounts: if accounts:
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) self.qb_selection_filter.append(self.ple.account.isin(accounts))
values += accounts
def add_customer_filters(
self,
):
self.customter = qb.DocType("Customer")
def add_customer_filters(self, conditions, values):
if self.filters.get("customer_group"): if self.filters.get("customer_group"):
conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group")) self.get_hierarchical_filters("Customer Group", "customer_group")
if self.filters.get("territory"): if self.filters.get("territory"):
conditions.append(self.get_hierarchical_filters("Territory", "territory")) self.get_hierarchical_filters("Territory", "territory")
if self.filters.get("payment_terms_template"): if self.filters.get("payment_terms_template"):
conditions.append("party in (select name from tabCustomer where payment_terms=%s)") self.qb_selection_filter.append(
values.append(self.filters.get("payment_terms_template")) self.ple.party_isin(
qb.from_(self.customer).where(
self.customer.payment_terms == self.filters.get("payment_terms_template")
)
)
)
if self.filters.get("sales_partner"): if self.filters.get("sales_partner"):
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") self.qb_selection_filter.append(
values.append(self.filters.get("sales_partner")) self.ple.party_isin(
qb.from_(self.customer).where(
def add_supplier_filters(self, conditions, values): self.customer.default_sales_partner == self.filters.get("payment_terms_template")
if self.filters.get("supplier_group"): )
conditions.append( )
"""party in (select name from tabSupplier )
where supplier_group=%s)"""
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == self.filters.get("supplier_group"))
)
) )
values.append(self.filters.get("supplier_group"))
if self.filters.get("payment_terms_template"): if self.filters.get("payment_terms_template"):
conditions.append("party in (select name from tabSupplier where payment_terms=%s)") self.qb_selection_filter.append(
values.append(self.filters.get("payment_terms_template")) self.ple.party.isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.payment_terms == self.filters.get("supplier_group"))
)
)
def get_hierarchical_filters(self, doctype, key): def get_hierarchical_filters(self, doctype, key):
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
return """party in (select name from tabCustomer doc = qb.DocType(doctype)
where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt} ple = self.ple
and name=tabCustomer.{key}))""".format( customer = self.customer
doctype=doctype, lft=lft, rgt=rgt, key=key groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
) customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
def add_accounting_dimensions_filters(self, conditions, values): def add_accounting_dimensions_filters(self):
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)
if accounting_dimensions: if accounting_dimensions:
@ -841,30 +835,16 @@ class ReceivablePayableReport(object):
self.filters[dimension.fieldname] = get_dimension_with_children( self.filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, self.filters.get(dimension.fieldname) dimension.document_type, self.filters.get(dimension.fieldname)
) )
conditions.append("{0} in %s".format(dimension.fieldname)) self.qb_selection_filter.append(
values.append(tuple(self.filters.get(dimension.fieldname))) self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
)
else:
self.qb_selection_filter.append(
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
)
def get_gle_balance(self, gle): def is_invoice(self, ple):
# get the balance of the GL (debit - credit) or reverse balance based on report type if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(
self.dr_or_cr + "_in_account_currency"
) - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get(
"debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency"
)
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get("debit" if self.dr_or_cr == "credit" else "credit")
def is_invoice(self, gle):
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
return True return True
def get_party_details(self, party): def get_party_details(self, party):
@ -926,9 +906,6 @@ class ReceivablePayableReport(object):
width=180, width=180,
) )
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
self.add_column(label="Due Date", fieldtype="Date") self.add_column(label="Due Date", fieldtype="Date")
if self.party_type == "Supplier": if self.party_type == "Supplier":

View File

@ -12,6 +12,7 @@ class TestAccountsReceivable(unittest.TestCase):
def test_accounts_receivable(self): def test_accounts_receivable(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
filters = { filters = {
"company": "_Test Company 2", "company": "_Test Company 2",