Merge branch 'develop' into mr-se-warehouse-validation
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index e117471..9bdd26b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -401,6 +401,8 @@
set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) {
+
+ var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -427,6 +429,14 @@
if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm);
+
+ if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
+ && frm.doc.paid_amount != frm.doc.received_amount) {
+ if (company_currency != frm.doc.paid_from_account_currency &&
+ frm.doc.payment_type == "Pay") {
+ frm.doc.paid_amount = frm.doc.received_amount;
+ }
+ }
}
},
() => {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 7830cfd..3863768 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -498,7 +498,7 @@
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Purchase Invoice': 'Debit Note',
+ 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment'
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 5efc32e..89b716c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -592,7 +592,7 @@
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
- 'Sales Invoice': 'Sales Return',
+ 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request',
'Payment Entry': 'Payment'
},
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 552a5d4..e023b47 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -446,7 +446,7 @@
if not self.generate_invoice_at_period_start:
return False
- if self.is_new_subscription():
+ if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True
# Check invoice dates and make sure it doesn't have outstanding invoices
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47483c9..38532d1 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -58,8 +58,8 @@
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() {
this.frm.custom_make_buttons = {
- 'Purchase Receipt': 'Receipt',
- 'Purchase Invoice': 'Invoice',
+ 'Purchase Receipt': 'Purchase Receipt',
+ 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
}
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index 6fbcd8a..886a7d8 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -124,21 +124,24 @@
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
- if filters.get('program'):
- return frappe.db.sql("""select course, course_name from `tabProgram Course`
- where parent = %(program)s and course like %(txt)s {match_cond}
- order by
- if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
- idx desc,
- `tabProgram Course`.course asc
- limit {start}, {page_len}""".format(
- match_cond=get_match_cond(doctype),
- start=start,
- page_len=page_len), {
- "txt": "%{0}%".format(txt),
- "_txt": txt.replace('%', ''),
- "program": filters['program']
- })
+ if not filters.get('program'):
+ frappe.msgprint(_("Please select a Program first."))
+ return []
+
+ return frappe.db.sql("""select course, course_name from `tabProgram Course`
+ where parent = %(program)s and course like %(txt)s {match_cond}
+ order by
+ if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
+ idx desc,
+ `tabProgram Course`.course asc
+ limit {start}, {page_len}""".format(
+ match_cond=get_match_cond(doctype),
+ start=start,
+ page_len=page_len), {
+ "txt": "%{0}%".format(txt),
+ "_txt": txt.replace('%', ''),
+ "program": filters['program']
+ })
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
index b63cc8e..c6f6b99 100644
--- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
@@ -103,7 +103,7 @@
loan_repayments = frappe.get_all("Loan Repayment",
filters = query_filters,
- fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount",
+ fields=["posting_date", "applicant", "name", "against_loan", "payable_amount",
"pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
)
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 1c4b7a1..15affd8 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -411,7 +411,7 @@
cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate;
-cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
+cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
get_bom_material_detail(doc, cdt, cdn, false);
};
@@ -419,17 +419,22 @@
if (doc.is_default) cur_frm.set_value("is_active", 1);
};
-var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) {
+var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
+ if (!doc.company) {
+ frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
+ }
+
var d = locals[cdt][cdn];
if (d.item_code) {
return frappe.call({
doc: doc,
method: "get_bom_material_detail",
args: {
- 'item_code': d.item_code,
- 'bom_no': d.bom_no != null ? d.bom_no: '',
+ "company": doc.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no != null ? d.bom_no: '',
"scrap_items": scrap_items,
- 'qty': d.qty,
+ "qty": d.qty,
"stock_qty": d.stock_qty,
"include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom,
@@ -468,7 +473,7 @@
}
if (d.bom_no) {
- frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item"));
+ frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
get_bom_material_detail(doc, cdt, cdn, scrap_items);
} else {
erpnext.bom.calculate_rm_cost(doc);
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6363242..03beedb 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -65,6 +65,10 @@
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
+
+ if not self.company:
+ frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
+
self.clear_operations()
self.validate_main_item()
self.validate_currency()
@@ -125,6 +129,7 @@
self.validate_bom_currecny(item)
ret = self.get_bom_material_detail({
+ "company": self.company,
"item_code": item.item_code,
"item_name": item.item_name,
"bom_no": item.bom_no,
@@ -213,6 +218,7 @@
for d in self.get("items"):
rate = self.get_rm_rate({
+ "company": self.company,
"item_code": d.item_code,
"bom_no": d.bom_no,
"qty": d.qty,
@@ -611,10 +617,20 @@
""" Get weighted average of valuation rate from all warehouses """
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
- for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin`
- where item_code=%s""", args['item_code'], as_dict=1):
- total_qty += flt(d.actual_qty)
- total_value += flt(d.stock_value)
+ item_bins = frappe.db.sql("""
+ select
+ bin.actual_qty, bin.stock_value
+ from
+ `tabBin` bin, `tabWarehouse` warehouse
+ where
+ bin.item_code=%(item)s
+ and bin.warehouse = warehouse.name
+ and warehouse.company=%(company)s""",
+ {"item": args['item_code'], "company": args['company']}, as_dict=1)
+
+ for d in item_bins:
+ total_qty += flt(d.actual_qty)
+ total_value += flt(d.stock_value)
if total_qty:
valuation_rate = total_value / total_qty
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index cc93bf9..ca530bb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -456,10 +456,10 @@
if data and len(data):
dates = [d.posting_datetime for d in data]
- self.actual_start_date = min(dates)
+ self.db_set('actual_start_date', min(dates))
if self.status == "Completed":
- self.actual_end_date = max(dates)
+ self.db_set('actual_end_date', max(dates))
self.set_lead_time()
@@ -725,6 +725,7 @@
args.update(item_data)
args["rate"] = get_bom_item_rate({
+ "company": wo_doc.company,
"item_code": args.get("item_code"),
"qty": args.get("required_qty"),
"uom": args.get("stock_uom"),
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 75ebcbc..1c6758e 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -20,6 +20,7 @@
_("Item") + ":Link/Item:150",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
+ _("BOM UoM") + "::160",
_("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200",
@@ -32,7 +33,7 @@
bom = filters.get("bom")
table = "`tabBOM Item`"
- qty_field = "qty"
+ qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0:
@@ -40,7 +41,6 @@
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
- qty_field = "stock_qty"
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@@ -59,6 +59,7 @@
bom_item.item_code,
bom_item.description ,
bom_item.{qty_field},
+ bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index d7893d0..46f6cd8 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt
+from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee
@@ -88,6 +88,8 @@
period_start = joining_date
if relieving_date and getdate(relieving_date) < getdate(period_end):
period_end = relieving_date
+ if month_diff(period_end, start_date) > 1:
+ start_date = add_months(start_date, - (month_diff(period_end, start_date)+1))
total_sub_periods, remaining_sub_periods = 0.0, 0.0
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
index 5c1c79d..3034370 100644
--- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
@@ -24,9 +24,8 @@
},
{
"fieldname": "reference_invoice",
- "fieldtype": "Link",
- "label": "Reference Invoice",
- "options": "Sales Invoice"
+ "fieldtype": "Data",
+ "label": "Reference Invoice"
},
{
"fieldname": "headers",
@@ -64,7 +63,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-24 21:09:38.882866",
+ "modified": "2021-01-13 12:06:57.253111",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Request Log",
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
index 4dcb22a..db8bda7 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -7,6 +7,7 @@
"field_order": [
"enable",
"section_break_2",
+ "sandbox_mode",
"credentials",
"auth_token",
"token_expiry"
@@ -41,12 +42,18 @@
"label": "Credentials",
"mandatory_depends_on": "enable",
"options": "E Invoice User"
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox_mode",
+ "fieldtype": "Check",
+ "label": "Sandbox Mode"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-12-22 15:34:57.280044",
+ "modified": "2021-01-13 12:04:49.449199",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index abe1504..d0cac90 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -218,8 +218,8 @@
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = invoice.discount_amount
- invoice_value_details.round_off = invoice.rounding_adjustment
+ invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@@ -322,7 +322,10 @@
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
- shipping_details = get_party_details(invoice.shipping_address_name)
+ if invoice.gst_category == 'Overseas':
+ shipping_details = get_overseas_address_details(invoice.shipping_address_name)
+ else:
+ shipping_details = get_party_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
@@ -414,15 +417,19 @@
class GSPConnector():
def __init__(self, doctype=None, docname=None):
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+ sandbox_mode = self.e_invoice_settings.sandbox_mode
+
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
self.credentials = self.get_credentials()
- self.base_url = 'https://gsp.adaequare.com'
- self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token'
- self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
- self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
- self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ # authenticate url is same for sandbox & live
+ self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
+ self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
+
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
+ self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
+ self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
@@ -758,7 +765,7 @@
_file = frappe.new_doc('File')
_file.update({
- 'file_name': f'QRCode_{docname}.png',
+ 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
'attached_to_doctype': doctype,
'attached_to_name': docname,
'content': 'qrcode',
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index f256a66..0d82638 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -48,6 +48,9 @@
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
+ if not doc.gst_state:
+ frappe.throw(_("Please Enter GST state"))
+
if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 661e107..5a0d9c9 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -7,7 +7,7 @@
frappe.ui.form.on('Quotation', {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Sales Order': 'Make Sales Order'
+ 'Sales Order': 'Sales Order'
},
frm.set_query("quotation_to", function() {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 5f2658c..cb1e31b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -13,7 +13,7 @@
frm.custom_make_buttons = {
'Packing Slip': 'Packing Slip',
'Installation Note': 'Installation Note',
- 'Sales Invoice': 'Invoice',
+ 'Sales Invoice': 'Sales Invoice',
'Stock Entry': 'Return',
'Shipment': 'Shipment'
},
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 98116ec..f75e8b7 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -524,7 +524,7 @@
})
);
}
-
+
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
@@ -675,7 +675,13 @@
});
refresh_field("items");
- if (!d.serial_no) {
+ let no_batch_serial_number_value = !d.serial_no;
+ if (d.has_batch_no && !d.has_serial_no) {
+ // check only batch_no for batched item
+ no_batch_serial_number_value = !d.batch_no;
+ }
+
+ if (no_batch_serial_number_value) {
erpnext.stock.select_batch_and_serial_no(frm, d);
}
}
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
index 28410f3..18cf87a 100644
--- a/erpnext/support/desk_page/support/support.json
+++ b/erpnext/support/desk_page/support/support.json
@@ -28,7 +28,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
- "modified": "2020-08-11 15:49:34.307341",
+ "modified": "2020-10-12 18:40:22.252915",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
diff --git a/erpnext/support/report/issue_summary/__init__.py b/erpnext/support/report/issue_summary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/support/report/issue_summary/__init__.py
diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js
new file mode 100644
index 0000000..684482a
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.js
@@ -0,0 +1,73 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Issue Summary"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "based_on",
+ label: __("Based On"),
+ fieldtype: "Select",
+ options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "status",
+ label: __("Status"),
+ fieldtype: "Select",
+ options:[
+ {label: __('Open'), value: 'Open'},
+ {label: __('Replied'), value: 'Replied'},
+ {label: __('Resolved'), value: 'Resolved'},
+ {label: __('Closed'), value: 'Closed'}
+ ]
+ },
+ {
+ fieldname: "priority",
+ label: __("Issue Priority"),
+ fieldtype: "Link",
+ options: "Issue Priority"
+ },
+ {
+ fieldname: "customer",
+ label: __("Customer"),
+ fieldtype: "Link",
+ options: "Customer"
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project"
+ },
+ {
+ fieldname: "assigned_to",
+ label: __("Assigned To"),
+ fieldtype: "Link",
+ options: "User"
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.json b/erpnext/support/report/issue_summary/issue_summary.json
new file mode 100644
index 0000000..b8a580c
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-10-12 01:01:55.181777",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-10-12 14:54:55.655920",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Issue Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Issue",
+ "report_name": "Issue Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
new file mode 100644
index 0000000..3d73531
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -0,0 +1,353 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import iteritems
+from frappe import _, scrub
+from frappe.utils import flt
+
+def execute(filters=None):
+ return IssueSummary(filters).run()
+
+class IssueSummary(object):
+ def __init__(self, filters=None):
+ self.filters = frappe._dict(filters or {})
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+ self.get_report_summary()
+
+ return self.columns, self.data, None, self.chart, self.report_summary
+
+ def get_columns(self):
+ self.columns = []
+
+ if self.filters.based_on == 'Customer':
+ self.columns.append({
+ 'label': _('Customer'),
+ 'options': 'Customer',
+ 'fieldname': 'customer',
+ 'fieldtype': 'Link',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Assigned To':
+ self.columns.append({
+ 'label': _('User'),
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Type':
+ self.columns.append({
+ 'label': _('Issue Type'),
+ 'fieldname': 'issue_type',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Type',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Priority':
+ self.columns.append({
+ 'label': _('Issue Priority'),
+ 'fieldname': 'priority',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Priority',
+ 'width': 200
+ })
+
+ self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
+ for status in self.statuses:
+ self.columns.append({
+ 'label': _(status),
+ 'fieldname': scrub(status),
+ 'fieldtype': 'Int',
+ 'width': 80
+ })
+
+ self.columns.append({
+ 'label': _('Total Issues'),
+ 'fieldname': 'total_issues',
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.sla_status_map = {
+ 'SLA Failed': 'failed',
+ 'SLA Fulfilled': 'fulfilled',
+ 'SLA Ongoing': 'ongoing'
+ }
+
+ for label, fieldname in self.sla_status_map.items():
+ self.columns.append({
+ 'label': _(label),
+ 'fieldname': fieldname,
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time',
+ 'Avg Resolution Time', 'Avg User Resolution Time']
+
+ for metric in self.metrics:
+ self.columns.append({
+ 'label': _(metric),
+ 'fieldname': scrub(metric),
+ 'fieldtype': 'Duration',
+ 'width': 170
+ })
+
+ def get_data(self):
+ self.get_issues()
+ self.get_rows()
+
+ def get_issues(self):
+ filters = self.get_common_filters()
+ self.field_map = {
+ 'Customer': 'customer',
+ 'Issue Type': 'issue_type',
+ 'Issue Priority': 'priority',
+ 'Assigned To': '_assign'
+ }
+
+ self.entries = frappe.db.get_all('Issue',
+ fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time',
+ 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'],
+ filters=filters
+ )
+
+ def get_common_filters(self):
+ filters = {}
+ filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+
+ if self.filters.get('assigned_to'):
+ filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+
+ for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ if self.filters.get(entry):
+ filters[entry] = self.filters.get(entry)
+
+ return filters
+
+ def get_rows(self):
+ self.data = []
+ self.get_summary_data()
+
+ for entity, data in iteritems(self.issue_summary_data):
+ if self.filters.based_on == 'Customer':
+ row = {'customer': entity}
+ elif self.filters.based_on == 'Assigned To':
+ row = {'user': entity}
+ elif self.filters.based_on == 'Issue Type':
+ row = {'issue_type': entity}
+ elif self.filters.based_on == 'Issue Priority':
+ row = {'priority': entity}
+
+ for status in self.statuses:
+ count = flt(data.get(status, 0.0))
+ row[scrub(status)] = count
+
+ row['total_issues'] = data.get('total_issues', 0.0)
+
+ for sla_status in self.sla_status_map.values():
+ value = flt(data.get(sla_status), 0.0)
+ row[sla_status] = value
+
+ for metric in self.metrics:
+ value = flt(data.get(scrub(metric)), 0.0)
+ row[scrub(metric)] = value
+
+ self.data.append(row)
+
+ def get_summary_data(self):
+ self.issue_summary_data = frappe._dict()
+
+ for d in self.entries:
+ status = d.status
+ agreement_status = scrub(d.agreement_status)
+
+ if self.filters.based_on == 'Assigned To':
+ if d._assign:
+ for entry in json.loads(d._assign):
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[entry][status] += 1
+ self.issue_summary_data[entry][agreement_status] += 1
+ self.issue_summary_data[entry]['total_issues'] += 1
+
+ else:
+ field = self.field_map.get(self.filters.based_on)
+ value = d.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[value][status] += 1
+ self.issue_summary_data[value][agreement_status] += 1
+ self.issue_summary_data[value]['total_issues'] += 1
+
+ self.get_metrics_data()
+
+ def get_metrics_data(self):
+ issues = []
+
+ metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time',
+ 'avg_resolution_time', 'avg_user_resolution_time']
+
+ for entry in self.entries:
+ issues.append(entry.name)
+
+ field = self.field_map.get(self.filters.based_on)
+
+ if issues:
+ if self.filters.based_on == 'Assigned To':
+ assignment_map = frappe._dict()
+ for d in self.entries:
+ if d._assign:
+ for entry in json.loads(d._assign):
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0
+ self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0
+ self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0
+
+ if not assignment_map.get(entry):
+ assignment_map[entry] = 0
+ assignment_map[entry] += 1
+
+ for entry in assignment_map:
+ for metric in metrics_list:
+ self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry))
+
+ else:
+ data = frappe.db.sql("""
+ SELECT
+ {0}, AVG(first_response_time) as avg_frt,
+ AVG(avg_response_time) as avg_resp_time,
+ AVG(total_hold_time) as avg_hold_time,
+ AVG(resolution_time) as avg_resolution_time,
+ AVG(user_resolution_time) as avg_user_resolution_time
+ FROM `tabIssue`
+ WHERE
+ name IN %(issues)s
+ GROUP BY {0}
+ """.format(field), {'issues': issues}, as_dict=1)
+
+ for entry in data:
+ value = entry.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0
+ self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0
+ self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0
+ self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0
+ self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
+
+ def get_chart_data(self):
+ if not self.data:
+ return None
+
+ labels = []
+ open_issues = []
+ replied_issues = []
+ resolved_issues = []
+ closed_issues = []
+
+ entity = self.filters.based_on
+ entity_field = self.field_map.get(entity)
+ if entity == 'Assigned To':
+ entity_field = 'user'
+
+ for entry in self.data:
+ labels.append(entry.get(entity_field))
+ open_issues.append(entry.get('open'))
+ replied_issues.append(entry.get('replied'))
+ resolved_issues.append(entry.get('resolved'))
+ closed_issues.append(entry.get('closed'))
+
+ self.chart = {
+ 'data': {
+ 'labels': labels[:30],
+ 'datasets': [
+ {
+ 'name': 'Open',
+ 'values': open_issues[:30]
+ },
+ {
+ 'name': 'Replied',
+ 'values': replied_issues[:30]
+ },
+ {
+ 'name': 'Resolved',
+ 'values': resolved_issues[:30]
+ },
+ {
+ 'name': 'Closed',
+ 'values': closed_issues[:30]
+ }
+ ]
+ },
+ 'type': 'bar',
+ 'barOptions': {
+ 'stacked': True
+ }
+ }
+
+ def get_report_summary(self):
+ if not self.data:
+ return None
+
+ open_issues = 0
+ replied = 0
+ resolved = 0
+ closed = 0
+
+ for entry in self.data:
+ open_issues += entry.get('open')
+ replied += entry.get('replied')
+ resolved += entry.get('resolved')
+ closed += entry.get('closed')
+
+ self.report_summary = [
+ {
+ 'value': open_issues,
+ 'indicator': 'Red',
+ 'label': _('Open'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': replied,
+ 'indicator': 'Grey',
+ 'label': _('Replied'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': resolved,
+ 'indicator': 'Green',
+ 'label': _('Resolved'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': closed,
+ 'indicator': 'Green',
+ 'label': _('Closed'),
+ 'datatype': 'Int',
+ }
+ ]
+