From 1ab75db0acce6a4071dab999a39da0f851502e30 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Sep 2019 16:07:46 +0530 Subject: [PATCH] refactor: Accounts Receivable / Payable report (#18906) --- .../accounts_receivable.html | 28 +- .../accounts_receivable.js | 13 +- .../accounts_receivable.py | 1228 ++++++++--------- .../test_accounts_receivable.py | 35 +- .../accounts_receivable_summary.py | 345 ++--- erpnext/selling/doctype/customer/customer.py | 4 +- 6 files changed, 748 insertions(+), 905 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index 192b6d7be3..d00bcf643e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -40,7 +40,7 @@ -{% if(filters.show_pdc_in_print) { %} +{% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); var range1 = report.columns[11].label; var range2 = report.columns[12].label; @@ -122,22 +122,22 @@ {%= __("Date") %} {%= __("Age (Days)") %} - {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person_in_print) { %} + {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %} {%= __("Reference") %} {%= __("Sales Person") %} {% } else { %} {%= __("Reference") %} {% } %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %} {% } %} {%= __("Invoiced Amount") %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {%= __("Paid Amount") %} {%= report.report_name === "Accounts Receivable" ? __('Credit Note') : __('Debit Note') %} {% } %} {%= __("Outstanding Amount") %} - {% if(filters.show_pdc_in_print) { %} + {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} {%= __("Customer LPO No.") %} {% } %} @@ -162,18 +162,18 @@ {%= frappe.datetime.str_to_user(data[i]["posting_date"]) %} {%= data[i][__("Age (Days)")] %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {%= data[i]["voucher_type"] %}
{% } %} {%= data[i]["voucher_no"] %} - {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person_in_print) { %} + {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %} {%= data[i]["sales_person"] %} {% } %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {% if(!(filters.customer || filters.supplier)) { %} {%= data[i][__("Customer")] || data[i][__("Supplier")] %} @@ -195,7 +195,7 @@ {%= format_currency(data[i]["invoiced_amount"], data[i]["currency"]) %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {%= format_currency(data[i]["paid_amount"], data[i]["currency"]) %} @@ -204,7 +204,7 @@ {%= format_currency(data[i]["outstanding_amount"], data[i]["currency"]) %} - {% if(filters.show_pdc_in_print) { %} + {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} {%= data[i]["po_no"] %} @@ -215,10 +215,10 @@ {% } %} {% } else { %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {% } %} - {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person_in_print) { %} + {% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %} {% } %} @@ -226,7 +226,7 @@ {%= format_currency(data[i]["invoiced_amount"], data[i]["currency"] ) %} - {% if(!filters.show_pdc_in_print) { %} + {% if(!filters.show_future_payments) { %} {%= format_currency(data[i]["paid_amount"], data[i]["currency"]) %} {%= report.report_name === "Accounts Receivable" ? format_currency(data[i]["credit_note"], data[i]["currency"]) : format_currency(data[i]["debit_note"], data[i]["currency"]) %} @@ -234,7 +234,7 @@ {%= format_currency(data[i]["outstanding_amount"], data[i]["currency"]) %} - {% if(filters.show_pdc_in_print) { %} + {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} {%= data[i][__("Customer LPO")] %} diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 4551973ac6..228be18d21 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -130,13 +130,18 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Check", }, { - "fieldname":"show_pdc_in_print", - "label": __("Show PDC in Print"), + "fieldname":"show_future_payments", + "label": __("Show Future Payments"), "fieldtype": "Check", }, { - "fieldname":"show_sales_person_in_print", - "label": __("Show Sales Person in Print"), + "fieldname":"show_delivery_notes", + "label": __("Show Delivery Notes"), + "fieldtype": "Check", + }, + { + "fieldname":"show_sales_person", + "label": __("Show Sales Person"), "fieldtype": "Check", }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 0e4ee12548..88e4227837 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -4,9 +4,33 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _, scrub -from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr +from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr, now, time_diff_in_seconds +from collections import OrderedDict +from erpnext.accounts.utils import get_currency_precision from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions +# This report gives a summary of all Outstanding Invoices considering the following + +# 1. Invoice can be booked via Sales/Purchase Invoice or Journal Entry +# 2. Report handles both receivable and payable +# 3. Key balances for each row are "Invoiced Amount", "Paid Amount", "Credit/Debit Note Amount", "Oustanding Amount" +# 4. For explicit payment terms in invoice (example: 30% advance, 30% on delivery, 40% post delivery), +# the invoice will be broken up into multiple rows, one for each payment term +# 5. If there are payments after the report date (post dated), these will be updated in additional columns +# for future amount +# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters +# 7. For overpayment against an invoice with payment terms, there will be an additional row +# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated +# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party +# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" + +def execute(filters=None): + args = { + "party_type": "Customer", + "naming_by": ["Selling Settings", "cust_master_name"], + } + return ReceivablePayableReport(filters).run(args) + class ReceivablePayableReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -16,459 +40,429 @@ class ReceivablePayableReport(object): else self.filters.report_date def run(self, args): - party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) - columns = self.get_columns(party_naming_by, args) - data = self.get_data(party_naming_by, args) - chart = self.get_chart_data(columns, data) - return columns, data, None, chart - - def get_columns(self, party_naming_by, args): - columns = [] - columns.append({ - "label": _("Posting Date"), - "fieldtype": "Date", - "fieldname": "posting_date", - "width": 90 - }) - - columns += [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"] - - if party_naming_by == "Naming Series": - columns += [args.get("party_type") + " Name::110"] - - if args.get("party_type") == 'Customer': - columns.append({ - "label": _("Customer Contact"), - "fieldtype": "Link", - "fieldname": "contact", - "options":"Contact", - "width": 100 - }) - - columns.append({ - "label": _("Voucher Type"), - "fieldtype": "Data", - "fieldname": "voucher_type", - "width": 110 - }) - - columns.append({ - "label": _("Voucher No"), - "fieldtype": "Dynamic Link", - "fieldname": "voucher_no", - "width": 110, - "options": "voucher_type", - }) - - columns += [_("Due Date") + ":Date:80"] - - if args.get("party_type") == "Supplier": - columns += [_("Bill No") + "::80", _("Bill Date") + ":Date:80"] - - credit_or_debit_note = "Credit Note" if args.get("party_type") == "Customer" else "Debit Note" - - if self.filters.based_on_payment_terms: - columns.append({ - "label": _("Payment Term"), - "fieldname": "payment_term", - "fieldtype": "Data", - "width": 120 - }) - columns.append({ - "label": _("Invoice Grand Total"), - "fieldname": "invoice_grand_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) - - for label in ("Invoiced Amount", "Paid Amount", credit_or_debit_note, "Outstanding Amount"): - columns.append({ - "label": _(label), - "fieldname": frappe.scrub(label), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) - - columns += [_("Age (Days)") + ":Int:80"] - - self.ageing_col_idx_start = len(columns) - - if not "range1" in self.filters: - self.filters["range1"] = "30" - if not "range2" in self.filters: - self.filters["range2"] = "60" - if not "range3" in self.filters: - self.filters["range3"] = "90" - if not "range4" in self.filters: - self.filters["range4"] = "120" - - for label in ("0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))): - columns.append({ - "label": label, - "fieldname":label, - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) - - columns += [ - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "width": 100 - }, - { - "fieldname": "pdc/lc_ref", - "label": _("PDC/LC Ref"), - "fieldtype": "Data", - "width": 110 - }, - { - "fieldname": "pdc/lc_amount", - "label": _("PDC/LC Amount"), - "fieldtype": "Currency", - "options": "currency", - "width": 130 - }, - { - "fieldname": "remaining_balance", - "label": _("Remaining Balance"), - "fieldtype": "Currency", - "options": "currency", - "width": 130 - }] - - if args.get('party_type') == 'Customer': - columns += [ - { - "label": _("Customer LPO"), - "fieldtype": "Data", - "fieldname": "po_no", - "width": 100, - }, - _("Delivery Note") + ":Data:100", - _("Territory") + ":Link/Territory:80", - _("Customer Group") + ":Link/Customer Group:120", - { - "label": _("Sales Person"), - "fieldtype": "Data", - "fieldname": "sales_person", - "width": 120, - } - ] - if args.get("party_type") == "Supplier": - columns += [_("Supplier Group") + ":Link/Supplier Group:80"] - - columns.append(_("Remarks") + "::200") - - return columns - - def get_data(self, party_naming_by, args): - from erpnext.accounts.utils import get_currency_precision - self.currency_precision = get_currency_precision() or 2 - self.dr_or_cr = "debit" if args.get("party_type") == "Customer" else "credit" - - future_vouchers = self.get_entries_after(self.filters.report_date, args.get("party_type")) + self.filters.update(args) + self.set_defaults() + self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.get_columns() + self.get_data() + self.get_chart_data() + return self.columns, self.data, None, self.chart + def set_defaults(self): if not self.filters.get("company"): - self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') - + self.filters.company = frappe.db.get_single_value('Global Defaults', 'default_company') self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + self.currency_precision = get_currency_precision() or 2 + self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" + self.party_type = self.filters.party_type + self.party_details = {} + self.invoices = set() - return_entries = self.get_return_entries(args.get("party_type")) + def get_data(self): + t1 = now() + self.get_gl_entries() + self.voucher_balance = OrderedDict() + self.init_voucher_balance() # invoiced, paid, credit_note, outstanding - data = [] - self.pdc_details = get_pdc_details(args.get("party_type"), self.filters.report_date) - gl_entries_data = self.get_entries_till(self.filters.report_date, args.get("party_type")) + # Build delivery note map against all sales invoices + self.build_delivery_note_map() - if gl_entries_data: - voucher_nos = [d.voucher_no for d in gl_entries_data] or [] - dn_details = get_dn_details(args.get("party_type"), voucher_nos) - self.voucher_details = get_voucher_details(args.get("party_type"), voucher_nos, dn_details) + # Get invoice details like bill_no, due_date etc for all invoices + self.get_invoice_details() - if self.filters.based_on_payment_terms and gl_entries_data: - self.payment_term_map = self.get_payment_term_detail(voucher_nos) + # fetch future payments against invoices + self.get_future_payments() - self.gle_inclusion_map = {} - for gle in gl_entries_data: - if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries): - self.gle_inclusion_map[gle.name] = True - outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount( - gle,self.filters.report_date, self.dr_or_cr, return_entries) - temp_outstanding_amt = outstanding_amount - temp_credit_note_amt = credit_note_amount + self.data = [] + for gle in self.gl_entries: + self.update_voucher_balance(gle) - if abs(outstanding_amount) > 0.1/10**self.currency_precision: - if self.filters.based_on_payment_terms and self.payment_term_map.get(gle.voucher_no): - for d in self.payment_term_map.get(gle.voucher_no): - # Allocate payment amount based on payment terms(FIFO order) - payment_amount, d.payment_amount = self.allocate_based_on_fifo(payment_amount, d.payment_term_amount) + self.build_data() - term_outstanding_amount = d.payment_term_amount - d.payment_amount + def init_voucher_balance(self): + # build all keys, since we want to exclude vouchers beyond the report date + for gle in self.gl_entries: + # get the balance object for voucher_type + key = (gle.voucher_type, gle.voucher_no, gle.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, + posting_date = gle.posting_date, + remarks = gle.remarks, + invoiced = 0.0, + paid = 0.0, + credit_note = 0.0, + outstanding = 0.0 + ) + self.get_invoices(gle) - # Allocate credit note based on payment terms(FIFO order) - credit_note_amount, d.credit_note_amount = self.allocate_based_on_fifo(credit_note_amount, term_outstanding_amount) + def get_invoices(self, gle): + if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + self.invoices.add(gle.voucher_no) - term_outstanding_amount -= d.credit_note_amount - - row_outstanding = term_outstanding_amount - # Allocate PDC based on payment terms(FIFO order) - d.pdc_details, d.pdc_amount = self.allocate_pdc_amount_in_fifo(gle, row_outstanding) - - if term_outstanding_amount > 0: - row = self.prepare_row(party_naming_by, args, gle, term_outstanding_amount, - d.credit_note_amount, d.due_date, d.payment_amount , d.payment_term_amount, - d.description, d.pdc_amount, d.pdc_details) - data.append(row) - - if credit_note_amount: - row = self.prepare_row_without_payment_terms(party_naming_by, args, gle, temp_outstanding_amt, - temp_credit_note_amt) - data.append(row) - - else: - row = self.prepare_row_without_payment_terms(party_naming_by, args, gle, outstanding_amount, - credit_note_amount) - data.append(row) - return data - - def allocate_pdc_amount_in_fifo(self, gle, row_outstanding): - pdc_list = self.pdc_details.get((gle.voucher_no, gle.party), []) - - pdc_details = [] - pdc_amount = 0 - for pdc in pdc_list: - if row_outstanding <= pdc.pdc_amount: - pdc_amount += row_outstanding - pdc.pdc_amount -= row_outstanding - if row_outstanding and pdc.pdc_ref and pdc.pdc_date: - pdc_details.append(cstr(pdc.pdc_ref) + "/" + formatdate(pdc.pdc_date)) - row_outstanding = 0 + def update_voucher_balance(self, gle): + # 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) + # 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) + 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 else: - pdc_amount = pdc.pdc_amount - if pdc.pdc_amount and pdc.pdc_ref and pdc.pdc_date: - pdc_details.append(cstr(pdc.pdc_ref) + "/" + formatdate(pdc.pdc_date)) - pdc.pdc_amount = 0 - row_outstanding -= pdc_amount - - return pdc_details, pdc_amount - - def prepare_row_without_payment_terms(self, party_naming_by, args, gle, outstanding_amount, credit_note_amount): - pdc_list = self.pdc_details.get((gle.voucher_no, gle.party), []) - pdc_amount = 0 - pdc_details = [] - for d in pdc_list: - pdc_amount += flt(d.pdc_amount) - if pdc_amount and d.pdc_ref and d.pdc_date: - pdc_details.append(cstr(d.pdc_ref) + "/" + formatdate(d.pdc_date)) - - row = self.prepare_row(party_naming_by, args, gle, outstanding_amount, - credit_note_amount, pdc_amount=pdc_amount, pdc_details=pdc_details) - - return row - - - def allocate_based_on_fifo(self, total_amount, row_amount): - allocated_amount = 0 - if row_amount <= total_amount: - allocated_amount = row_amount - total_amount -= row_amount + # invoice + row.invoiced += gle_balance else: - allocated_amount = total_amount - total_amount = 0 + # payment or credit note for receivables + if self.is_invoice(gle): + # stand alone debit / credit note + row.credit_note -= gle_balance + else: + # advance / unlinked payment or other adjustment + row.paid -= gle_balance - return total_amount, allocated_amount + def get_voucher_balance(self, gle): + voucher_balance = None - def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_note_amount, - due_date=None, paid_amt=None, payment_term_amount=None, payment_term=None, pdc_amount=None, pdc_details=None): - row = [gle.posting_date, gle.party] + if gle.against_voucher: + # find invoice + voucher_balance = self.voucher_balance.get((gle.against_voucher_type, gle.against_voucher, gle.party)) - # customer / supplier name - if party_naming_by == "Naming Series": - row += [self.get_party_name(gle.party_type, 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)) - if args.get("party_type") == 'Customer': - row += [self.get_customer_contact(gle.party_type, gle.party)] + return voucher_balance - # get due date - if not due_date: - due_date = self.voucher_details.get(gle.voucher_no, {}).get("due_date", "") - bill_date = self.voucher_details.get(gle.voucher_no, {}).get("bill_date", "") + def build_data(self): + # set outstanding for all the accumulated balances + # as we can use this to filter out invoices without outstanding + for key, row in self.voucher_balance.items(): + row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.invoice_grand_total = row.invoiced - row += [gle.voucher_type, gle.voucher_no, due_date] + if abs(row.outstanding) > 0.1/10 ** self.currency_precision: + # non-zero oustanding, we must consider this row - # get supplier bill details - if args.get("party_type") == "Supplier": - row += [ - self.voucher_details.get(gle.voucher_no, {}).get("bill_no", ""), - self.voucher_details.get(gle.voucher_no, {}).get("bill_date", "") - ] + if self.is_invoice(row) and self.filters.based_on_payment_terms: + # is an invoice, allocate based on fifo + # adds a list `payment_terms` which contains new rows for each term + self.allocate_outstanding_based_on_payment_terms(row) - # invoiced and paid amounts - invoiced_amount = gle.get(self.dr_or_cr) if (gle.get(self.dr_or_cr) > 0) else 0 + if row.payment_terms: + # make separate rows for each payment term + for d in row.payment_terms: + if d.outstanding > 0: + self.append_row(d) - if self.filters.based_on_payment_terms: - row+=[payment_term, invoiced_amount] - if payment_term_amount: - invoiced_amount = payment_term_amount - - if not payment_term_amount: - paid_amt = invoiced_amount - outstanding_amount - credit_note_amount - row += [invoiced_amount, paid_amt, credit_note_amount, outstanding_amount] - - # ageing data - if self.filters.ageing_based_on == "Due Date": - entry_date = due_date - elif self.filters.ageing_based_on == "Supplier Invoice Date": - entry_date = bill_date - else: - entry_date = gle.posting_date - - row += get_ageing_data(cint(self.filters.range1), cint(self.filters.range2), - cint(self.filters.range3), cint(self.filters.range4), self.age_as_on, entry_date, outstanding_amount) - - # issue 6371-Ageing buckets should not have amounts if due date is not reached - if self.filters.ageing_based_on == "Due Date" \ - and getdate(due_date) > getdate(self.filters.report_date): - row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 - - if self.filters.ageing_based_on == "Supplier Invoice Date" \ - and getdate(bill_date) > getdate(self.filters.report_date): - - row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 - - if self.filters.get(scrub(args.get("party_type"))): - row.append(gle.account_currency) - else: - row.append(self.company_currency) - - remaining_balance = outstanding_amount - flt(pdc_amount) - pdc_details = ", ".join(pdc_details) - row += [pdc_details, pdc_amount, remaining_balance] - - if args.get('party_type') == 'Customer': - # customer LPO - row += [self.voucher_details.get(gle.voucher_no, {}).get("po_no")] - - # Delivery Note - row += [self.voucher_details.get(gle.voucher_no, {}).get("delivery_note")] - - # customer territory / supplier group - if args.get("party_type") == "Customer": - row += [self.get_territory(gle.party), self.get_customer_group(gle.party), - self.voucher_details.get(gle.voucher_no, {}).get("sales_person")] - if args.get("party_type") == "Supplier": - row += [self.get_supplier_group(gle.party)] - - row.append(gle.remarks) - - return row - - def get_entries_after(self, report_date, party_type): - # returns a distinct list - return list(set([(e.voucher_type, e.voucher_no) for e in self.get_gl_entries(party_type, report_date, for_future=True)])) - - def get_entries_till(self, report_date, party_type): - # returns a generator - return self.get_gl_entries(party_type, report_date) - - def is_receivable_or_payable(self, gle, dr_or_cr, future_vouchers, return_entries): - return ( - # advance - (not gle.against_voucher) or - - # against sales order/purchase order - (gle.against_voucher_type in ["Sales Order", "Purchase Order"]) or - - # sales invoice/purchase invoice - (gle.against_voucher==gle.voucher_no and gle.get(dr_or_cr) > 0) or - - # standalone credit notes - (gle.against_voucher==gle.voucher_no and gle.voucher_no in return_entries and not return_entries.get(gle.voucher_no)) or - - # entries adjusted with future vouchers - ((gle.against_voucher_type, gle.against_voucher) in future_vouchers) - ) - - def get_return_entries(self, party_type): - doctype = "Sales Invoice" if party_type=="Customer" else "Purchase Invoice" - return_entries = frappe._dict(frappe.get_all(doctype, - filters={"is_return": 1, "docstatus": 1}, fields=["name", "return_against"], as_list=1)) - return return_entries - - def get_outstanding_amount(self, gle, report_date, dr_or_cr, return_entries): - payment_amount, credit_note_amount = 0.0, 0.0 - reverse_dr_or_cr = "credit" if dr_or_cr=="debit" else "debit" - for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no): - if getdate(e.posting_date) <= report_date \ - and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))): - if e.name!=gle.name and self.gle_inclusion_map.get(e.name): - continue - self.gle_inclusion_map[e.name] = True - amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision) - if e.voucher_no not in return_entries: - payment_amount += amount + # if there is overpayment, add another row + self.allocate_extra_payments_or_credits(row) + else: + self.append_row(row) else: - credit_note_amount += amount + self.append_row(row) - voucher_amount = flt(gle.get(dr_or_cr), self.currency_precision) - flt(gle.get(reverse_dr_or_cr), self.currency_precision) - if gle.voucher_no in return_entries and not return_entries.get(gle.voucher_no): - voucher_amount = 0 + def append_row(self, row): + self.allocate_future_payments(row) + self.set_invoice_details(row) + self.set_party_details(row) + self.set_ageing(row) + self.data.append(row) - outstanding_amount = flt((voucher_amount - payment_amount - credit_note_amount), self.currency_precision) - credit_note_amount = flt(credit_note_amount, self.currency_precision) + def set_invoice_details(self, row): + row.update(self.invoice_details.get(row.voucher_no, {})) + if row.voucher_type == 'Sales Invoice': + if self.filters.show_delivery_notes: + self.set_delivery_notes(row) - return outstanding_amount, credit_note_amount, payment_amount + if self.filters.show_sales_person and row.sales_team: + row.sales_person = ", ".join(row.sales_team) + del row['sales_team'] - def get_party_name(self, party_type, party_name): - return self.get_party_map(party_type).get(party_name, {}).get("customer_name" if party_type == "Customer" else "supplier_name") or "" + def set_delivery_notes(self, row): + delivery_notes = self.delivery_notes.get(row.voucher_no, []) + if delivery_notes: + row.delivery_notes = ', '.join(delivery_notes) - def get_customer_contact(self, party_type, party_name): - return self.get_party_map(party_type).get(party_name, {}).get("customer_primary_contact") + def build_delivery_note_map(self): + if self.invoices and self.filters.show_delivery_notes: + self.delivery_notes = frappe._dict() - def get_territory(self, party_name): - return self.get_party_map("Customer").get(party_name, {}).get("territory") or "" + # delivery note link inside sales invoice + si_against_dn = frappe.db.sql(""" + select parent, delivery_note + from `tabSales Invoice Item` + where docstatus=1 and parent in (%s) + """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices), as_dict=1) - def get_customer_group(self, party_name): - return self.get_party_map("Customer").get(party_name, {}).get("customer_group") or "" + for d in si_against_dn: + if d.delivery_note: + self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note) - def get_supplier_group(self, party_name): - return self.get_party_map("Supplier").get(party_name, {}).get("supplier_group") or "" + dn_against_si = frappe.db.sql(""" + select distinct parent, against_sales_invoice + from `tabDelivery Note Item` + where against_sales_invoice in (%s) + """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices) , as_dict=1) - def get_party_map(self, party_type): - if not hasattr(self, "party_map"): - if party_type == "Customer": - select_fields = "name, customer_name, territory, customer_group, customer_primary_contact" - elif party_type == "Supplier": - select_fields = "name, supplier_name, supplier_group" + for d in dn_against_si: + self.delivery_notes.setdefault(d.against_sales_invoice, set()).add(d.parent) - self.party_map = dict(((r.name, r) for r in frappe.db.sql("select {0} from `tab{1}`" - .format(select_fields, party_type), as_dict=True))) + def get_invoice_details(self): + self.invoice_details = frappe._dict() + if self.party_type == "Customer": + si_list = frappe.db.sql(""" + select name, due_date, po_no + from `tabSales Invoice` + where posting_date <= %s + """,self.filters.report_date, as_dict=1) + for d in si_list: + self.invoice_details.setdefault(d.name, d) - return self.party_map + # Get Sales Team + if self.filters.show_sales_person: + sales_team = frappe.db.sql(""" + select parent, sales_person + from `tabSales Team` + where parenttype = 'Sales Invoice' + """, as_dict=1) + for d in sales_team: + self.invoice_details.setdefault(d.parent, {})\ + .setdefault('sales_team', []).append(d.sales_person) - def get_gl_entries(self, party_type, date=None, for_future=False): - conditions, values = self.prepare_conditions(party_type) + if self.party_type == "Supplier": + for pi in frappe.db.sql(""" + select name, due_date, bill_no, bill_date + from `tabPurchase Invoice` + where posting_date <= %s + """, self.filters.report_date, as_dict=1): + self.invoice_details.setdefault(pi.name, pi) - if self.filters.get(scrub(party_type)): - select_fields = "sum(debit_in_account_currency) as debit, sum(credit_in_account_currency) as credit" + # Invoices booked via Journal Entries + journal_entries = frappe.db.sql(""" + select name, due_date, bill_no, bill_date + from `tabJournal Entry` + where posting_date <= %s + """, self.filters.report_date, as_dict=1) + + for je in journal_entries: + if je.bill_no: + self.invoice_details.setdefault(je.name, je) + + def set_party_details(self, row): + # customer / supplier name + party_details = self.get_party_details(row.party) + row.update(party_details) + + if self.filters.get(scrub(self.filters.party_type)): + row.currency = row.account_currency else: - select_fields = "sum(debit) as debit, sum(credit) as credit" + row.currency = self.company_currency - if date and not for_future: - conditions += " and posting_date <= '%s'" % date + def allocate_outstanding_based_on_payment_terms(self, row): + self.get_payment_terms(row) + for term in row.payment_terms: + term.outstanding = term.invoiced - if date and for_future: - conditions += " and posting_date > '%s'" % date + # update "paid" and "oustanding" for this term + self.allocate_closing_to_term(row, term, 'paid') + + # update "credit_note" and "oustanding" for this term + if term.outstanding: + self.allocate_closing_to_term(row, term, 'credit_note') + + def get_payment_terms(self, row): + # build payment_terms for row + payment_terms_details = frappe.db.sql(""" + select + si.name, si.party_account_currency, si.currency, si.conversion_rate, + ps.due_date, ps.payment_amount, ps.description + from `tab{0}` si, `tabPayment Schedule` ps + where + si.name = ps.parent and + si.name = %s + order by ps.due_date + """.format(row.voucher_type), row.voucher_no, as_dict = 1) + + + original_row = frappe._dict(row) + row.payment_terms = [] + + # If no or single payment terms, no need to split the row + if len(payment_terms_details) <= 1: + return + + for d in payment_terms_details: + term = frappe._dict(original_row) + self.append_payment_term(row, d, term) + + def append_payment_term(self, row, d, term): + if self.filters.get("customer") and d.currency == d.party_account_currency: + invoiced = d.payment_amount + else: + invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) + + row.payment_terms.append(term.update({ + "due_date": d.due_date, + "invoiced": invoiced, + "invoice_grand_total": row.invoiced, + "payment_term": d.description, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 0.0 + })) + + def allocate_closing_to_term(self, row, term, key): + if row[key]: + if row[key] > term.outstanding: + term[key] = term.outstanding + row[key] -= term.outstanding + else: + term[key] = row[key] + row[key] = 0 + term.outstanding -= term[key] + + def allocate_extra_payments_or_credits(self, row): + # allocate extra payments / credits + additional_row = None + for key in ('paid', 'credit_note'): + if row[key] > 0: + if not additional_row: + additional_row = frappe._dict(row) + additional_row.invoiced = 0.0 + additional_row[key] = row[key] + + if additional_row: + additional_row.outstanding = additional_row.invoiced - additional_row.paid - additional_row.credit_note + self.append_row(additional_row) + + def get_future_payments(self): + if self.filters.show_future_payments: + self.future_payments = frappe._dict() + future_payments = list(self.get_future_payments_from_payment_entry()) + future_payments += list(self.get_future_payments_from_journal_entry()) + if future_payments: + for d in future_payments: + if d.future_amount and d.invoice_no: + self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) + + def get_future_payments_from_payment_entry(self): + return frappe.db.sql(""" + select + ref.reference_name as invoice_no, + payment_entry.party, + payment_entry.party_type, + payment_entry.posting_date as future_date, + ref.allocated_amount as future_amount, + payment_entry.reference_no as future_ref + from + `tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref + on + (ref.parent = payment_entry.name) + where + payment_entry.docstatus = 1 + and payment_entry.posting_date > %s + and payment_entry.party_type = %s + """, (self.filters.report_date, self.party_type), as_dict=1) + + def get_future_payments_from_journal_entry(self): + if self.filters.get('party'): + amount_field = ("jea.debit_in_account_currency - jea.credit_in_account_currency" + if self.party_type == 'Supplier' else "jea.credit_in_account_currency - jea.debit_in_account_currency") + else: + amount_field = ("jea.debit - " if self.party_type == 'Supplier' else "jea.credit") + + return frappe.db.sql(""" + select + jea.reference_name as invoice_no, + jea.party, + jea.party_type, + je.posting_date as future_date, + sum({0}) as future_amount, + je.cheque_no as future_ref + from + `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea + on + (jea.parent = je.name) + where + je.docstatus = 1 + and je.posting_date > %s + and jea.party_type = %s + and jea.reference_name is not null and jea.reference_name != '' + group by je.name, jea.reference_name + having future_amount > 0 + """.format(amount_field), (self.filters.report_date, self.party_type), as_dict=1) + + def allocate_future_payments(self, row): + # future payments are captured in additional columns + # this method allocates pending future payments against a voucher to + # the current row (which could be generated from payment terms) + if not self.filters.show_future_payments: + return + + row.remaining_balance = row.outstanding + row.future_amount = 0.0 + for future in self.future_payments.get((row.voucher_no, row.party), []): + if row.remaining_balance > 0 and future.future_amount: + if future.future_amount > row.outstanding: + row.future_amount = row.outstanding + future.future_amount = future.future_amount - row.outstanding + row.remaining_balance = 0 + else: + row.future_amount += future.future_amount + future.future_amount = 0 + row.remaining_balance = row.outstanding - row.future_amount + + row.setdefault('future_ref', []).append(cstr(future.future_ref) + '/' + cstr(future.future_date)) + + if row.future_ref: + row.future_ref = ', '.join(row.future_ref) + + def set_ageing(self, row): + if self.filters.ageing_based_on == "Due Date": + entry_date = row.due_date + elif self.filters.ageing_based_on == "Supplier Invoice Date": + entry_date = row.bill_date + else: + entry_date = row.posting_date + + self.get_ageing_data(entry_date, row) + + # ageing buckets should not have amounts if due date is not reached + if getdate(entry_date) > getdate(self.filters.report_date): + row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + + def get_ageing_data(self, entry_date, row): + # [0-30, 30-60, 60-90, 90-120, 120-above] + row.range1 = row.range2 = row.range3 = row.range4 = range5 = 0.0 + + if not (self.age_as_on and entry_date): + return + + row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0 + index = None + for i, days in enumerate([self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]): + if row.age <= days: + index = i + break + + if index is None: index = 4 + row['range' + str(index+1)] = row.outstanding + + def get_gl_entries(self): + # get all the GL entries filtered by the given filters + + conditions, values = self.prepare_conditions() + + 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" self.gl_entries = frappe.db.sql(""" select @@ -477,91 +471,95 @@ class ReceivablePayableReport(object): from `tabGL Entry` where - docstatus < 2 and party_type=%s and (party is not null and party != '') {1} - group by voucher_type, voucher_no, against_voucher_type, against_voucher, party - order by posting_date, party""" + docstatus < 2 + and party_type=%s + and (party is not null and party != '') + and posting_date <= %s + {1} + order by posting_date, party""" .format(select_fields, conditions), values, as_dict=True) - return self.gl_entries - - def prepare_conditions(self, party_type): + def prepare_conditions(self): conditions = [""] - values = [party_type] + values = [self.party_type, self.filters.report_date] + party_type_field = scrub(self.party_type) - party_type_field = scrub(party_type) + self.add_common_filters(conditions, values, party_type_field) + if party_type_field=="customer": + self.add_customer_filters(conditions, values) + + elif party_type_field=="supplier": + self.add_supplier_filters(conditions, values) + + self.add_accounting_dimensions_filters() + + return " and ".join(conditions), values + + def add_common_filters(self, conditions, values, party_type_field): if self.filters.company: conditions.append("company=%s") values.append(self.filters.company) if self.filters.finance_book: - conditions.append("ifnull(finance_book,'') in (%s, '')") + conditions.append("ifnull(finance_book, '') in (%s, '')") values.append(self.filters.finance_book) if self.filters.get(party_type_field): conditions.append("party=%s") values.append(self.filters.get(party_type_field)) - if party_type_field=="customer": - account_type = "Receivable" - if self.filters.get("customer_group"): - lft, rgt = frappe.db.get_value("Customer Group", - self.filters.get("customer_group"), ["lft", "rgt"]) + # get GL with "receivable" or "payable" account_type + account_type = "Receivable" if self.party_type == "Customer" else "Payable" + accounts = [d.name for d in frappe.get_all("Account", + filters={"account_type": account_type, "company": self.filters.company})] + conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) + values += accounts - 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)) + def add_customer_filters(self, conditions, values): + if self.filters.get("customer_group"): + conditions.append(self.get_hierarchical_filters('Customer Group', 'customer_group')) - if self.filters.get("territory"): - lft, rgt = frappe.db.get_value("Territory", - self.filters.get("territory"), ["lft", "rgt"]) + if self.filters.get("territory"): + conditions.append(self.get_hierarchical_filters('Territory', 'territory')) - 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=%s)") + values.append(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)") - values.append(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")) - 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")) + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) - if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + conditions.append("""exists(select name from `tabSales Team` steam where + steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) + and ((steam.parent = voucher_no and steam.parenttype = voucher_type) + or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) + or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) - and ((steam.parent = voucher_no and steam.parenttype = voucher_type) - or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) + 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)""") + values.append(self.filters.get("supplier_group")) - elif party_type_field=="supplier": - account_type = "Payable" - if self.filters.get("supplier_group"): - conditions.append("""party in (select name from tabSupplier - where supplier_group=%s)""") - 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")) - 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")) + def get_hierarchical_filters(self, doctype, key): + lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"]) - if self.filters.get("cost_center"): - lft, rgt = frappe.get_cached_value("Cost Center", - self.filters.get("cost_center"), ['lft', 'rgt']) - - conditions.append("""cost_center in (select name from `tabCost Center` where - lft >= {0} and rgt <= {1})""".format(lft, rgt)) - - if self.filters.company: - accounts = [d.name for d in frappe.get_all("Account", - filters={"account_type": account_type, "company": self.filters.company})] - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) - values += accounts + 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) + def add_accounting_dimensions_filters(self, conditions, values): accounting_dimensions = get_accounting_dimensions() if accounting_dimensions: @@ -570,195 +568,133 @@ class ReceivablePayableReport(object): conditions.append("{0} = %s".format(dimension)) values.append(self.filters.get(dimension)) - return " and ".join(conditions), values + 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_gl_entries_for(self, party, party_type, against_voucher_type, against_voucher): - if not hasattr(self, "gl_entries_map"): - self.gl_entries_map = {} - for gle in self.get_gl_entries(party_type): - if gle.against_voucher_type and gle.against_voucher: - self.gl_entries_map.setdefault(gle.party, {})\ - .setdefault(gle.against_voucher_type, {})\ - .setdefault(gle.against_voucher, [])\ - .append(gle) + 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') - return self.gl_entries_map.get(party, {})\ - .get(against_voucher_type, {})\ - .get(against_voucher, []) + def is_invoice(self, gle): + if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + return True - def get_payment_term_detail(self, voucher_nos): - payment_term_map = frappe._dict() - payment_terms_details = frappe.db.sql(""" select si.name, - party_account_currency, currency, si.conversion_rate, - ps.due_date, ps.payment_amount, ps.description - from `tabSales Invoice` si, `tabPayment Schedule` ps - where si.name = ps.parent and - si.docstatus = 1 and si.company = %s and - si.name in (%s) order by ps.due_date - """ % (frappe.db.escape(self.filters.company), ','.join(['%s'] *len(voucher_nos))), - (tuple(voucher_nos)), as_dict = 1) - - for d in payment_terms_details: - if self.filters.get("customer") and d.currency == d.party_account_currency: - payment_term_amount = d.payment_amount + def get_party_details(self, party): + if not party in self.party_details: + if self.party_type == 'Customer': + self.party_details[party] = frappe.db.get_value('Customer', party, ['customer_name', + 'territory', 'customer_group', 'customer_primary_contact'], as_dict=True) else: - payment_term_amount = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) + self.party_details[party] = frappe.db.get_value('Supplier', party, ['supplier_name', + 'supplier_group'], as_dict=True) - payment_term_map.setdefault(d.name, []).append(frappe._dict({ - "due_date": d.due_date, - "payment_term_amount": payment_term_amount, - "description": d.description - })) - return payment_term_map + return self.party_details[party] - def get_chart_data(self, columns, data): - ageing_columns = columns[self.ageing_col_idx_start : self.ageing_col_idx_start+5] + def get_columns(self): + self.columns = [] + self.add_column('Posting Date', fieldtype='Date') + self.add_column(label=_(self.party_type), fieldname='party', + fieldtype='Link', options=self.party_type, width=180) + + if self.party_naming_by == "Naming Series": + self.add_column(_('{0} Name').format(self.party_type), + fieldname = scrub(self.party_type) + '_name', fieldtype='Data') + + if self.party_type == 'Customer': + self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', + fieldtype='Link', options='Contact') + + self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') + self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', + options='voucher_type', width=180) + self.add_column(label='Due Date', fieldtype='Date') + + if self.party_type == "Supplier": + self.add_column(label=_('Bill No'), fieldname='bill_no', fieldtype='Data') + self.add_column(label=_('Bill Date'), fieldname='bill_date', fieldtype='Date') + + if self.filters.based_on_payment_terms: + self.add_column(label=_('Payment Term'), fieldname='payment_term', fieldtype='Data') + self.add_column(label=_('Invoice Grand Total'), fieldname='invoice_grand_total') + + self.add_column(_('Invoiced Amount'), fieldname='invoiced') + self.add_column(_('Paid Amount'), fieldname='paid') + if self.party_type == "Customer": + self.add_column(_('Credit Note'), fieldname='credit_note') + else: + # note: fieldname is still `credit_note` + self.add_column(_('Debit Note'), fieldname='credit_note') + self.add_column(_('Outstanding Amount'), fieldname='outstanding') + + self.setup_ageing_columns() + + self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', options='Currency', width=80) + + if self.filters.show_future_payments: + self.add_column(label=_('Future Payment Ref'), fieldname='future_ref', fieldtype='Data') + self.add_column(label=_('Future Payment Amount'), fieldname='future_amount') + self.add_column(label=_('Remaining Balance'), fieldname='remaining_balance') + + if self.filters.party_type == 'Customer': + self.add_column(label=_('Customer LPO'), fieldname='po_no', fieldtype='Data') + + # comma separated list of linked delivery notes + if self.filters.show_delivery_notes: + self.add_column(label=_('Delivery Notes'), fieldname='delivery_notes', fieldtype='Data') + self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', + options='Territory') + self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', + options='Customer Group') + if self.filters.show_sales_person: + self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + + if self.filters.party_type == "Supplier": + self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', + options='Supplier Group') + + self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200) + + def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): + if not fieldname: fieldname = scrub(label) + if fieldtype=='Currency': options='currency' + if fieldtype=='Date': width = 90 + + self.columns.append(dict( + label=label, + fieldname=fieldname, + fieldtype=fieldtype, + options=options, + width=width + )) + + def setup_ageing_columns(self): + # for charts + self.ageing_column_labels = [] + self.add_column(label=_('Age (Days)'), fieldname='age', fieldtype='Int', width=80) + + for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), + "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), + "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), + "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): + self.add_column(label=label, fieldname='range' + str(i+1)) + self.ageing_column_labels.append(label) + + def get_chart_data(self): rows = [] - for d in data: - values = d[self.ageing_col_idx_start : self.ageing_col_idx_start+5] - precision = cint(frappe.db.get_default("float_precision")) or 2 - formatted_values = [frappe.utils.rounded(val, precision) for val in values] + for row in self.data: rows.append( { - 'values': formatted_values + 'values': [row.range1, row.range2, row.range3, row.range4, row.range5] } ) - return { + self.chart = { "data": { - 'labels': [d.get("label") for d in ageing_columns], + 'labels': self.ageing_column_labels, 'datasets': rows }, "type": 'percentage' - } - -def execute(filters=None): - args = { - "party_type": "Customer", - "naming_by": ["Selling Settings", "cust_master_name"], - } - return ReceivablePayableReport(filters).run(args) - -def get_ageing_data(first_range, second_range, third_range, - fourth_range, age_as_on, entry_date, outstanding_amount): - # [0-30, 30-60, 60-90, 90-120, 120-above] - outstanding_range = [0.0, 0.0, 0.0, 0.0, 0.0] - - if not (age_as_on and entry_date): - return [0] + outstanding_range - - age = (getdate(age_as_on) - getdate(entry_date)).days or 0 - index = None - for i, days in enumerate([first_range, second_range, third_range, fourth_range]): - if age <= days: - index = i - break - - if index is None: index = 4 - outstanding_range[index] = outstanding_amount - - return [age] + outstanding_range - -def get_pdc_details(party_type, report_date): - pdc_details = frappe._dict() - pdc_via_pe = frappe.db.sql(""" - select - pref.reference_name as invoice_no, pent.party, pent.party_type, - pent.posting_date as pdc_date, ifnull(pref.allocated_amount,0) as pdc_amount, - pent.reference_no as pdc_ref - from - `tabPayment Entry` as pent inner join `tabPayment Entry Reference` as pref - on - (pref.parent = pent.name) - where - pent.docstatus < 2 and pent.posting_date > %s - and pent.party_type = %s - """, (report_date, party_type), as_dict=1) - - for pdc in pdc_via_pe: - pdc_details.setdefault((pdc.invoice_no, pdc.party), []).append(pdc) - - if scrub(party_type): - amount_field = ("jea.debit_in_account_currency" - if party_type == 'Supplier' else "jea.credit_in_account_currency") - else: - amount_field = "jea.debit + jea.credit" - - pdc_via_je = frappe.db.sql(""" - select - jea.reference_name as invoice_no, jea.party, jea.party_type, - je.posting_date as pdc_date, ifnull({0},0) as pdc_amount, - je.cheque_no as pdc_ref - from - `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea - on - (jea.parent = je.name) - where - je.docstatus < 2 and je.posting_date > %s - and jea.party_type = %s - """.format(amount_field), (report_date, party_type), as_dict=1) - - for pdc in pdc_via_je: - pdc_details.setdefault((pdc.invoice_no, pdc.party), []).append(pdc) - - return pdc_details - -def get_dn_details(party_type, voucher_nos): - dn_details = frappe._dict() - - if party_type == "Customer": - for si in frappe.db.sql(""" - select - parent, GROUP_CONCAT(delivery_note SEPARATOR ', ') as dn - from - `tabSales Invoice Item` - where - docstatus=1 and delivery_note is not null and delivery_note != '' - and parent in (%s) group by parent - """ %(','.join(['%s'] * len(voucher_nos))), tuple(voucher_nos) , as_dict=1): - dn_details.setdefault(si.parent, si.dn) - - for si in frappe.db.sql(""" - select - against_sales_invoice as parent, GROUP_CONCAT(parent SEPARATOR ', ') as dn - from - `tabDelivery Note Item` - where - docstatus=1 and against_sales_invoice is not null and against_sales_invoice != '' - and against_sales_invoice in (%s) - group by against_sales_invoice - """ %(','.join(['%s'] * len(voucher_nos))), tuple(voucher_nos) , as_dict=1): - if si.parent in dn_details: - dn_details[si.parent] += ', %s' %(si.dn) - else: - dn_details.setdefault(si.parent, si.dn) - - return dn_details - -def get_voucher_details(party_type, voucher_nos, dn_details): - voucher_details = frappe._dict() - - if party_type == "Customer": - for si in frappe.db.sql(""" - select inv.name, inv.due_date, inv.po_no, GROUP_CONCAT(steam.sales_person SEPARATOR ', ') as sales_person - from `tabSales Invoice` inv - left join `tabSales Team` steam on steam.parent = inv.name and steam.parenttype = 'Sales Invoice' - where inv.docstatus=1 and inv.name in (%s) - group by inv.name - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): - si['delivery_note'] = dn_details.get(si.name) - voucher_details.setdefault(si.name, si) - - if party_type == "Supplier": - for pi in frappe.db.sql("""select name, due_date, bill_no, bill_date - from `tabPurchase Invoice` where docstatus = 1 and name in (%s) - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): - voucher_details.setdefault(pi.name, pi) - - for pi in frappe.db.sql("""select name, due_date, bill_no, bill_date from - `tabJournal Entry` where docstatus = 1 and bill_no is not NULL and name in (%s) - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): - voucher_details.setdefault(pi.name, pi) - - return voucher_details + } \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 43786a4446..f0274b4472 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -14,33 +14,44 @@ class TestAccountsReceivable(unittest.TestCase): filters = { 'company': '_Test Company 2', - 'based_on_payment_terms': 1 + 'based_on_payment_terms': 1, + 'report_date': today(), + 'range1': 30, + 'range2': 60, + 'range3': 90, + 'range4': 120 } + # check invoice grand total and invoiced column's value for 3 payment terms name = make_sales_invoice() report = execute(filters) - expected_data = [[100,30], [100,50], [100,20]] + expected_data = [[100, 30], [100, 50], [100, 20]] - self.assertEqual(expected_data[0], report[1][0][7:9]) - self.assertEqual(expected_data[1], report[1][1][7:9]) - self.assertEqual(expected_data[2], report[1][2][7:9]) + for i in range(3): + row = report[1][i-1] + self.assertEqual(expected_data[i-1], [row.invoice_grand_total, row.invoiced]) + # check invoice grand total, invoiced, paid and outstanding column's value after payment make_payment(name) report = execute(filters) - expected_data_after_payment = [[100,50], [100,20]] + expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] - self.assertEqual(expected_data_after_payment[0], report[1][0][7:9]) - self.assertEqual(expected_data_after_payment[1], report[1][1][7:9]) + for i in range(2): + row = report[1][i-1] + self.assertEqual(expected_data_after_payment[i-1], + [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding]) + # check invoice grand total, invoiced, paid and outstanding column's value after credit note make_credit_note(name) report = execute(filters) - expected_data_after_credit_note = [[100,100,30,100,-30]] - - self.assertEqual(expected_data_after_credit_note[0], report[1][0][7:12]) + expected_data_after_credit_note = [100, 0, 0, 40, -40] + row = report[1][0] + self.assertEqual(expected_data_after_credit_note, + [row.invoice_grand_total, row.invoiced, row.paid, row.credit_note, row.outstanding]) def make_sales_invoice(): frappe.set_user("Administrator") @@ -64,7 +75,7 @@ def make_sales_invoice(): return si.name def make_payment(docname): - pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=30) + pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) pe.paid_from = "Debtors - _TC2" pe.insert() pe.submit() diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index ec24aece5f..350e081957 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -3,236 +3,11 @@ from __future__ import unicode_literals import frappe -from frappe import _, scrub -from frappe.utils import flt +from frappe import _ +from frappe.utils import flt, cint from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport - from six import iteritems -from six.moves import zip - -class AccountsReceivableSummary(ReceivablePayableReport): - def run(self, args): - party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) - return self.get_columns(party_naming_by, args), self.get_data(party_naming_by, args) - - def get_columns(self, party_naming_by, args): - columns = [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"] - - if party_naming_by == "Naming Series": - columns += [ args.get("party_type") + " Name::140"] - - credit_debit_label = "Credit Note Amt" if args.get('party_type') == 'Customer' else "Debit Note Amt" - - columns += [{ - "label": _("Advance Amount"), - "fieldname": "advance_amount", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - },{ - "label": _("Total Invoiced Amt"), - "fieldname": "total_invoiced_amt", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - }, - { - "label": _("Total Paid Amt"), - "fieldname": "total_paid_amt", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - }] - - columns += [ - { - "label": _(credit_debit_label), - "fieldname": scrub(credit_debit_label), - "fieldtype": "Currency", - "options": "currency", - "width": 140 - }, - { - "label": _("Total Outstanding Amt"), - "fieldname": "total_outstanding_amt", - "fieldtype": "Currency", - "options": "currency", - "width": 160 - }, - { - "label": _("0-" + str(self.filters.range1)), - "fieldname": scrub("0-" + str(self.filters.range1)), - "fieldtype": "Currency", - "options": "currency", - "width": 160 - }, - { - "label": _(str(self.filters.range1) + "-" + str(self.filters.range2)), - "fieldname": scrub(str(self.filters.range1) + "-" + str(self.filters.range2)), - "fieldtype": "Currency", - "options": "currency", - "width": 160 - }, - { - "label": _(str(self.filters.range2) + "-" + str(self.filters.range3)), - "fieldname": scrub(str(self.filters.range2) + "-" + str(self.filters.range3)), - "fieldtype": "Currency", - "options": "currency", - "width": 160 - }, - { - "label": _(str(self.filters.range3) + "-" + str(self.filters.range4)), - "fieldname": scrub(str(self.filters.range3) + "-" + str(self.filters.range4)), - "fieldtype": "Currency", - "options": "currency", - "width": 160 - }, - { - "label": _(str(self.filters.range4) + _("-Above")), - "fieldname": scrub(str(self.filters.range4) + _("-Above")), - "fieldtype": "Currency", - "options": "currency", - "width": 160 - } - ] - - if args.get("party_type") == "Customer": - columns += [{ - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Link", - "options": "Territory", - "width": 80 - }, - { - "label": _("Customer Group"), - "fieldname": "customer_group", - "fieldtype": "Link", - "options": "Customer Group", - "width": 80 - }, - { - "label": _("Sales Person"), - "fieldtype": "Data", - "fieldname": "sales_person", - "width": 120, - }] - - if args.get("party_type") == "Supplier": - columns += [{ - "label": _("Supplier Group"), - "fieldname": "supplier_group", - "fieldtype": "Link", - "options": "Supplier Group", - "width": 80 - }] - - columns.append({ - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "width": 80 - }) - - return columns - - def get_data(self, party_naming_by, args): - data = [] - - partywise_total = self.get_partywise_total(party_naming_by, args) - - partywise_advance_amount = get_partywise_advanced_payment_amount(args.get("party_type"), - self.filters.get("report_date")) or {} - for party, party_dict in iteritems(partywise_total): - row = [party] - - if party_naming_by == "Naming Series": - row += [self.get_party_name(args.get("party_type"), party)] - - row += [partywise_advance_amount.get(party, 0)] - - paid_amt = 0 - if party_dict.paid_amt > 0: - paid_amt = flt(party_dict.paid_amt - partywise_advance_amount.get(party, 0)) - - row += [ - party_dict.invoiced_amt, paid_amt, party_dict.credit_amt, party_dict.outstanding_amt, - party_dict.range1, party_dict.range2, party_dict.range3, party_dict.range4, party_dict.range5 - ] - - if args.get("party_type") == "Customer": - row += [self.get_territory(party), self.get_customer_group(party), ", ".join(set(party_dict.sales_person))] - if args.get("party_type") == "Supplier": - row += [self.get_supplier_group(party)] - - row.append(party_dict.currency) - data.append(row) - - return data - - def get_partywise_total(self, party_naming_by, args): - party_total = frappe._dict() - for d in self.get_voucherwise_data(party_naming_by, args): - party_total.setdefault(d.party, - frappe._dict({ - "invoiced_amt": 0, - "paid_amt": 0, - "credit_amt": 0, - "outstanding_amt": 0, - "range1": 0, - "range2": 0, - "range3": 0, - "range4": 0, - "range5": 0, - "sales_person": [] - }) - ) - for k in list(party_total[d.party]): - if k not in ["currency", "sales_person"]: - party_total[d.party][k] += flt(d.get(k, 0)) - - party_total[d.party].currency = d.currency - - if d.sales_person: - party_total[d.party].sales_person.append(d.sales_person) - - return party_total - - def get_voucherwise_data(self, party_naming_by, args): - voucherwise_data = ReceivablePayableReport(self.filters).run(args)[1] - - cols = ["posting_date", "party"] - - if party_naming_by == "Naming Series": - cols += ["party_name"] - - if args.get("party_type") == 'Customer': - cols += ["contact"] - - cols += ["voucher_type", "voucher_no", "due_date"] - - if args.get("party_type") == "Supplier": - cols += ["bill_no", "bill_date"] - - cols += ["invoiced_amt", "paid_amt", "credit_amt", - "outstanding_amt", "age", "range1", "range2", "range3", "range4", "range5", "currency", "pdc/lc_date", "pdc/lc_ref", - "pdc/lc_amount"] - - if args.get("party_type") == "Supplier": - cols += ["supplier_group", "remarks"] - if args.get("party_type") == "Customer": - cols += ["po_no", "do_no", "territory", "customer_group", "sales_person", "remarks"] - - return self.make_data_dict(cols, voucherwise_data) - - def make_data_dict(self, cols, data): - data_dict = [] - for d in data: - data_dict.append(frappe._dict(zip(cols, d))) - - return data_dict def execute(filters=None): args = { @@ -241,3 +16,119 @@ def execute(filters=None): } return AccountsReceivableSummary(filters).run(args) + +class AccountsReceivableSummary(ReceivablePayableReport): + def run(self, args): + self.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]) + self.get_columns() + self.get_data(args) + return self.columns, self.data + + def get_data(self, args): + self.data = [] + + self.receivables = ReceivablePayableReport(self.filters).run(args)[1] + + self.get_party_total(args) + + party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, + self.filters.report_date) or {} + + for party, party_dict in iteritems(self.party_total): + row = frappe._dict() + + row.party = party + if self.party_naming_by == "Naming Series": + row.party_name = frappe.get_cached_value(self.party_type, party, [self.party_type + "_name"]) + + row.update(party_dict) + + # Advance against party + row.advance = party_advance_amount.get(party, 0) + + # In AR/AP, advance shown in paid columns, + # but in summary report advance shown in separate column + row.paid -= row.advance + + self.data.append(row) + + def get_party_total(self, args): + self.party_total = frappe._dict() + + for d in self.receivables: + self.init_party_total(d) + + # Add all amount columns + for k in list(self.party_total[d.party]): + if k not in ["currency", "sales_person"]: + + self.party_total[d.party][k] += d.get(k, 0.0) + + # set territory, customer_group, sales person etc + self.set_party_details(d) + + def init_party_total(self, row): + self.party_total.setdefault(row.party, frappe._dict({ + "invoiced": 0.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 0.0, + "range1": 0.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "sales_person": [] + })) + + def set_party_details(self, row): + self.party_total[row.party].currency = row.currency + + for key in ('territory', 'customer_group', 'supplier_group'): + if row.get(key): + self.party_total[row.party][key] = row.get(key) + + if row.sales_person: + self.party_total[row.party].sales_person.append(row.sales_person) + + def get_columns(self): + self.columns = [] + self.add_column(label=_(self.party_type), fieldname='party', + fieldtype='Link', options=self.party_type, width=180) + + if self.party_naming_by == "Naming Series": + self.add_column(_('{0} Name').format(self.party_type), + fieldname = 'party_name', fieldtype='Data') + + credit_debit_label = "Credit Note" if self.party_type == 'Customer' else "Debit Note" + + self.add_column(_('Advance Amount'), fieldname='advance') + self.add_column(_('Invoiced Amount'), fieldname='invoiced') + self.add_column(_('Paid Amount'), fieldname='paid') + self.add_column(_(credit_debit_label), fieldname='credit_note') + self.add_column(_('Outstanding Amount'), fieldname='outstanding') + + self.setup_ageing_columns() + + if self.party_type == "Customer": + self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', + options='Territory') + self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', + options='Customer Group') + if self.filters.show_sales_person: + self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + else: + self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', + options='Supplier Group') + + self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', + options='Currency', width=80) + + def setup_ageing_columns(self): + for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), + "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), + "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), + "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): + self.add_column(label=label, fieldname='range' + str(i+1)) \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index d0b4ba0e65..3c9c3cca03 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -372,7 +372,7 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil return frappe.db.sql(""" select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s - and `tabDynamic Link`.link_doctype = 'Customer' and `tabContact`.is_primary_contact = 1 + and `tabDynamic Link`.link_doctype = 'Customer' and `tabContact`.name like %(txt)s """, { 'customer': customer, @@ -384,7 +384,7 @@ def get_customer_primary_address(doctype, txt, searchfield, start, page_len, fil return frappe.db.sql(""" select `tabAddress`.name from `tabAddress`, `tabDynamic Link` where `tabAddress`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s - and `tabDynamic Link`.link_doctype = 'Customer' and `tabAddress`.is_primary_address = 1 + and `tabDynamic Link`.link_doctype = 'Customer' and `tabAddress`.name like %(txt)s """, { 'customer': customer,