Merge branch 'develop' into mr-se-warehouse-validation
This commit is contained in:
commit
17614c9860
@ -401,6 +401,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
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 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
|
@ -498,7 +498,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -592,7 +592,7 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Delivery Note': 'Delivery',
|
||||
'Sales Invoice': 'Sales Return',
|
||||
'Sales Invoice': 'Return / Credit Note',
|
||||
'Payment Request': 'Payment Request',
|
||||
'Payment Entry': 'Payment'
|
||||
},
|
||||
|
@ -446,7 +446,7 @@ class Subscription(Document):
|
||||
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
|
||||
|
@ -58,8 +58,8 @@ frappe.ui.form.on("Purchase Order Item", {
|
||||
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',
|
||||
}
|
||||
|
@ -124,21 +124,24 @@ class ProgramEnrollment(Document):
|
||||
@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
|
||||
|
@ -103,7 +103,7 @@ def get_data(filters):
|
||||
|
||||
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"]
|
||||
)
|
||||
|
||||
|
@ -411,7 +411,7 @@ cur_frm.cscript.hour_rate = function(doc) {
|
||||
|
||||
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 @@ cur_frm.cscript.is_default = function(doc) {
|
||||
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 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -65,6 +65,10 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
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 @@ class BOM(WebsiteGenerator):
|
||||
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 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
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 @@ def get_valuation_rate(args):
|
||||
""" 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
|
||||
|
@ -456,10 +456,10 @@ class WorkOrder(Document):
|
||||
|
||||
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 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
|
||||
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"),
|
||||
|
@ -20,6 +20,7 @@ def get_columns():
|
||||
_("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 @@ def get_bom_stock(filters):
|
||||
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 @@ def get_bom_stock(filters):
|
||||
|
||||
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 @@ def get_bom_stock(filters):
|
||||
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)))
|
||||
|
@ -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 @@ def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll
|
||||
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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -218,8 +218,8 @@ def update_item_taxes(invoice, item):
|
||||
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 @@ def make_einvoice(invoice):
|
||||
|
||||
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 RequestFailed(Exception): pass
|
||||
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 @@ class GSPConnector():
|
||||
|
||||
_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',
|
||||
|
@ -48,6 +48,9 @@ def validate_gstin_for_india(doc, method):
|
||||
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))
|
||||
|
@ -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() {
|
||||
|
@ -13,7 +13,7 @@ frappe.ui.form.on("Delivery Note", {
|
||||
frm.custom_make_buttons = {
|
||||
'Packing Slip': 'Packing Slip',
|
||||
'Installation Note': 'Installation Note',
|
||||
'Sales Invoice': 'Invoice',
|
||||
'Sales Invoice': 'Sales Invoice',
|
||||
'Stock Entry': 'Return',
|
||||
'Shipment': 'Shipment'
|
||||
},
|
||||
|
@ -524,7 +524,7 @@ frappe.ui.form.on('Stock Entry', {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
for (let i in frm.doc.items) {
|
||||
let item = frm.doc.items[i];
|
||||
|
||||
@ -675,7 +675,13 @@ frappe.ui.form.on('Stock Entry Detail', {
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
0
erpnext/support/report/issue_summary/__init__.py
Normal file
0
erpnext/support/report/issue_summary/__init__.py
Normal file
73
erpnext/support/report/issue_summary/issue_summary.js
Normal file
73
erpnext/support/report/issue_summary/issue_summary.js
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
26
erpnext/support/report/issue_summary/issue_summary.json
Normal file
26
erpnext/support/report/issue_summary/issue_summary.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
353
erpnext/support/report/issue_summary/issue_summary.py
Normal file
353
erpnext/support/report/issue_summary/issue_summary.py
Normal file
@ -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',
|
||||
}
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user