mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 07:02:54 +00:00
feat: Selling Desk, Dashboard and Onboarding (#22055)
* feat: Selling Desk, Dashboard and Onboarding * chore: Selling Onboarding and fixes in Other onboardings * chore: Dashboard and Number card Fixtures * fix: Escape filters and Reposition Accounts Dashboard shortcut. Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
@@ -12,12 +12,6 @@ frappe.query_reports["Item-wise Sales History"] = {
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group"
|
||||
},
|
||||
{
|
||||
fieldname:"from_date",
|
||||
reqd: 1,
|
||||
@@ -32,6 +26,38 @@ frappe.query_reports["Item-wise Sales History"] = {
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
},
|
||||
{
|
||||
fieldname:"item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group"
|
||||
},
|
||||
{
|
||||
fieldname:"item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldname:"customer",
|
||||
label: __("Customer"),
|
||||
fieldtype: "Link",
|
||||
options: "Customer"
|
||||
}
|
||||
],
|
||||
|
||||
]
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
let format_fields = ["delivered_quantity", "billed_amount"];
|
||||
|
||||
if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green;'>" + value + "</span>";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,10 @@ def execute(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
chart_data = get_chart_data(data)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
@@ -181,6 +184,12 @@ def get_conditions(filters):
|
||||
if filters.get('to_date'):
|
||||
conditions += "AND so.transaction_date <= '%s'" %filters.to_date
|
||||
|
||||
if filters.get("item_code"):
|
||||
conditions += "AND so_item.item_code = '%s'" %frappe.db.escape(filters.item_code)
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions += "AND so.customer = '%s'" %frappe.db.escape(filters.customer)
|
||||
|
||||
return conditions
|
||||
|
||||
def get_customer_details():
|
||||
@@ -212,3 +221,34 @@ def get_sales_order_details(company_list, filters):
|
||||
AND so.company in ({0})
|
||||
AND so.docstatus = 1 {1}
|
||||
""".format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1)
|
||||
|
||||
def get_chart_data(data):
|
||||
item_wise_sales_map = {}
|
||||
labels, datapoints = [], []
|
||||
|
||||
for row in data:
|
||||
item_key = row.get("item_code")
|
||||
|
||||
if not item_key in item_wise_sales_map:
|
||||
item_wise_sales_map[item_key] = 0
|
||||
|
||||
item_wise_sales_map[item_key] = flt(item_wise_sales_map[item_key]) + flt(row.get("amount"))
|
||||
|
||||
item_wise_sales_map = { item: value for item, value in (sorted(item_wise_sales_map.items(), key = lambda i: i[1], reverse=True))}
|
||||
|
||||
for key in item_wise_sales_map:
|
||||
labels.append(key)
|
||||
datapoints.append(item_wise_sales_map[key])
|
||||
|
||||
return {
|
||||
"data" : {
|
||||
"labels" : labels[:30], # show max of 30 items in chart
|
||||
"datasets" : [
|
||||
{
|
||||
"name" : _(" Total Sales Amount"),
|
||||
"values" : datapoints[:30]
|
||||
}
|
||||
]
|
||||
},
|
||||
"type" : "bar"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from erpnext.controllers.trends import get_columns, get_data
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -11,4 +12,48 @@ def execute(filters=None):
|
||||
conditions = get_columns(filters, "Quotation")
|
||||
data = get_data(filters, conditions)
|
||||
|
||||
return conditions["columns"], data
|
||||
chart_data = get_chart_data(data, conditions, filters)
|
||||
|
||||
return conditions["columns"], data, None, chart_data
|
||||
|
||||
def get_chart_data(data, conditions, filters):
|
||||
if not (data and conditions):
|
||||
return []
|
||||
|
||||
datapoints = []
|
||||
|
||||
start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1
|
||||
if filters.get("group_by"):
|
||||
start += 1
|
||||
|
||||
# fetch only periodic columns as labels
|
||||
columns = conditions.get("columns")[start:-2][1::2]
|
||||
labels = [column.split(':')[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start-1]:
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
row = row[1::2]
|
||||
|
||||
for i in range(len(row)):
|
||||
datapoints[i] += row[i]
|
||||
|
||||
return {
|
||||
"data" : {
|
||||
"labels" : labels,
|
||||
"datasets" : [
|
||||
{
|
||||
"name" : _("{0}").format(filters.get("period")) + _(" Quoted Amount"),
|
||||
"values" : datapoints
|
||||
}
|
||||
]
|
||||
},
|
||||
"type" : "line",
|
||||
"lineOptions": {
|
||||
"regionFill": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Sales Order Analysis"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"width": "80",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_default("company")
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("From Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": "80",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"label": __("To Date"),
|
||||
"fieldtype": "Date",
|
||||
"width": "80",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order",
|
||||
"label": __("Sales Order"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": "80",
|
||||
"options": "Sales Order",
|
||||
"get_data": function(txt) {
|
||||
return frappe.db.get_link_options("Sales Order", txt);
|
||||
},
|
||||
"get_query": () =>{
|
||||
return {
|
||||
filters: { "docstatus": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"label": __("Status"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": "80",
|
||||
get_data: function(txt) {
|
||||
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
|
||||
let options = []
|
||||
for (let option of status){
|
||||
options.push({
|
||||
"value": option,
|
||||
"description": ""
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "group_by_so",
|
||||
"label": __("Group by Sales Order"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
}
|
||||
],
|
||||
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
let format_fields = ["delivered_qty", "billed_amount"];
|
||||
|
||||
if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green;'>" + value + "</span>";
|
||||
}
|
||||
|
||||
if (column.fieldname == "delay" && data && data[column.fieldname] > 0) {
|
||||
value = "<span style='color:red;'>" + value + "</span>";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2020-05-29 14:54:53.591445",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-05-29 14:54:53.591445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Analysis",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Order",
|
||||
"report_name": "Sales Order Analysis",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import copy
|
||||
from frappe import _
|
||||
from frappe.utils import flt, date_diff, getdate
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
return [], [], None, []
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
conditions = get_conditions(filters)
|
||||
data = get_data(conditions, filters)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
|
||||
def validate_filters(filters):
|
||||
from_date, to_date = filters.get("from_date"), filters.get("to_date")
|
||||
|
||||
if not from_date and to_date:
|
||||
frappe.throw(_("From and To Dates are required."))
|
||||
elif date_diff(to_date, from_date) < 0:
|
||||
frappe.throw(_("To Date cannot be before From Date."))
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
conditions += " and so.transaction_date between %(from_date)s and %(to_date)s"
|
||||
|
||||
if filters.get("company"):
|
||||
conditions += " and so.company = %(company)s"
|
||||
|
||||
if filters.get("sales_order"):
|
||||
conditions += " and so.name in %(sales_order)s"
|
||||
|
||||
if filters.get("status"):
|
||||
conditions += " and so.status in %(status)s"
|
||||
|
||||
return conditions
|
||||
|
||||
def get_data(conditions, filters):
|
||||
data = frappe.db.sql("""
|
||||
SELECT
|
||||
so.transaction_date as date,
|
||||
soi.delivery_date as delivery_date,
|
||||
so.name as sales_order,
|
||||
so.status, so.customer, soi.item_code,
|
||||
DATEDIFF(CURDATE(), soi.delivery_date) as delay_days,
|
||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||
soi.qty, soi.delivered_qty,
|
||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||
IFNULL(sii.qty, 0) as billed_qty,
|
||||
soi.base_amount as amount,
|
||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||
soi.warehouse as warehouse,
|
||||
so.company, soi.name
|
||||
FROM
|
||||
`tabSales Order` so,
|
||||
`tabSales Order Item` soi
|
||||
LEFT JOIN `tabSales Invoice Item` sii
|
||||
ON sii.so_detail = soi.name
|
||||
WHERE
|
||||
soi.parent = so.name
|
||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||
and so.docstatus = 1
|
||||
{conditions}
|
||||
GROUP BY soi.name
|
||||
ORDER BY so.transaction_date ASC
|
||||
""".format(conditions=conditions), filters, as_dict=1)
|
||||
|
||||
return data
|
||||
|
||||
def prepare_data(data, filters):
|
||||
completed, pending = 0, 0
|
||||
|
||||
if filters.get("group_by_so"):
|
||||
sales_order_map = {}
|
||||
|
||||
for row in data:
|
||||
# sum data for chart
|
||||
completed += row["billed_amount"]
|
||||
pending += row["pending_amount"]
|
||||
|
||||
# prepare data for report view
|
||||
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
|
||||
|
||||
row["delay"] = 0 if row["delay"] < 0 else row["delay"]
|
||||
if filters.get("group_by_so"):
|
||||
so_name = row["sales_order"]
|
||||
|
||||
if not so_name in sales_order_map:
|
||||
# create an entry
|
||||
row_copy = copy.deepcopy(row)
|
||||
sales_order_map[so_name] = row_copy
|
||||
else:
|
||||
# update existing entry
|
||||
so_row = sales_order_map[so_name]
|
||||
so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"]))
|
||||
so_row["delay"] = min(so_row["delay"], row["delay"])
|
||||
|
||||
# sum numeric columns
|
||||
fields = ["qty", "delivered_qty", "pending_qty", "billed_qty", "qty_to_bill", "amount",
|
||||
"delivered_qty_amount", "billed_amount", "pending_amount"]
|
||||
for field in fields:
|
||||
so_row[field] = flt(row[field]) + flt(so_row[field])
|
||||
|
||||
chart_data = prepare_chart_data(pending, completed)
|
||||
|
||||
if filters.get("group_by_so"):
|
||||
data = []
|
||||
for so in sales_order_map:
|
||||
data.append(sales_order_map[so])
|
||||
return data, chart_data
|
||||
|
||||
return data, chart_data
|
||||
|
||||
def prepare_chart_data(pending, completed):
|
||||
labels = ["Amount to Bill", "Billed Amount"]
|
||||
|
||||
return {
|
||||
"data" : {
|
||||
"labels": labels,
|
||||
"datasets": [
|
||||
{"values": [pending, completed]}
|
||||
]
|
||||
},
|
||||
"type": 'donut',
|
||||
"height": 300
|
||||
}
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label":_("Date"),
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"width": 90
|
||||
},
|
||||
{
|
||||
"label": _("Sales Order"),
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Order",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label":_("Status"),
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"width": 130
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
"width": 130
|
||||
}]
|
||||
|
||||
if not filters.get("group_by_so"):
|
||||
columns.append({
|
||||
"label":_("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
"label": _("Qty"),
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"convertible": "qty"
|
||||
},
|
||||
{
|
||||
"label": _("Delivered Qty"),
|
||||
"fieldname": "delivered_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"convertible": "qty"
|
||||
},
|
||||
{
|
||||
"label": _("Qty to Deliver"),
|
||||
"fieldname": "pending_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"convertible": "qty"
|
||||
},
|
||||
{
|
||||
"label": _("Billed Qty"),
|
||||
"fieldname": "billed_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty"
|
||||
},
|
||||
{
|
||||
"label": _("Qty to Bill"),
|
||||
"fieldname": "qty_to_bill",
|
||||
"fieldtype": "Float",
|
||||
"width": 80,
|
||||
"convertible": "qty"
|
||||
},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate"
|
||||
},
|
||||
{
|
||||
"label": _("Billed Amount"),
|
||||
"fieldname": "billed_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate"
|
||||
},
|
||||
{
|
||||
"label": _("Pending Amount"),
|
||||
"fieldname": "pending_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 130,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate"
|
||||
},
|
||||
{
|
||||
"label": _("Amount Delivered"),
|
||||
"fieldname": "delivered_qty_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 100,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate"
|
||||
},
|
||||
{
|
||||
"label":_("Delivery Date"),
|
||||
"fieldname": "delivery_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": _("Delay (in Days)"),
|
||||
"fieldname": "delay",
|
||||
"fieldtype": "Data",
|
||||
"width": 100
|
||||
}
|
||||
])
|
||||
if not filters.get("group_by_so"):
|
||||
columns.append({
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"width": 100
|
||||
})
|
||||
columns.append({
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 100
|
||||
})
|
||||
|
||||
|
||||
return columns
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from erpnext.controllers.trends import get_columns,get_data
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -10,4 +11,48 @@ def execute(filters=None):
|
||||
data = []
|
||||
conditions = get_columns(filters, "Sales Order")
|
||||
data = get_data(filters, conditions)
|
||||
return conditions["columns"], data
|
||||
chart_data = get_chart_data(data, conditions, filters)
|
||||
|
||||
return conditions["columns"], data, None, chart_data
|
||||
|
||||
def get_chart_data(data, conditions, filters):
|
||||
if not (data and conditions):
|
||||
return []
|
||||
|
||||
datapoints = []
|
||||
|
||||
start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1
|
||||
if filters.get("group_by"):
|
||||
start += 1
|
||||
|
||||
# fetch only periodic columns as labels
|
||||
columns = conditions.get("columns")[start:-2][1::2]
|
||||
labels = [column.split(':')[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start-1]:
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
row = row[1::2]
|
||||
|
||||
for i in range(len(row)):
|
||||
datapoints[i] += row[i]
|
||||
|
||||
return {
|
||||
"data" : {
|
||||
"labels" : labels,
|
||||
"datasets" : [
|
||||
{
|
||||
"name" : _("{0}").format(filters.get("period")) + _(" Sales Value"),
|
||||
"values" : datapoints
|
||||
}
|
||||
]
|
||||
},
|
||||
"type" : "line",
|
||||
"lineOptions": {
|
||||
"regionFill": 1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user