From 1bac7930834d6f688950e836c45305a62e7ecb3f Mon Sep 17 00:00:00 2001 From: ruthra Date: Tue, 4 Jan 2022 15:53:41 +0530 Subject: [PATCH 01/10] feat: Payment Terms Status report - calculate status at runtime for payment terms based on invoices - invoices are used in FIFO method --- .../__init__.py | 0 .../payment_terms_status_for_sales_order.js | 84 +++++++ .../payment_terms_status_for_sales_order.json | 38 ++++ .../payment_terms_status_for_sales_order.py | 211 ++++++++++++++++++ ...st_payment_terms_status_for_sales_order.py | 119 ++++++++++ 5 files changed, 452 insertions(+) create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js new file mode 100644 index 0000000000..0450631a3b --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -0,0 +1,84 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"sales_order", + "label": __("Sales Order"), + "fieldtype": "MultiSelectList", + "width": 100, + "options": "Sales Order", + "get_data": function(txt) { + return frappe.db.get_link_options("Sales Order", txt, this.filters()); + }, + "filters": () => { + return { + docstatus: 1, + payment_terms_template: ['not in', ['']], + company: frappe.query_report.get_filter_value("company"), + transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + } + }, + on_change: function(){ + frappe.query_report.refresh(); + } + } + ] + + return filters; +} + +frappe.query_reports["Payment Terms Status for Sales Order"] = { + "filters": get_filters(), + "formatter": function(value, row, column, data, default_formatter){ + if(column.fieldname == 'invoices' && value) { + invoices = value.split(','); + const invoice_formatter = (prev_value, curr_value) => { + if(prev_value != "") { + return prev_value + ", " + default_formatter(curr_value, row, column, data); + } + else { + return default_formatter(curr_value, row, column, data); + } + } + return invoices.reduce(invoice_formatter, "") + } + else if (column.fieldname == 'paid_amount' && value){ + formatted_value = default_formatter(value, row, column, data); + if(value > 0) { + formatted_value = "" + formatted_value + "" + } + return formatted_value; + } + else if (column.fieldname == 'status' && value == 'Completed'){ + return "" + default_formatter(value, row, column, data) + ""; + } + + return default_formatter(value, row, column, data); + }, + +}; diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json new file mode 100644 index 0000000000..850fa4dc47 --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-12-28 10:39:34.533964", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-30 10:42:06.058457", + "modified_by": "Administrator", + "module": "Selling", + "name": "Payment Terms Status for Sales Order", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Payment Terms Status for Sales Order", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py new file mode 100644 index 0000000000..aa2f757218 --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -0,0 +1,211 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _, qb, query_builder +from frappe.query_builder import functions + + +def get_columns(): + columns = [ + { + "label": _("Sales Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Sales Order", + "read_only": 1, + }, + { + "label": _("Submitted"), + "fieldname": "submitted", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Payment Term"), + "fieldname": "payment_term", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Due Date"), + "fieldname": "due_date", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Invoice Portion"), + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "read_only": 1, + }, + { + "label": _("Payment Amount"), + "fieldname": "payment_amount", + "fieldtype": "Currency", + "read_only": 1, + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "read_only": 1 + }, + { + "label": _("Invoices"), + "fieldname": "invoices", + "fieldtype": "Link", + "options": "Sales Invoice", + "read_only": 1, + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "read_only": 1 + } + ] + return columns + + +def get_conditions(filters): + """ + Convert filter options to conditions used in query + """ + filters = frappe._dict(filters) if filters else frappe._dict({}) + conditions = frappe._dict({}) + + conditions.company = filters.company or frappe.defaults.get_user_default("company") + conditions.end_date = filters.period_end_date or frappe.utils.today() + conditions.start_date = filters.period_start_date or frappe.utils.add_months( + conditions.end_date, -1 + ) + conditions.sales_order = filters.sales_order or [] + + return conditions + + +def get_so_with_invoices(filters): + """ + Get Sales Order with payment terms template with their associated Invoices + """ + sorders = [] + + so = qb.DocType("Sales Order") + ps = qb.DocType("Payment Schedule") + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + + conditions = get_conditions(filters) + query_so = ( + qb.from_(so) + .join(ps) + .on(ps.parent == so.name) + .select( + so.name, + so.transaction_date.as_("submitted"), + ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), + ps.payment_term, + ps.description, + ps.due_date, + ps.invoice_portion, + ps.payment_amount, + ps.paid_amount, + ) + .where( + (so.docstatus == 1) + & (so.payment_terms_template != "NULL") + & (so.company == conditions.company) + & (so.transaction_date[conditions.start_date : conditions.end_date]) + ) + .orderby(so.name, so.transaction_date, ps.due_date) + ) + + if conditions.sales_order != []: + query_so = query_so.where(so.name.isin(conditions.sales_order)) + + sorders = query_so.run(as_dict=True) + + invoices = [] + if sorders != []: + soi = qb.DocType("Sales Order Item") + si = qb.DocType("Sales Invoice") + sii = qb.DocType("Sales Invoice Item") + query_inv = ( + qb.from_(sii) + .right_join(si) + .on(si.name == sii.parent) + .inner_join(soi) + .on(soi.name == sii.so_detail) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) + .groupby(sii.parent) + ) + invoices = query_inv.run(as_dict=True) + + return sorders, invoices + + +def set_payment_terms_statuses(sales_orders, invoices): + """ + compute status for payment terms with associated sales invoice using FIFO + """ + + for so in sales_orders: + for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: + if so.payment_amount - so.paid_amount > 0: + amount = so.payment_amount - so.paid_amount + if inv.invoice_amount >= amount: + inv.invoice_amount -= amount + so.paid_amount += amount + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Completed" + break + else: + so.paid_amount += inv.invoice_amount + inv.invoice_amount = 0 + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Partly Paid" + + return sales_orders, invoices + + +def prepare_chart(s_orders): + if len(set([x.name for x in s_orders])) == 1: + chart = { + "data": { + "labels": [term.payment_term for term in s_orders], + "datasets": [ + {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + ], + }, + "type": "bar", + } + return chart + + +def execute(filters=None): + columns = get_columns() + sales_orders, so_invoices = get_so_with_invoices(filters) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + + prepare_chart(sales_orders) + + data = sales_orders + message = [] + chart = prepare_chart(sales_orders) + + return columns, data, message, chart diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py new file mode 100644 index 0000000000..e9dba84f3a --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -0,0 +1,119 @@ +import datetime +import unittest + +import frappe +from frappe import qb +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( + execute, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] + + +class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): + def test_payment_terms_status(self): + # disable Must be a whole number + nos = frappe.get_doc("UOM", "Nos") + nos.must_be_whole_number = 0 + nos.save() + + template = None + if frappe.db.exists("Payment Terms Template", "_Test 50-50"): + template = frappe.get_doc("Payment Terms Template", "_Test 50-50") + else: + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 15 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 30 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date="2021-06-15", + delivery_date=add_days("2021-06-15", -30), + item=item.item_code, + qty=1, + rate=1000000, + po_no=54321, + do_not_save=True, + ) + so.payment_terms_template = template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + # sinv.posting_date = "2021-06-29" + sinv.items[0].qty *= 0.60 + sinv.insert() + sinv.submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # revert changes to Nos + nos.must_be_whole_number = 1 + nos.save() + + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 500000.0, + "invoices": sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 100000.0, + "invoices": sinv.name, + }, + ] + + self.assertEqual(data, expected_value) From 9f1e68801d527628551984402fd0c06e401084d8 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:11:19 +0530 Subject: [PATCH 02/10] test: fix failing test case payment terms status --- .../test_payment_terms_status_for_sales_order.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index e9dba84f3a..19c01f2d43 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -21,8 +21,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): # disable Must be a whole number nos = frappe.get_doc("UOM", "Nos") - nos.must_be_whole_number = 0 - nos.save() + nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -62,9 +61,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): item=item.item_code, qty=1, rate=1000000, - po_no=54321, do_not_save=True, ) + so.po_no = "" so.payment_terms_template = template.name so.save() so.submit() @@ -86,8 +85,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ) # revert changes to Nos - nos.must_be_whole_number = 1 - nos.save() + nos.db_set("must_be_whole_number", 1, commit=True) expected_value = [ { From edd980acdc9e51f74eb6b70a793ae17b2e827710 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:43:20 +0530 Subject: [PATCH 03/10] refactor: remove unused imports --- .../test_payment_terms_status_for_sales_order.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 19c01f2d43..4f27a5683d 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,11 +1,8 @@ import datetime -import unittest import frappe -from frappe import qb from frappe.utils import add_days -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( From 4535a7a301f76fa3b867902f19e806dcb01bdb75 Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:46:32 +0530 Subject: [PATCH 04/10] test: qty and rate changed to remove need for fractional Nos --- .../test_payment_terms_status_for_sales_order.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 4f27a5683d..5d6e91e8a5 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -16,9 +16,6 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): - # disable Must be a whole number - nos = frappe.get_doc("UOM", "Nos") - nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -56,8 +53,8 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), item=item.item_code, - qty=1, - rate=1000000, + qty=10, + rate=100000, do_not_save=True, ) so.po_no = "" @@ -67,8 +64,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) - # sinv.posting_date = "2021-06-29" - sinv.items[0].qty *= 0.60 + sinv.items[0].qty = 6 sinv.insert() sinv.submit() @@ -81,9 +77,6 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) - # revert changes to Nos - nos.db_set("must_be_whole_number", 1, commit=True) - expected_value = [ { "name": so.name, From 4284017e9de3b033156ca6947665bc99f0daefc3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:26:01 +0530 Subject: [PATCH 05/10] fix: Copyright info --- .../payment_terms_status_for_sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0450631a3b..0e36b3fe3d 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -1,4 +1,4 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt /* eslint-disable */ From bc244d074062d23be99922a370564bba13e15890 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Feb 2022 18:53:08 +0530 Subject: [PATCH 06/10] refactor: currency field and code cleanup --- .../payment_terms_status_for_sales_order.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index aa2f757218..4eafa9b2ef 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -13,62 +13,60 @@ def get_columns(): "fieldname": "name", "fieldtype": "Link", "options": "Sales Order", - "read_only": 1, }, { - "label": _("Submitted"), + "label": _("Posting Date"), "fieldname": "submitted", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Payment Term"), "fieldname": "payment_term", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Description"), "fieldname": "description", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Due Date"), "fieldname": "due_date", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Invoice Portion"), "fieldname": "invoice_portion", "fieldtype": "Percent", - "read_only": 1, }, { "label": _("Payment Amount"), "fieldname": "payment_amount", "fieldtype": "Currency", - "read_only": 1, + "options": "currency", }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", - "read_only": 1 + "options": "currency", }, { "label": _("Invoices"), "fieldname": "invoices", "fieldtype": "Link", "options": "Sales Invoice", - "read_only": 1, }, { "label": _("Status"), "fieldname": "status", "fieldtype": "Data", - "read_only": 1 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "hidden": 1 } ] return columns @@ -152,12 +150,13 @@ def get_so_with_invoices(filters): return sorders, invoices -def set_payment_terms_statuses(sales_orders, invoices): +def set_payment_terms_statuses(sales_orders, invoices, filters): """ compute status for payment terms with associated sales invoice using FIFO """ for so in sales_orders: + so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.payment_amount - so.paid_amount > 0: amount = so.payment_amount - so.paid_amount @@ -200,7 +199,7 @@ def prepare_chart(s_orders): def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) - sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters) prepare_chart(sales_orders) From 85ed0fb8d6ef45197bfef4a71cb8f02355d61930 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:06 +0530 Subject: [PATCH 07/10] fix: default to company currency in report output --- .../payment_terms_status_for_sales_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 4eafa9b2ef..d0902e111a 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -41,7 +41,7 @@ def get_columns(): }, { "label": _("Payment Amount"), - "fieldname": "payment_amount", + "fieldname": "base_payment_amount", "fieldtype": "Currency", "options": "currency", }, @@ -113,7 +113,7 @@ def get_so_with_invoices(filters): ps.description, ps.due_date, ps.invoice_portion, - ps.payment_amount, + ps.base_payment_amount, ps.paid_amount, ) .where( @@ -141,7 +141,7 @@ def get_so_with_invoices(filters): .on(si.name == sii.parent) .inner_join(soi) .on(soi.name == sii.so_detail) - .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount")) .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) .groupby(sii.parent) ) @@ -158,8 +158,8 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: - if so.payment_amount - so.paid_amount > 0: - amount = so.payment_amount - so.paid_amount + if so.base_payment_amount - so.paid_amount > 0: + amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount @@ -187,7 +187,7 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, ], }, From a4b8d673232fd313396788ef745e67572c235dcc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:41 +0530 Subject: [PATCH 08/10] refactor: create invoices list without if else --- .../payment_terms_status_for_sales_order.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index d0902e111a..e6a56eea31 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -157,25 +157,20 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Completed" break else: so.paid_amount += inv.invoice_amount inv.invoice_amount = 0 - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Partly Paid" return sales_orders, invoices From 49fdc6c52e9752362b754f1615ca77ac9e09b418 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:20:29 +0530 Subject: [PATCH 09/10] test: refactor and fix failing test case --- ...st_payment_terms_status_for_sales_order.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 5d6e91e8a5..ee6cee3be8 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -15,8 +15,8 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): - def test_payment_terms_status(self): - + def create_payment_terms_template(self): + # create template for 50-50 payments template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): template = frappe.get_doc("Payment Terms Template", "_Test 50-50") @@ -46,8 +46,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) template.insert() + self.template = template - # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + def test_payment_terms_status(self): + self.create_payment_terms_template() item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", @@ -58,16 +60,19 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): do_not_save=True, ) so.po_no = "" - so.payment_terms_template = template.name + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name so.save() so.submit() # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) + sinv.taxes_and_charges = "" + sinv.taxes = "" sinv.items[0].qty = 6 sinv.insert() sinv.submit() - columns, data, message, chart = execute( { "company": "_Test Company", @@ -86,9 +91,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, { "name": so.name, @@ -98,10 +104,13 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, ] + self.assertEqual(data, expected_value) + self.assertEqual(data, expected_value) From 48f37c76594fad1cd64cd44b7126d6ef1ddd5bd1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:21:08 +0530 Subject: [PATCH 10/10] test: added test for alternate currency - Sales Order and Invoice will be submitted in USD with exchange rate of 70 with the default company currency - Report will display in defauly company currency --- ...st_payment_terms_status_for_sales_order.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index ee6cee3be8..cad41e1dc0 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -112,5 +112,87 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ] self.assertEqual(data, expected_value) + def create_exchange_rate(self, date): + # make an entry in Currency Exchange list. serves as a static exchange rate + if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + return + else: + doc = frappe.get_doc({ + 'doctype': "Currency Exchange", + 'date': date, + 'from_currency': 'USD', + 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), + 'exchange_rate': 70, + 'for_buying': True, + 'for_selling': True + }) + doc.insert() + def test_alternate_currency(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + self.create_exchange_rate(transaction_date) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + currency="USD", + delivery_date=add_days(transaction_date, -30), + item=item.item_code, + qty=10, + rate=10000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + sinv.currency = "USD" + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.items[0].qty = 6 + sinv.insert() + sinv.submit() + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # report defaults to company currency. + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 3500000.0, + "invoices": ","+sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 700000.0, + "invoices": ","+sinv.name, + }, + ] self.assertEqual(data, expected_value)