Merge pull request #29137 from ruthra-kumar/payment_terms_report

feat: Payment Terms Status report
This commit is contained in:
Deepesh Garg 2022-02-16 14:15:24 +05:30 committed by GitHub
commit d251bec380
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 525 additions and 0 deletions

View File

@ -0,0 +1,84 @@
// Copyright (c) 2022, 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 = "<span style='color:green;'>" + formatted_value + "</span>"
}
return formatted_value;
}
else if (column.fieldname == 'status' && value == 'Completed'){
return "<span style='color:green;'>" + default_formatter(value, row, column, data) + "</span>";
}
return default_formatter(value, row, column, data);
},
};

View File

@ -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"
}
]
}

View File

@ -0,0 +1,205 @@
# 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",
},
{
"label": _("Posting Date"),
"fieldname": "submitted",
"fieldtype": "Date",
},
{
"label": _("Payment Term"),
"fieldname": "payment_term",
"fieldtype": "Data",
},
{
"label": _("Description"),
"fieldname": "description",
"fieldtype": "Data",
},
{
"label": _("Due Date"),
"fieldname": "due_date",
"fieldtype": "Date",
},
{
"label": _("Invoice Portion"),
"fieldname": "invoice_portion",
"fieldtype": "Percent",
},
{
"label": _("Payment Amount"),
"fieldname": "base_payment_amount",
"fieldtype": "Currency",
"options": "currency",
},
{
"label": _("Paid Amount"),
"fieldname": "paid_amount",
"fieldtype": "Currency",
"options": "currency",
},
{
"label": _("Invoices"),
"fieldname": "invoices",
"fieldtype": "Link",
"options": "Sales Invoice",
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Currency",
"hidden": 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.base_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_grand_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, 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')
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
so.invoices += "," + inv.invoice
so.status = "Completed"
break
else:
so.paid_amount += inv.invoice_amount
inv.invoice_amount = 0
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.base_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, filters)
prepare_chart(sales_orders)
data = sales_orders
message = []
chart = prepare_chart(sales_orders)
return columns, data, message, chart

View File

@ -0,0 +1,198 @@
import datetime
import frappe
from frappe.utils import add_days
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 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")
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()
self.template = template
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",
delivery_date=add_days("2021-06-15", -30),
item=item.item_code,
qty=10,
rate=100000,
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.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],
}
)
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": "INR",
"base_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,
"currency": "INR",
"base_payment_amount": 500000.0,
"paid_amount": 100000.0,
"invoices": ","+sinv.name,
},
]
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)