diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index de9d63d849..ad8fa5ba05 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -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):