Merge pull request #29137 from ruthra-kumar/payment_terms_report
feat: Payment Terms Status report
This commit is contained in:
commit
d251bec380
@ -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);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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
|
@ -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)
|
Loading…
Reference in New Issue
Block a user