refactor: AR/AP will use payment ledger
This commit is contained in:
parent
3019f93915
commit
dfe3082596
@ -5,7 +5,8 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@ -41,6 +42,8 @@ def execute(filters=None):
|
||||
class ReceivablePayableReport(object):
|
||||
def __init__(self, filters=None):
|
||||
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.age_as_on = (
|
||||
getdate(nowdate())
|
||||
@ -78,7 +81,7 @@ class ReceivablePayableReport(object):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_gl_entries()
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
@ -96,25 +99,25 @@ class ReceivablePayableReport(object):
|
||||
self.get_return_entries()
|
||||
|
||||
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()
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# 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
|
||||
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:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
party=gle.party,
|
||||
party_account=gle.account,
|
||||
posting_date=gle.posting_date,
|
||||
account_currency=gle.account_currency,
|
||||
remarks=gle.remarks if self.filters.get("show_remarks") else None,
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
@ -124,23 +127,22 @@ class ReceivablePayableReport(object):
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
self.get_invoices(gle)
|
||||
|
||||
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"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, gle):
|
||||
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
def get_invoices(self, ple):
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
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", []
|
||||
) or gle.party in self.sales_person_records.get("Customer", []):
|
||||
self.invoices.add(gle.voucher_no)
|
||||
) or ple.party in self.sales_person_records.get("Customer", []):
|
||||
self.invoices.add(ple.voucher_no)
|
||||
else:
|
||||
self.invoices.add(gle.voucher_no)
|
||||
self.invoices.add(ple.voucher_no)
|
||||
|
||||
def init_subtotal_row(self, party):
|
||||
if not self.total_row_map.get(party):
|
||||
@ -162,39 +164,49 @@ class ReceivablePayableReport(object):
|
||||
"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
|
||||
# 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:
|
||||
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:
|
||||
if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher:
|
||||
# debit against sales / purchase invoice
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
|
||||
# update voucher
|
||||
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:
|
||||
# invoice
|
||||
row.invoiced += gle_balance
|
||||
row.invoiced_in_account_currency += gle_balance_in_account_currency
|
||||
row.invoiced += amount
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
# payment or credit note for receivables
|
||||
if self.is_invoice(gle):
|
||||
# stand alone debit / credit note
|
||||
row.credit_note -= gle_balance
|
||||
row.credit_note_in_account_currency -= gle_balance_in_account_currency
|
||||
if self.is_invoice(ple):
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
# advance / unlinked payment or other adjustment
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if gle.cost_center:
|
||||
row.cost_center = str(gle.cost_center)
|
||||
if ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
@ -210,39 +222,6 @@ class ReceivablePayableReport(object):
|
||||
self.data.append({})
|
||||
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):
|
||||
# set outstanding for all the accumulated balances
|
||||
# as we can use this to filter out invoices without outstanding
|
||||
@ -260,6 +239,7 @@ class ReceivablePayableReport(object):
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
||||
):
|
||||
|
||||
# non-zero oustanding, we must consider this row
|
||||
|
||||
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
||||
@ -669,48 +649,42 @@ class ReceivablePayableReport(object):
|
||||
index = 4
|
||||
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
|
||||
|
||||
conditions, values = self.prepare_conditions()
|
||||
order_by = self.get_order_by_condition()
|
||||
self.prepare_conditions()
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
values.insert(2, self.filters.report_date)
|
||||
self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date))
|
||||
|
||||
date_condition = """AND (posting_date <= %s
|
||||
OR (against_voucher IS NULL AND DATE(creation) <= %s))"""
|
||||
else:
|
||||
date_condition = "AND posting_date <=%s"
|
||||
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
|
||||
else:
|
||||
select_fields = "debit, credit"
|
||||
|
||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
||||
|
||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||
|
||||
self.gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
docstatus < 2
|
||||
and is_cancelled = 0
|
||||
and party_type=%s
|
||||
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,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.party_type,
|
||||
ple.cost_center,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
ple.amount,
|
||||
ple.amount_in_account_currency,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
)
|
||||
|
||||
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):
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
@ -731,23 +705,21 @@ class ReceivablePayableReport(object):
|
||||
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
|
||||
|
||||
def prepare_conditions(self):
|
||||
conditions = [""]
|
||||
values = [self.party_type, self.filters.report_date]
|
||||
self.qb_selection_filter = []
|
||||
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":
|
||||
self.add_customer_filters(conditions, values)
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters(conditions, values)
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions(conditions)
|
||||
self.get_cost_center_conditions()
|
||||
|
||||
self.add_accounting_dimensions_filters(conditions, values)
|
||||
return " and ".join(conditions), values
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self, conditions):
|
||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||
@ -755,32 +727,20 @@ class ReceivablePayableReport(object):
|
||||
center.name
|
||||
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)
|
||||
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):
|
||||
def add_common_filters(self, party_type_field):
|
||||
if self.filters.company:
|
||||
conditions.append("company=%s")
|
||||
values.append(self.filters.company)
|
||||
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
||||
|
||||
if self.filters.finance_book:
|
||||
conditions.append("ifnull(finance_book, '') in (%s, '')")
|
||||
values.append(self.filters.finance_book)
|
||||
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
||||
|
||||
if self.filters.get(party_type_field):
|
||||
conditions.append("party=%s")
|
||||
values.append(self.filters.get(party_type_field))
|
||||
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
|
||||
|
||||
if self.filters.party_account:
|
||||
conditions.append("account =%s")
|
||||
values.append(self.filters.party_account)
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
@ -792,46 +752,68 @@ class ReceivablePayableReport(object):
|
||||
]
|
||||
|
||||
if accounts:
|
||||
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
|
||||
values += accounts
|
||||
self.qb_selection_filter.append(self.ple.account.isin(accounts))
|
||||
|
||||
def add_customer_filters(
|
||||
self,
|
||||
):
|
||||
self.customter = qb.DocType("Customer")
|
||||
|
||||
def add_customer_filters(self, conditions, values):
|
||||
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"):
|
||||
conditions.append(self.get_hierarchical_filters("Territory", "territory"))
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
conditions.append("party in (select name from tabCustomer where payment_terms=%s)")
|
||||
values.append(self.filters.get("payment_terms_template"))
|
||||
self.qb_selection_filter.append(
|
||||
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"):
|
||||
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
|
||||
values.append(self.filters.get("sales_partner"))
|
||||
|
||||
def add_supplier_filters(self, conditions, values):
|
||||
if self.filters.get("supplier_group"):
|
||||
conditions.append(
|
||||
"""party in (select name from tabSupplier
|
||||
where supplier_group=%s)"""
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party_isin(
|
||||
qb.from_(self.customer).where(
|
||||
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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"):
|
||||
conditions.append("party in (select name from tabSupplier where payment_terms=%s)")
|
||||
values.append(self.filters.get("payment_terms_template"))
|
||||
self.qb_selection_filter.append(
|
||||
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):
|
||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||
|
||||
return """party in (select name from tabCustomer
|
||||
where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt}
|
||||
and name=tabCustomer.{key}))""".format(
|
||||
doctype=doctype, lft=lft, rgt=rgt, key=key
|
||||
)
|
||||
doc = qb.DocType(doctype)
|
||||
ple = self.ple
|
||||
customer = self.customer
|
||||
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)
|
||||
|
||||
if accounting_dimensions:
|
||||
@ -841,30 +823,16 @@ class ReceivablePayableReport(object):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions.append("{0} in %s".format(dimension.fieldname))
|
||||
values.append(tuple(self.filters.get(dimension.fieldname)))
|
||||
self.qb_selection_filter.append(
|
||||
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):
|
||||
# get the balance of the GL (debit - credit) or reverse balance based on report type
|
||||
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"):
|
||||
def is_invoice(self, ple):
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
return True
|
||||
|
||||
def get_party_details(self, party):
|
||||
|
Loading…
Reference in New Issue
Block a user