Server side script report for sales analytics (#15589)
* Sales Analytics Report
* Codacy issue fixes and name column addition
* Minor Fixes
* Minor Changes
* Codacy Issue Fixes
* Codacy Issue Fixes
* Bug Fixes
* Code cleaning and optimization
* Deleted Duplicate code
* Indentation Issue Fixes
* Added Supplier Condition
* Cleaned code and better function naming
* Added report link for sales analytics in selling.py
* fix(patch): Patch to delete old analytics reports
* feat(refactor): Created class and refactored code using object oriented paradigm
* Column condition fix
* Minor condition fix
* Minor fix
* parent child map for purchase analytics
* Minor Fixes in get_periodic_data
* Used dots for filters instead of brackets
* Minor Bug fix in get_period_date_ranges
* Test Cases for Analytics Report
diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py
index 029fdac..94f3102 100644
--- a/erpnext/config/selling.py
+++ b/erpnext/config/selling.py
@@ -185,10 +185,10 @@
"icon": "fa fa-table",
"items": [
{
- "type": "page",
- "name": "sales-analytics",
- "label": _("Sales Analytics"),
- "icon": "fa fa-bar-chart",
+ "type": "report",
+ "is_query_report": True,
+ "name": "Sales Analytics",
+ "doctype": "Sales Order"
},
{
"type": "page",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 9c56948..19857d2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -568,6 +568,10 @@
erpnext.patches.v11_0.add_default_dispatch_notification_template
erpnext.patches.v11_0.add_market_segments
erpnext.patches.v11_0.add_sales_stages
+execute:frappe.delete_doc("Page", "Sales Analytics")
+execute:frappe.delete_doc("Page", "Purchase Analytics")
+execute:frappe.delete_doc("Page", "Stock Analytics")
+execute:frappe.delete_doc("Page", "Production Analytics")
erpnext.patches.v11_0.ewaybill_fields_gst_india
erpnext.patches.v11_0.drop_column_max_days_allowed
erpnext.patches.v11_0.change_healthcare_desktop_icons
diff --git a/erpnext/selling/report/sales_analytics/__init__.py b/erpnext/selling/report/sales_analytics/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/selling/report/sales_analytics/__init__.py
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
new file mode 100644
index 0000000..7c9e3ec
--- /dev/null
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -0,0 +1,129 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Sales Analytics"] = {
+ "filters": [
+ {
+ fieldname: "tree_type",
+ label: __("Tree Type"),
+ fieldtype: "Select",
+ options: ["Customer Group","Customer","Item Group","Item","Territory"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "doc_type",
+ label: __("based_on"),
+ fieldtype: "Select",
+ options: ["Sales Order","Delivery Note","Sales Invoice"],
+ default: "Sales Invoice",
+ reqd: 1
+ },
+ {
+ fieldname: "value_quantity",
+ label: __("Value Or Qty"),
+ fieldtype: "Select",
+ options: [
+ { "value": "Value", "label": __("Value") },
+ { "value": "Quantity", "label": __("Quantity") },
+ ],
+ default: "Value",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_user_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_user_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "range",
+ label: __("Range"),
+ fieldtype: "Select",
+ options: [
+ { "value": "Weekly", "label": __("Weekly") },
+ { "value": "Monthly", "label": __("Monthly") },
+ { "value": "Quarterly", "label": __("Quarterly") },
+ { "value": "Yearly", "label": __("Yearly") }
+ ],
+ default: "Monthly",
+ reqd: 1
+ }
+ ],
+ "formatter": function(value, row, column, data) {
+ if(!value){
+ value = 0
+ }
+ return value;
+ },
+ get_datatable_options(options) {
+ return Object.assign(options, {
+ checkboxColumn: true,
+ events: {
+ onCheckRow: function(data) {
+ row_name = data[2].content;
+ row_values = data.slice(5).map(function (column) {
+ return column.content;
+ })
+
+ entry = {
+ 'name':row_name,
+ 'values':row_values
+ }
+
+ let raw_data = frappe.query_report.chart.data;
+ let new_datasets = raw_data.datasets;
+
+ var found = false;
+
+ for(var i=0; i < new_datasets.length;i++){
+ if(new_datasets[i].name == row_name){
+ found = true;
+ new_datasets.splice(i,1);
+ break;
+ }
+ }
+
+ if(!found){
+ new_datasets.push(entry);
+ }
+
+ let new_data = {
+ labels: raw_data.labels,
+ datasets: new_datasets
+ }
+
+ setTimeout(() => {
+ frappe.query_report.chart.update(new_data)
+ },200)
+
+
+ setTimeout(() => {
+ frappe.query_report.chart.draw(true);
+ }, 800)
+
+ frappe.query_report.raw_chart_data = new_data;
+ },
+ }
+ })
+ },
+}
+
+
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json
new file mode 100644
index 0000000..5c95f28
--- /dev/null
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "creation": "2018-09-21 12:46:29.451048",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2018-09-21 12:46:29.451048",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Sales Analytics",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Order",
+ "report_name": "Sales Analytics",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Maintenance User"
+ },
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Sales Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
new file mode 100644
index 0000000..ef9e666
--- /dev/null
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -0,0 +1,286 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _, scrub
+from frappe.utils import getdate, flt
+from six import iteritems
+from erpnext.accounts.utils import get_fiscal_year
+
+def execute(filters=None):
+ return Analytics(filters).run()
+
+class Analytics(object):
+ def __init__(self, filters=None):
+ self.filters = frappe._dict(filters or {})
+ self.date_field = 'transaction_date' \
+ if self.filters.doc_type in ['Sales Order', 'Purchase Order'] else 'posting_date'
+ self.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ self.get_period_date_ranges()
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+ return self.columns, self.data , None, self.chart
+
+ def get_columns(self):
+ self.columns =[{
+ "label": _(self.filters.tree_type + " ID"),
+ "options": self.filters.tree_type,
+ "fieldname": "entity",
+ "fieldtype": "Link",
+ "width": 140
+ }]
+ if self.filters.tree_type in ["Customer", "Supplier", "Item"]:
+ self.columns.append({
+ "label": _(self.filters.tree_type + " Name"),
+ "fieldname": "entity_name",
+ "fieldtype": "Data",
+ "width": 140
+ })
+ for dummy, end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ self.columns.append({
+ "label": _(period),
+ "fieldname": scrub(period),
+ "fieldtype": "Float",
+ "width": 120
+ })
+
+ self.columns.append({
+ "label": _("Total"),
+ "fieldname": "total",
+ "fieldtype": "Float",
+ "width": 120
+ })
+
+ def get_data(self):
+ if self.filters.tree_type in ["Customer", "Supplier"]:
+ self.get_sales_transactions_based_on_customers_or_suppliers()
+ self.get_rows()
+
+ elif self.filters.tree_type == 'Item':
+ self.get_sales_transactions_based_on_items()
+ self.get_rows()
+
+ elif self.filters.tree_type in ["Customer Group", "Supplier Group", "Territory"]:
+ self.get_sales_transactions_based_on_customer_or_territory_group()
+ self.get_rows_by_group()
+
+ elif self.filters.tree_type == 'Item Group':
+ self.get_sales_transactions_based_on_item_group()
+ self.get_rows_by_group()
+
+ def get_sales_transactions_based_on_customers_or_suppliers(self):
+ if self.filters["value_quantity"] == 'Value':
+ value_field = "base_net_total as value_field"
+ else:
+ value_field = "total_qty as value_field"
+
+ if self.filters.tree_type == 'Customer':
+ entity = "customer as entity"
+ entity_name = "customer_name as entity_name"
+ else:
+ entity = "supplier as entity"
+ entity_name = "supplier_name as entity_name"
+
+ self.entries = frappe.get_all(self.filters.doc_type,
+ fields=[entity, entity_name, value_field, self.date_field],
+ filters = {
+ "docstatus": 1,
+ "company": self.filters.company,
+ self.date_field: ('between', [self.filters.from_date, self.filters.to_date])
+ }
+ )
+
+ self.entity_names = {}
+ for d in self.entries:
+ self.entity_names.setdefault(d.entity, d.entity_name)
+
+ def get_sales_transactions_based_on_items(self):
+
+ if self.filters["value_quantity"] == 'Value':
+ value_field = 'base_amount'
+ else:
+ value_field = 'qty'
+
+ self.entries = frappe.db.sql("""
+ select i.item_code as entity, i.item_name as entity_name, i.{value_field} as value_field, s.{date_field}
+ from `tab{doctype} Item` i , `tab{doctype}` s
+ where s.name = i.parent and i.docstatus = 1 and s.company = %s
+ and s.{date_field} between %s and %s
+ """
+ .format(date_field=self.date_field, value_field = value_field, doctype=self.filters.doc_type),
+ (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1)
+
+ self.entity_names = {}
+ for d in self.entries:
+ self.entity_names.setdefault(d.entity, d.entity_name)
+
+ def get_sales_transactions_based_on_customer_or_territory_group(self):
+ if self.filters["value_quantity"] == 'Value':
+ value_field = "base_net_total as value_field"
+ else:
+ value_field = "total_qty as value_field"
+
+ if self.filters.tree_type == 'Customer Group':
+ entity_field = 'customer_group as entity'
+ elif self.filters.tree_type == 'Supplier Group':
+ entity_field = "supplier as entity"
+ self.get_supplier_parent_child_map()
+ else:
+ entity_field = "territory as entity"
+
+ self.entries = frappe.get_all(self.filters.doc_type,
+ fields=[entity_field, value_field, self.date_field],
+ filters = {
+ "docstatus": 1,
+ "company": self.filters.company,
+ self.date_field: ('between', [self.filters.from_date, self.filters.to_date])
+ }
+ )
+ self.get_groups()
+
+ def get_sales_transactions_based_on_item_group(self):
+ if self.filters["value_quantity"] == 'Value':
+ value_field = "base_amount"
+ else:
+ value_field = "qty"
+
+ self.entries = frappe.db.sql("""
+ select i.item_group as entity, i.{value_field} as value_field, s.{date_field}
+ from `tab{doctype} Item` i , `tab{doctype}` s
+ where s.name = i.parent and i.docstatus = 1 and s.company = %s
+ and s.{date_field} between %s and %s
+ """.format(date_field=self.date_field, value_field = value_field, doctype=self.filters.doc_type),
+ (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1)
+
+ self.get_groups()
+
+ def get_rows(self):
+ self.data=[]
+ self.get_periodic_data()
+
+ for entity, period_data in iteritems(self.entity_periodic_data):
+ row = {
+ "entity": entity,
+ "entity_name": self.entity_names.get(entity)
+ }
+ total = 0
+ for dummy, end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ amount = flt(period_data.get(period, 0.0))
+ row[scrub(period)] = amount
+ total += amount
+
+ row["total"] = total
+ self.data.append(row)
+
+ def get_rows_by_group(self):
+ self.get_periodic_data()
+ out = []
+
+ for d in reversed(self.group_entries):
+ row = {
+ "entity": d.name,
+ "indent": self.depth_map.get(d.name)
+ }
+ total = 0
+ for dummy, end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ amount = flt(self.entity_periodic_data.get(d.name, {}).get(period, 0.0))
+ row[scrub(period)] = amount
+ if d.parent:
+ self.entity_periodic_data.setdefault(d.parent, frappe._dict()).setdefault(period, 0.0)
+ self.entity_periodic_data[d.parent][period] += amount
+ total += amount
+ row["total"] = total
+ out = [row] + out
+ self.data = out
+
+ def get_periodic_data(self):
+ self.entity_periodic_data = frappe._dict()
+
+ for d in self.entries:
+ if self.filters.tree_type == "Supplier Group":
+ d.entity = self.parent_child_map.get(d.entity)
+ period = self.get_period(d.get(self.date_field))
+ self.entity_periodic_data.setdefault(d.entity, frappe._dict()).setdefault(period, 0.0)
+ self.entity_periodic_data[d.entity][period] += flt(d.value_field)
+
+ def get_period(self, posting_date):
+ if self.filters.range == 'Weekly':
+ period = "Week " + str(posting_date.isocalendar()[1])
+ elif self.filters.range == 'Monthly':
+ period = self.months[posting_date.month - 1]
+ elif self.filters.range == 'Quarterly':
+ period = "Quarter " + str(((posting_date.month-1)//3)+1)
+ else:
+ year = get_fiscal_year(posting_date, company=self.filters.company)
+ period = str(year[2])
+
+ return period
+
+ def get_period_date_ranges(self):
+ from dateutil.relativedelta import relativedelta
+ from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
+
+ increment = {
+ "Monthly": 1,
+ "Quarterly": 3,
+ "Half-Yearly": 6,
+ "Yearly": 12
+ }.get(self.filters.range, 1)
+
+ self.periodic_daterange = []
+ for dummy in range(1, 53, increment):
+ if self.filters.range == "Weekly":
+ period_end_date = from_date + relativedelta(days=6)
+ else:
+ period_end_date = from_date + relativedelta(months=increment, days=-1)
+
+ if period_end_date > to_date:
+ period_end_date = to_date
+ self.periodic_daterange.append([from_date, period_end_date])
+
+ from_date = period_end_date + relativedelta(days=1)
+ if period_end_date == to_date:
+ break
+
+ def get_groups(self):
+ if self.filters.tree_type == "Territory":
+ parent = 'parent_territory'
+ if self.filters.tree_type == "Customer Group":
+ parent = 'parent_customer_group'
+ if self.filters.tree_type == "Item Group":
+ parent = 'parent_item_group'
+ if self.filters.tree_type == "Supplier Group":
+ parent = 'parent_supplier_group'
+
+ self.depth_map = frappe._dict()
+
+ self.group_entries = frappe.db.sql("""select name, lft, rgt , {parent} as parent
+ from `tab{tree}` order by lft"""
+ .format(tree=self.filters.tree_type, parent=parent), as_dict=1)
+
+ for d in self.group_entries:
+ if d.parent:
+ self.depth_map.setdefault(d.name, self.depth_map.get(d.parent) + 1)
+ else:
+ self.depth_map.setdefault(d.name, 0)
+
+ def get_supplier_parent_child_map(self):
+ self.parent_child_map = frappe._dict(frappe.db.sql(""" select name, supplier_group from `tabSupplier`"""))
+
+ def get_chart_data(self):
+ labels = [d.get("label") for d in self.columns[3:]]
+ self.chart = {
+ "data": {
+ 'labels': labels,
+ 'datasets':[
+ ]
+ },
+ "type": "line"
+ }
\ No newline at end of file
diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py
new file mode 100644
index 0000000..c9a00fa
--- /dev/null
+++ b/erpnext/selling/report/sales_analytics/test_analytics.py
@@ -0,0 +1,250 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import frappe.defaults
+import unittest
+from erpnext.selling.report.sales_analytics.sales_analytics import execute
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+
+class TestAnalytics(unittest.TestCase):
+
+ def tearDown(self):
+ frappe.db.sql(""" DELETE FROM `tabSales Order` """)
+
+ def test_by_entity(self):
+ create_sales_order()
+
+ filters = {
+ 'doc_type': 'Sales Order',
+ 'range': 'Monthly',
+ 'to_date': '2018-03-31',
+ 'tree_type': 'Customer',
+ 'company': '_Test Company',
+ 'from_date': '2017-04-01',
+ 'value_quantity': 'Value'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ "entity": "_Test Customer 1",
+ "entity_name": "_Test Customer 1",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 2000.0,
+ "mar": 0.0,
+ "total":2000.0
+ },
+ {
+ "entity": "_Test Customer 3",
+ "entity_name": "_Test Customer 3",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 2000.0,
+ "jul": 1000.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total": 3000.0
+ },
+ {
+ "entity": "_Test Customer 2",
+ "entity_name": "_Test Customer 2",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 1500.0,
+ "oct": 1000.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total":2500.0
+ }
+ ]
+ self.assertEqual(expected_data, report[1])
+
+ def test_by_group(self):
+ create_sales_order()
+
+ filters = {
+ 'doc_type': 'Sales Order',
+ 'range': 'Monthly',
+ 'to_date': '2018-03-31',
+ 'tree_type': 'Customer Group',
+ 'company': '_Test Company',
+ 'from_date': '2017-04-01',
+ 'value_quantity': 'Value'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ "entity": "All Customer Groups",
+ "indent": 0,
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 2000.0,
+ "jul": 1000.0,
+ "aug": 0.0,
+ "sep": 1500.0,
+ "oct": 1000.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 2000.0,
+ "mar": 0.0,
+ "total":7500.0
+ },
+ {
+ "entity": "Individual",
+ "indent": 1,
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total": 0.0
+ },
+ {
+ "entity": "_Test Customer Group",
+ "indent": 1,
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total":0.0
+ },
+ {
+ "entity": "_Test Customer Group 1",
+ "indent": 1,
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total":0.0
+ }
+ ]
+ self.assertEqual(expected_data, report[1])
+
+ def test_by_quantity(self):
+ create_sales_order()
+
+ filters = {
+ 'doc_type': 'Sales Order',
+ 'range': 'Monthly',
+ 'to_date': '2018-03-31',
+ 'tree_type': 'Customer',
+ 'company': '_Test Company',
+ 'from_date': '2017-04-01',
+ 'value_quantity': 'Quantity'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ "entity": "_Test Customer 1",
+ "entity_name": "_Test Customer 1",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 20.0,
+ "mar": 0.0,
+ "total":20.0
+ },
+ {
+ "entity": "_Test Customer 3",
+ "entity_name": "_Test Customer 3",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 20.0,
+ "jul": 10.0,
+ "aug": 0.0,
+ "sep": 0.0,
+ "oct": 0.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total": 30.0
+ },
+ {
+ "entity": "_Test Customer 2",
+ "entity_name": "_Test Customer 2",
+ "apr": 0.0,
+ "may": 0.0,
+ "jun": 0.0,
+ "jul": 0.0,
+ "aug": 0.0,
+ "sep": 15.0,
+ "oct": 10.0,
+ "nov": 0.0,
+ "dec": 0.0,
+ "jan": 0.0,
+ "feb": 0.0,
+ "mar": 0.0,
+ "total":25.0
+ }
+ ]
+ self.assertEqual(expected_data, report[1])
+
+def create_sales_order():
+ frappe.set_user("Administrator")
+
+ make_sales_order(qty=10, customer = "_Test Customer 1", transaction_date='2018-02-10')
+ make_sales_order(qty=10, customer = "_Test Customer 1", transaction_date='2018-02-15')
+ make_sales_order(qty=15, customer = "_Test Customer 2", transaction_date='2017-09-23')
+ make_sales_order(qty=10, customer = "_Test Customer 2", transaction_date='2017-10-10')
+ make_sales_order(qty=20, customer = "_Test Customer 3", transaction_date='2017-06-15')
+ make_sales_order(qty=10, customer = "_Test Customer 3", transaction_date='2017-07-10')