Merge branch 'develop' into rcm_develop

This commit is contained in:
Deepesh Garg 2020-06-17 15:33:29 +05:30 committed by GitHub
commit 0511e8cc03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 2563 additions and 1916 deletions

View File

@ -5,7 +5,22 @@ import frappe
import json
from frappe.utils import nowdate, add_months, get_date_str
from frappe import _
from erpnext.accounts.utils import get_fiscal_year, get_account_name
from erpnext.accounts.utils import get_fiscal_year, get_account_name, FiscalYearError
def _get_fiscal_year(date=None):
try:
fiscal_year = get_fiscal_year(date=nowdate(), as_dict=True)
return fiscal_year
except FiscalYearError:
#if no fiscal year for current date then get default fiscal year
try:
fiscal_year = get_fiscal_year(as_dict=True)
return fiscal_year
except FiscalYearError:
#if still no fiscal year found then no accounting data created, return
return None
def get_company_for_dashboards():
company = frappe.defaults.get_defaults().company
@ -18,10 +33,16 @@ def get_company_for_dashboards():
return None
def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
return frappe._dict({
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards()
"charts": get_charts(fiscal_year),
"number_cards": get_number_cards(fiscal_year)
})
def get_dashboards():
@ -46,10 +67,9 @@ def get_dashboards():
]
}]
def get_charts():
def get_charts(fiscal_year):
company = frappe.get_doc("Company", get_company_for_dashboards())
bank_account = company.default_bank_account or get_account_name("Bank", company=company.name)
fiscal_year = get_fiscal_year(date=nowdate())
default_cost_center = company.cost_center
return [
@ -61,8 +81,8 @@ def get_charts():
"filters_json": json.dumps({
"company": company.name,
"filter_based_on": "Fiscal Year",
"from_fiscal_year": fiscal_year[0],
"to_fiscal_year": fiscal_year[0],
"from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year.get('name'),
"periodicity": "Monthly",
"include_default_book_entries": 1
}),
@ -158,8 +178,8 @@ def get_charts():
"report_name": "Budget Variance Report",
"filters_json": json.dumps({
"company": company.name,
"from_fiscal_year": fiscal_year[0],
"to_fiscal_year": fiscal_year[0],
"from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year.get('name'),
"period": "Monthly",
"budget_against": "Cost Center"
}),
@ -190,10 +210,10 @@ def get_charts():
},
]
def get_number_cards():
fiscal_year = get_fiscal_year(date=nowdate())
year_start_date = get_date_str(fiscal_year[1])
year_end_date = get_date_str(fiscal_year[2])
def get_number_cards(fiscal_year):
year_start_date = get_date_str(fiscal_year.get("year_start_date"))
year_end_date = get_date_str(fiscal_year.get("year_end_date"))
return [
{
"doctype": "Number Card",

View File

@ -72,7 +72,11 @@ def make_dimension_in_accounting_doctypes(doc):
if doctype == "Budget":
add_dimension_to_budget_doctype(df, doc)
else:
create_custom_field(doctype, df)
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
if df['fieldname'] not in fieldnames:
create_custom_field(doctype, df)
count += 1

View File

@ -582,14 +582,14 @@ class SalesInvoice(SellingController):
def validate_item_code(self):
for d in self.get('items'):
if not d.item_code:
if not d.item_code and self.is_opening == "No":
msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
def validate_warehouse(self):
super(SalesInvoice, self).validate_warehouse()
for d in self.get_item_list():
if not d.warehouse and frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code))
def validate_delivery_note(self):

View File

@ -1745,53 +1745,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_error_email(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income
acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
acc_settings.acc_frozen_upto = '2019-01-31'
acc_settings.save()
pda = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda.insert()
pda.submit()
email = frappe.db.sql(""" select name from `tabEmail Queue`
where message like %(txt)s """, {
'txt': "%%%s%%" % "Error while processing deferred accounting for {0}".format(pda.name)
})
self.assertTrue(email)
acc_settings.load_from_db()
acc_settings.acc_frozen_upto = None
acc_settings.save()
def test_inter_company_transaction(self):
if not frappe.db.exists("Customer", "_Test Internal Customer"):

View File

@ -5,14 +5,23 @@ import frappe
import json
from frappe.utils import nowdate, add_months, get_date_str
from frappe import _
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.dashboard_fixtures import _get_fiscal_year
from erpnext.buying.dashboard_fixtures import get_company_for_dashboards
def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
year_start_date = get_date_str(fiscal_year.get('year_start_date'))
year_end_date = get_date_str(fiscal_year.get('year_end_date'))
return frappe._dict({
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards(),
"charts": get_charts(fiscal_year, year_start_date, year_end_date),
"number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date),
})
def get_dashboards():
@ -31,12 +40,7 @@ def get_dashboards():
]
}]
fiscal_year = get_fiscal_year(date=nowdate())
year_start_date = get_date_str(fiscal_year[1])
year_end_date = get_date_str(fiscal_year[2])
def get_charts():
def get_charts(fiscal_year, year_start_date, year_end_date):
company = get_company_for_dashboards()
return [
{
@ -55,8 +59,8 @@ def get_charts():
"company": company,
"status": "In Location",
"filter_based_on": "Fiscal Year",
"from_fiscal_year": fiscal_year[0],
"to_fiscal_year": fiscal_year[0],
"from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year.get('name'),
"period_start_date": year_start_date,
"period_end_date": year_end_date,
"date_based_on": "Purchase Date",
@ -134,7 +138,7 @@ def get_charts():
}
]
def get_number_cards():
def get_number_cards(fiscal_year, year_start_date, year_end_date):
return [
{
"name": "Total Assets",
@ -173,13 +177,3 @@ def get_number_cards():
"doctype": "Number Card"
}
]
def get_company_for_dashboards():
company = frappe.defaults.get_defaults().company
if company:
return company
else:
company_list = frappe.get_list("Company")
if company_list:
return company_list[0].name
return None

View File

@ -41,7 +41,7 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
team_member = frappe.db.get_value('User', assign_to_member, "email")
args = {
'doctype' : 'Asset Maintenance',
'assign_to' : team_member,
'assign_to' : [team_member],
'name' : asset_maintenance_name,
'description' : maintenance_task,
'date' : next_due_date

View File

@ -5,13 +5,24 @@ import frappe
import json
from frappe import _
from frappe.utils import nowdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.dashboard_fixtures import _get_fiscal_year
def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
return frappe._dict({
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards(),
"charts": get_charts(company, fiscal_year_name, start_date, end_date),
"number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date),
})
def get_company_for_dashboards():
@ -24,12 +35,6 @@ def get_company_for_dashboards():
return company_list[0].name
return None
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year = get_fiscal_year(nowdate(), as_dict=1)
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
def get_dashboards():
return [{
"name": "Buying",
@ -48,7 +53,7 @@ def get_dashboards():
]
}]
def get_charts():
def get_charts(company, fiscal_year_name, start_date, end_date):
return [
{
"name": "Purchase Order Analysis",
@ -139,7 +144,7 @@ def get_charts():
}
]
def get_number_cards():
def get_number_cards(company, fiscal_year_name, start_date, end_date):
return [
{
"name": "Annual Purchase",

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,15 @@ class PurchaseOrder(BuyingController):
"compare_fields": [["project", "="], ["item_code", "="],
["uom", "="], ["conversion_factor", "="]],
"is_child_table": True
},
"Material Request": {
"ref_dn_field": "material_request",
"compare_fields": [["company", "="]],
},
"Material Request Item": {
"ref_dn_field": "material_request_item",
"compare_fields": [["project", "="], ["item_code", "="]],
"is_child_table": True
}
})

View File

@ -15,7 +15,7 @@ class TestProcurementTracker(unittest.TestCase):
def test_result_for_procurement_tracker(self):
filters = {
'company': '_Test Procurement Company',
'cost_center': '_Test Cost Center - _TC'
'cost_center': 'Main - _TPC'
}
expected_data = self.generate_expected_data()
report = execute(filters)
@ -33,24 +33,27 @@ class TestProcurementTracker(unittest.TestCase):
country="Pakistan"
)).insert()
warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company")
mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse)
mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC")
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.get("items")[0].cost_center = "_Test Cost Center - _TC"
po.get("items")[0].cost_center = "Main - _TPC"
po.submit()
pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC"
pr.submit()
frappe.db.commit()
date_obj = datetime.date(datetime.now())
po.load_from_db()
expected_data = {
"material_request_date": date_obj,
"cost_center": "_Test Cost Center - _TC",
"cost_center": "Main - _TPC",
"project": None,
"requesting_site": "_Test Procurement Warehouse - _TPC",
"requestor": "Administrator",
"material_request_no": mr.name,
"description": '_Test Item 1',
"item_code": '_Test Item',
"quantity": 10.0,
"unit_of_measurement": "_Test UOM",
"status": "To Bill",
@ -58,9 +61,9 @@ class TestProcurementTracker(unittest.TestCase):
"purchase_order": po.name,
"supplier": "_Test Supplier",
"estimated_cost": 0.0,
"actual_cost": None,
"purchase_order_amt": 5000.0,
"purchase_order_amt_in_company_currency": 300000.0,
"actual_cost": 0.0,
"purchase_order_amt": po.net_total,
"purchase_order_amt_in_company_currency": po.base_net_total,
"expected_delivery_date": date_obj,
"actual_delivery_date": date_obj
}

View File

@ -349,7 +349,7 @@ class BuyingController(StockController):
})
if not rm.rate:
rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse,
rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
rm.amount = qty * flt(rm.rate)

View File

@ -19,7 +19,8 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass
class StockController(AccountsController):
def validate(self):
super(StockController, self).validate()
self.validate_inspection()
if not self.get('is_return'):
self.validate_inspection()
self.validate_serialized_batch()
self.validate_customer_provided_item()
@ -226,7 +227,9 @@ class StockController(AccountsController):
def check_expense_account(self, item):
if not item.get("expense_account"):
frappe.throw(_("Expense Account not set for Item {0}. Please set an Expense Account for the item in the Items table").format(item.item_code))
frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
title=_("Expense Account Missing"))
else:
is_expense_account = frappe.db.get_value("Account",

View File

@ -30,24 +30,32 @@
"fieldname": "text",
"fieldtype": "Small Text",
"label": "Tweet",
"mandatory_depends_on": "eval:doc.twitter ==1"
"mandatory_depends_on": "eval:doc.twitter ==1",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Image"
"label": "Image",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "twitter",
"fieldtype": "Check",
"label": "Twitter"
"label": "Twitter",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "linkedin",
"fieldtype": "Check",
"label": "LinkedIn"
"label": "LinkedIn",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "amended_from",
@ -56,13 +64,17 @@
"no_copy": 1,
"options": "Social Media Post",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.twitter ==1",
"fieldname": "content",
"fieldtype": "Section Break",
"label": "Twitter"
"label": "Twitter",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
@ -70,7 +82,9 @@
"fieldtype": "Select",
"label": "Post Status",
"options": "\nScheduled\nPosted\nError",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
@ -78,7 +92,9 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Twitter Post Id",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
@ -86,68 +102,89 @@
"fieldtype": "Data",
"hidden": 1,
"label": "LinkedIn Post Id",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "campaign_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Campaign",
"options": "Campaign"
"options": "Campaign",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"label": "Share On"
"label": "Share On",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "tweet_preview",
"fieldtype": "HTML"
"fieldtype": "HTML",
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"depends_on": "eval:doc.linkedin==1",
"fieldname": "linkedin_section",
"fieldtype": "Section Break",
"label": "LinkedIn"
"label": "LinkedIn",
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "attachments_section",
"fieldtype": "Section Break",
"label": "Attachments"
"label": "Attachments",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "linkedin_post",
"fieldtype": "Text",
"label": "Post",
"mandatory_depends_on": "eval:doc.linkedin ==1"
"mandatory_depends_on": "eval:doc.linkedin ==1",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "scheduled_time",
"fieldtype": "Datetime",
"label": "Scheduled Time",
"read_only_depends_on": "eval:doc.post_status == \"Posted\""
"read_only_depends_on": "eval:doc.post_status == \"Posted\"",
"show_days": 1,
"show_seconds": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-04-21 15:10:04.953713",
"modified": "2020-06-14 10:31:33.961381",
"modified_by": "Administrator",
"module": "CRM",
"name": "Social Media Post",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@ -157,6 +194,35 @@
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"submit": 1,
"write": 1
}
],

View File

@ -1,398 +1,119 @@
{
"allow_copy": 0,
"actions": [],
"allow_guest_to_view": 1,
"allow_import": 0,
"allow_rename": 1,
"autoname": "",
"beta": 0,
"creation": "2016-09-13 03:05:27.154713",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"route",
"column_break_3",
"academic_year",
"admission_start_date",
"admission_end_date",
"published",
"enable_admission_application",
"section_break_5",
"program_details",
"introduction"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Title"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "route",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Route",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "application_form_route",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Application Form Route",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "academic_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Academic Year",
"length": 0,
"no_copy": 1,
"options": "Academic Year",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "admission_start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Admission Start Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"no_copy": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "admission_end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Admission End Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"no_copy": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Publish on website",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Publish on website"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Eligibility and Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Eligibility and Details"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "program_details",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Eligibility and Details",
"length": 0,
"no_copy": 0,
"options": "Student Admission Program",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "Student Admission Program"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "introduction",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Introduction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Introduction"
},
{
"default": "0",
"fieldname": "enable_admission_application",
"fieldtype": "Check",
"label": "Enable Admission Application"
}
],
"has_web_view": 1,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_published_field": "published",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-11-10 18:57:34.570376",
"links": [],
"modified": "2020-06-15 20:18:38.591626",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Admission",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"route": "admissions",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 0,
"track_seen": 0
"title_field": "title"
}

View File

@ -43,8 +43,8 @@
<thead>
<tr class="active">
<th style="width: 90px">Program/Std.</th>
<th style="width: 170px">Minumum Age(DOB)</th>
<th style="width: 170px">Maximum Age(DOB)</th>
<th style="width: 170px">Minumum Age</th>
<th style="width: 170px">Maximum Age</th>
<th style="width: 100px">Application Fee</th>
</tr>
</thead>
@ -52,8 +52,8 @@
{% for row in program_details %}
<tr>
<td>{{ row.program }}</td>
<td>{{ row.minimum_age }}</td>
<td>{{ row.maximum_age }}</td>
<td>{{ row.min_age }}</td>
<td>{{ row.max_age }}</td>
<td>{{ row.application_fee }}</td>
</tr>
{% endfor %}
@ -61,12 +61,11 @@
</table>
</div>
{% endif %}
{%- if application_form_route -%}
{%- if doc.enable_admission_application -%}
<br>
<p>
<a class='btn btn-primary'
href='/{{ doc.application_form_route }}?new=1'>
href='/student-applicant?new=1&student_admission={{doc.name}}'>
{{ _("Apply Now") }}</a>
</p>
{% endif %}

View File

@ -11,7 +11,7 @@ QUnit.test('Test: Student Admission', function(assert) {
{admission_start_date: '2016-04-20'},
{admission_end_date: '2016-05-31'},
{title: '2016-17 Admissions'},
{application_form_route: 'student-applicant'},
{enable_admission_application: 1},
{introduction: 'Test intro'},
{program_details: [
[
@ -28,7 +28,7 @@ QUnit.test('Test: Student Admission', function(assert) {
assert.ok(cur_frm.doc.admission_start_date == '2016-04-20');
assert.ok(cur_frm.doc.admission_end_date == '2016-05-31');
assert.ok(cur_frm.doc.title == '2016-17 Admissions');
assert.ok(cur_frm.doc.application_form_route == 'student-applicant');
assert.ok(cur_frm.doc.enable_admission_application == 1);
assert.ok(cur_frm.doc.introduction == 'Test intro');
assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected');
assert.ok(cur_frm.doc.program_details[0].application_fee == 1000);

View File

@ -1,237 +1,77 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"actions": [],
"creation": "2017-09-15 12:59:43.207923",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"program",
"min_age",
"max_age",
"column_break_4",
"application_fee",
"applicant_naming_series"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "program",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Program",
"length": 0,
"no_copy": 0,
"options": "Program",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "minimum_age",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Minimum Age",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maximum_age",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Maximum Age",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "application_fee",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Application Fee",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "applicant_naming_series",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Naming Series (for Student Applicant)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "min_age",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Minimum Age",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "max_age",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Maximum Age",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-11-04 03:37:17.408427",
"links": [],
"modified": "2020-06-10 23:06:30.037404",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Admission Program",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from frappe.utils import getdate, add_years, nowdate, date_diff
class StudentApplicant(Document):
def autoname(self):
@ -31,6 +31,7 @@ class StudentApplicant(Document):
def validate(self):
self.validate_dates()
self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
if self.student_admission and self.program and self.date_of_birth:
self.validation_from_student_admission()
@ -48,16 +49,16 @@ class StudentApplicant(Document):
frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant"))
def validation_from_student_admission(self):
student_admission = get_student_admission_data(self.student_admission, self.program)
# different validation for minimum and maximum age so that either min/max can also work independently.
if student_admission and student_admission.minimum_age and \
getdate(student_admission.minimum_age) < getdate(self.date_of_birth):
frappe.throw(_("Not eligible for the admission in this program as per DOB"))
if student_admission and student_admission.min_age and \
date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.min_age)) < 0:
frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth"))
if student_admission and student_admission.maximum_age and \
getdate(student_admission.maximum_age) > getdate(self.date_of_birth):
frappe.throw(_("Not eligible for the admission in this program as per DOB"))
if student_admission and student_admission.max_age and \
date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.max_age)) > 0:
frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth"))
def on_payment_authorized(self, *args, **kwargs):
@ -65,10 +66,12 @@ class StudentApplicant(Document):
def get_student_admission_data(student_admission, program):
student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date,
sap.program, sap.minimum_age, sap.maximum_age, sap.applicant_naming_series
sap.program, sap.min_age, sap.max_age, sap.applicant_naming_series
from `tabStudent Admission` sa, `tabStudent Admission Program` sap
where sa.name = sap.parent and sa.name = %s and sap.program = %s""", (student_admission, program), as_dict=1)
if student_admission:
return student_admission[0]
else:

View File

@ -16,7 +16,7 @@
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2017-02-21 05:44:46.022738",
"modified": "2020-06-11 22:53:45.875310",
"modified_by": "Administrator",
"module": "Education",
"name": "student-applicant",
@ -24,12 +24,16 @@
"payment_button_label": "Buy Now",
"published": 1,
"route": "student-applicant",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 1,
"sidebar_items": [],
"success_url": "/student-applicant",
"title": "Student Applicant",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "first_name",
"fieldtype": "Data",
"hidden": 0,
@ -37,9 +41,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "middle_name",
"fieldtype": "Data",
"hidden": 0,
@ -47,9 +53,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "last_name",
"fieldtype": "Data",
"hidden": 0,
@ -57,9 +65,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "image",
"fieldtype": "Data",
"hidden": 0,
@ -67,9 +77,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "program",
"fieldtype": "Link",
"hidden": 0,
@ -78,9 +90,11 @@
"max_value": 0,
"options": "Program",
"read_only": 0,
"reqd": 1
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "academic_year",
"fieldtype": "Link",
"hidden": 0,
@ -89,9 +103,11 @@
"max_value": 0,
"options": "Academic Year",
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "date_of_birth",
"fieldtype": "Date",
"hidden": 0,
@ -99,9 +115,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "blood_group",
"fieldtype": "Select",
"hidden": 0,
@ -110,9 +128,11 @@
"max_value": 0,
"options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-",
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "student_email_id",
"fieldtype": "Data",
"hidden": 0,
@ -120,9 +140,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "student_mobile_number",
"fieldtype": "Data",
"hidden": 0,
@ -130,9 +152,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"default": "INDIAN",
"fieldname": "nationality",
"fieldtype": "Data",
@ -142,9 +166,11 @@
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "address_line_1",
"fieldtype": "Data",
"hidden": 0,
@ -152,9 +178,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "address_line_2",
"fieldtype": "Data",
"hidden": 0,
@ -162,9 +190,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "pincode",
"fieldtype": "Data",
"hidden": 0,
@ -172,9 +202,11 @@
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "guardians",
"fieldtype": "Table",
"hidden": 0,
@ -183,9 +215,11 @@
"max_value": 0,
"options": "Student Guardian",
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "siblings",
"fieldtype": "Table",
"hidden": 0,
@ -194,7 +228,21 @@
"max_value": 0,
"options": "Student Sibling",
"read_only": 0,
"reqd": 0
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "student_admission",
"fieldtype": "Link",
"hidden": 0,
"label": "Student Admission",
"max_length": 0,
"max_value": 0,
"options": "Student Admission",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@ -15,6 +15,7 @@ class TestInpatientRecord(unittest.TestCase):
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status"))
@ -26,7 +27,7 @@ class TestInpatientRecord(unittest.TestCase):
self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
# Discharge
schedule_discharge(patient=patient)
schedule_discharge(frappe.as_json({'patient': patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
@ -44,8 +45,10 @@ class TestInpatientRecord(unittest.TestCase):
patient = create_patient()
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
ip_record_new = create_inpatient(patient)
ip_record_new.expected_length_of_stay = 0
self.assertRaises(frappe.ValidationError, ip_record_new.save)
service_unit = get_healthcare_service_unit()

View File

@ -93,7 +93,7 @@
"idx": 0,
"is_standard": 1,
"label": "HR",
"modified": "2020-06-10 12:41:41.695669",
"modified": "2020-06-16 19:20:50.976045",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
@ -126,7 +126,7 @@
},
{
"label": "Salary Structure",
"link_to": "Payroll Entry",
"link_to": "Salary Structure",
"type": "DocType"
},
{

View File

@ -205,7 +205,7 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nActive\nLeft",
"options": "Active\nLeft",
"reqd": 1,
"search_index": 1
},
@ -667,6 +667,7 @@
"oldfieldtype": "Date"
},
{
"depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
@ -803,7 +804,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2020-05-05 18:51:03.152503",
"modified": "2020-06-15 12:26:30.003741",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -66,6 +66,7 @@
"fieldname": "employee",
"fieldtype": "Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "From Employee",
"oldfieldname": "employee",
"oldfieldtype": "Link",
@ -164,6 +165,7 @@
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_standard_filter": 1,
"label": "Posting Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
@ -236,6 +238,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
@ -368,7 +371,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2019-12-14 23:52:05.388458",
"modified": "2020-06-15 12:43:04.099803",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim",

View File

@ -4,7 +4,6 @@
import frappe, erpnext, json
from frappe import _
from frappe.utils import nowdate, get_first_day, get_last_day, add_months
from erpnext.accounts.utils import get_fiscal_year
def get_data():
return frappe._dict({

View File

@ -119,6 +119,7 @@ class BOM(WebsiteGenerator):
"description": d.description,
"time_in_mins": d.time_in_mins,
"batch_size": d.batch_size,
"operating_cost": d.operating_cost,
"idx": d.idx
})
child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2)

View File

@ -78,6 +78,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.doctype == 'BOM'",
"fieldname": "base_hour_rate",
"fieldtype": "Currency",
"label": "Base Hour Rate(Company Currency)",
@ -87,6 +88,7 @@
},
{
"default": "5",
"depends_on": "eval:parent.doctype == 'BOM'",
"fieldname": "base_operating_cost",
"fieldtype": "Currency",
"label": "Operating Cost(Company Currency)",
@ -108,8 +110,8 @@
],
"idx": 1,
"istable": 1,
"modified": "2019-07-16 22:35:55.374037",
"modified_by": "govindsmenokee@gmail.com",
"modified": "2020-06-16 17:01:11.128420",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
"owner": "Administrator",

View File

@ -44,7 +44,6 @@ frappe.ui.form.on('BOM Operation', {
name: d.workstation
},
callback: function (data) {
frappe.model.set_value(d.doctype, d.name, "base_hour_rate", data.message.hour_rate);
frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate);
frm.events.calculate_operating_cost(frm, d);
}

View File

@ -697,3 +697,5 @@ execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
erpnext.patches.v12_0.update_uom_conversion_factor
erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
erpnext.patches.v13_0.update_sla_enhancements
erpnext.patches.v12_0.update_address_template_for_india

View File

@ -6,4 +6,6 @@ import frappe
def execute():
''' Move from due_advance_amount to pending_amount '''
frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''')
if frappe.db.has_column("Employee Advance", "due_advance_amount"):
frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''')

View File

@ -0,0 +1,12 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.regional.address_template.setup import set_up_address_templates
def execute():
if frappe.db.get_value('Company', {'country': 'India'}, 'name'):
address_template = frappe.db.get_value('Address Template', 'India', 'template')
if not address_template or "gstin" not in address_template:
set_up_address_templates(default_country='India')

View File

@ -0,0 +1,93 @@
# Copyright (c) 2018, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
# add holiday list and employee group fields in SLA
# change response and resolution time in priorities child table
if frappe.db.exists('DocType', 'Service Level Agreement'):
sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level'])
priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={
'parenttype': ('in', ['Service Level Agreement', 'Service Level'])
})
frappe.reload_doc('support', 'doctype', 'service_level_agreement')
frappe.reload_doc('support', 'doctype', 'pause_sla_on_status')
frappe.reload_doc('support', 'doctype', 'service_level_priority')
frappe.reload_doc('support', 'doctype', 'service_day')
for entry in sla_details:
values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group'])
if values:
holiday_list = values[0]
employee_group = values[1]
frappe.db.set_value('Service Level Agreement', entry.name, {
'holiday_list': holiday_list,
'employee_group': employee_group
})
priority_dict = {}
for priority in priorities:
if priority.parenttype == 'Service Level Agreement':
response_time = convert_to_seconds(priority.response_time, priority.response_time_period)
resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period)
frappe.db.set_value('Service Level Priority', priority.name, {
'response_time': response_time,
'resolution_time': resolution_time
})
if priority.parenttype == 'Service Level':
if not priority.parent in priority_dict:
priority_dict[priority.parent] = []
priority_dict[priority.parent].append(priority)
# copy Service Levels to Service Level Agreements
sl = [entry.service_level for entry in sla_details]
if frappe.db.exists('DocType', 'Service Level'):
service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*'])
for entry in service_levels:
sla = frappe.new_doc('Service Level Agreement')
sla.service_level = entry.service_level
sla.holiday_list = entry.holiday_list
sla.employee_group = entry.employee_group
sla.flags.ignore_validate = True
sla = sla.insert(ignore_mandatory=True)
frappe.db.sql("""
UPDATE
`tabService Day`
SET
parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement'
WHERE
parent = %(old_parent)s
""", {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1)
priority_list = priority_dict.get(entry.name)
if priority_list:
sla = frappe.get_doc('Service Level Agreement', sla.name)
for priority in priority_list:
row = sla.append('priorities', {
'priority': priority.priority,
'default_priority': priority.default_priority,
'response_time': convert_to_seconds(priority.response_time, priority.response_time_period),
'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period)
})
row.db_update()
sla.db_update()
frappe.delete_doc_if_exists('DocType', 'Service Level')
def convert_to_seconds(value, unit):
seconds = 0
if unit == "Hour":
seconds = value * 3600
if unit == "Day":
seconds = value * 3600 * 24
if unit == "Week":
seconds = value * 3600 * 24 * 7
return seconds

View File

@ -1,4 +1,5 @@
import frappe
from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
def get_field_filter_data():
@ -243,6 +244,8 @@ def get_next_attribute_and_values(item_code, selected_attributes):
else:
product_info = None
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
return {
'next_attribute': next_attribute,
'valid_options_for_attributes': valid_options_for_attributes,

View File

@ -64,7 +64,7 @@ class TestTask(unittest.TestCase):
def assign():
from frappe.desk.form import assign_to
assign_to.add({
"assign_to": "test@example.com",
"assign_to": ["test@example.com"],
"doctype": task.doctype,
"name": task.name,
"description": "Close this task"

View File

@ -13,7 +13,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.hr.doctype.salary_structure.test_salary_structure \
import make_salary_structure, create_salary_structure_assignment
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestTimesheet(unittest.TestCase):
def setUp(self):
@ -25,8 +25,10 @@ class TestTimesheet(unittest.TestCase):
def test_timesheet_billing_amount(self):
make_salary_structure_for_timesheet("_T-Employee-00001")
timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1)
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, billable=1)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 2)
@ -35,8 +37,10 @@ class TestTimesheet(unittest.TestCase):
self.assertEqual(timesheet.total_billable_amount, 100)
def test_timesheet_billing_amount_not_billable(self):
make_salary_structure_for_timesheet("_T-Employee-00001")
timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=0)
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, billable=0)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 0)
@ -45,8 +49,10 @@ class TestTimesheet(unittest.TestCase):
self.assertEqual(timesheet.total_billable_amount, 0)
def test_salary_slip_from_timesheet(self):
salary_structure = make_salary_structure_for_timesheet("_T-Employee-00001")
timesheet = make_timesheet("_T-Employee-00001", simulate = True, billable=1)
emp = make_employee("test_employee_6@salary.com")
salary_structure = make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate = True, billable=1)
salary_slip = make_salary_slip(timesheet.name)
salary_slip.submit()
@ -65,7 +71,9 @@ class TestTimesheet(unittest.TestCase):
self.assertEqual(timesheet.status, 'Submitted')
def test_sales_invoice_from_timesheet(self):
timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1)
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, billable=1)
sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer')
sales_invoice.due_date = nowdate()
sales_invoice.submit()
@ -80,7 +88,9 @@ class TestTimesheet(unittest.TestCase):
self.assertEqual(item.rate, 50.00)
def test_timesheet_billing_based_on_project(self):
timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1, project = '_Test Project', company='_Test Company')
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True)
sales_invoice.project = '_Test Project'
sales_invoice.submit()
@ -90,6 +100,8 @@ class TestTimesheet(unittest.TestCase):
self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name)
def test_timesheet_time_overlap(self):
emp = make_employee("test_employee_6@salary.com")
settings = frappe.get_single('Projects Settings')
initial_setting = settings.ignore_employee_time_overlap
settings.ignore_employee_time_overlap = 0
@ -97,7 +109,7 @@ class TestTimesheet(unittest.TestCase):
update_activity_type("_Test Activity Type")
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = "_T-Employee-00001"
timesheet.employee = emp
timesheet.append(
'time_logs',
{
@ -129,12 +141,14 @@ class TestTimesheet(unittest.TestCase):
settings.save()
def test_timesheet_std_working_hours(self):
emp = make_employee("test_employee_6@salary.com")
company = frappe.get_doc('Company', "_Test Company")
company.standard_working_hours = 8
company.save()
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = "_T-Employee-00001"
timesheet.employee = emp
timesheet.company = '_Test Company'
timesheet.append(
'time_logs',
@ -156,7 +170,7 @@ class TestTimesheet(unittest.TestCase):
company.save()
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = "_T-Employee-00001"
timesheet.employee = emp
timesheet.company = '_Test Company'
timesheet.append(
'time_logs',

View File

@ -73,6 +73,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
me.frm.set_query('contact_person', erpnext.queries.contact_query);
me.frm.set_query('supplier_address', erpnext.queries.address_query);
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
if(this.frm.fields_dict.supplier) {
this.frm.set_query("supplier", function() {
return{ query: "erpnext.controllers.queries.supplier_query" }});
@ -283,6 +285,11 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
"shipping_address_display", true);
},
billing_address: function() {
erpnext.utils.get_address_display(this.frm, "billing_address",
"billing_address_display", true);
},
tc_name: function() {
this.get_terms();
},

View File

@ -1,7 +1,7 @@
{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>
{% if gst_state %}{{ gst_state }}{% endif -%}
{% if gst_state_number %}, State Code: {{ gst_state_number }}<br>{% endif -%}
{% if pincode %}PIN: {{ pincode }}<br>{% endif -%}
{% if pincode %}Postal Code: {{ pincode }}<br>{% endif -%}
{{ country }}<br>
{% if phone %}Phone: {{ phone }}<br>{% endif -%}
{% if fax %}Fax: {{ fax }}<br>{% endif -%}

View File

@ -9,14 +9,14 @@ def setup(company=None, patch=True):
make_custom_fields()
add_print_formats()
def make_custom_fields():
def make_custom_fields(update=True):
custom_fields = {
'Supplier': [
dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id',
label='Is IRS 1099 reporting required for supplier?')
]
}
create_custom_fields(custom_fields)
create_custom_fields(custom_fields, update=update)
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
@ -49,7 +50,7 @@
"fieldname": "terms_and_conditions_help",
"fieldtype": "HTML",
"label": "Terms and Conditions Help",
"options": "<h4>Standard Terms and Conditions Example</h4>\n\n<pre>Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Langauge. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
"options": "<h4>Standard Terms and Conditions Example</h4>\n\n<pre>Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
},
{
"fieldname": "applicable_modules_section",
@ -81,7 +82,8 @@
],
"icon": "icon-legal",
"idx": 1,
"modified": "2019-07-04 13:31:30.393425",
"links": [],
"modified": "2020-06-16 22:54:38.094844",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",

View File

@ -105,3 +105,4 @@ def add_company_to_session_defaults():
"ref_doctype": "Company"
})
settings.save()

View File

@ -42,9 +42,9 @@ def get_cart_quotation(doc=None):
return {
"doc": decorate_quotation_doc(doc),
"shipping_addresses": [{"name": address.name, "display": address.display}
"shipping_addresses": [{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses if address.address_type == "Shipping"],
"billing_addresses": [{"name": address.name, "display": address.display}
"billing_addresses": [{"name": address.name, "title": address.address_title, "display": address.display}
for address in addresses if address.address_type == "Billing"],
"shipping_rules": get_applicable_shipping_rules(party),
"cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
@ -78,8 +78,10 @@ def place_order():
if is_stock_item:
item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{1} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
sales_order.flags.ignore_permissions = True
sales_order.insert()

View File

@ -5,31 +5,26 @@ import frappe
import json
from frappe import _
from frappe.utils import nowdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.dashboard_fixtures import _get_fiscal_year
from erpnext.buying.dashboard_fixtures import get_company_for_dashboards
def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
return frappe._dict({
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards(),
"charts": get_charts(company, fiscal_year_name, start_date, end_date),
"number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date),
})
def get_company_for_dashboards():
company = frappe.defaults.get_defaults().company
if company:
return company
else:
company_list = frappe.get_list("Company")
if company_list:
return company_list[0].name
return None
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year = get_fiscal_year(nowdate(), as_dict=1)
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
def get_dashboards():
return [{
"name": "Stock",
@ -48,7 +43,7 @@ def get_dashboards():
]
}]
def get_charts():
def get_charts(company, fiscal_year_name, start_date, end_date):
return [
{
"doctype": "Dashboard Chart",
@ -133,7 +128,7 @@ def get_charts():
}
]
def get_number_cards():
def get_number_cards(company, fiscal_year_name, start_date, end_date):
return [
{
"name": "Total Active Items",

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
{
"hidden": 0,
"label": "Service Level Agreement",
"links": "[\n {\n \"description\": \"Service Level.\",\n \"label\": \"Service Level\",\n \"name\": \"Service Level\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
"modified": "2020-05-28 13:51:23.869954",
"modified": "2020-06-04 11:54:56.124219",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
@ -65,8 +65,8 @@
"type": "DocType"
},
{
"label": "Service Level",
"link_to": "Service Level",
"label": "Service Level Agreement",
"link_to": "Service Level Agreement",
"type": "DocType"
}
]

View File

@ -38,10 +38,35 @@ frappe.ui.form.on("Issue", {
},
refresh: function (frm) {
if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
if (frm.doc.service_level_agreement) {
set_time_to_resolve_and_response(frm);
frappe.call({
'method': 'frappe.client.get',
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm);
}
}
});
}
frm.add_custom_button(__("Close"), function () {
@ -55,6 +80,7 @@ frappe.ui.form.on("Issue", {
frm: frm
});
}, __("Make"));
} else {
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();

View File

@ -31,9 +31,13 @@
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
"response",
"mins_to_first_response",
"first_responded_on",
"column_break_26",
"avg_response_time",
"additional_info",
"lead",
"contact",
@ -50,7 +54,9 @@
"resolution_date",
"content_type",
"attachment",
"via_customer_portal"
"via_customer_portal",
"resolution_time",
"user_resolution_time"
],
"fields": [
{
@ -114,7 +120,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Open\nReplied\nHold\nClosed",
"options": "Open\nReplied\nHold\nResolved\nClosed",
"search_index": 1
},
{
@ -161,6 +167,7 @@
"options": "Service Level Agreement"
},
{
"depends_on": "eval: doc.status != 'Replied';",
"fieldname": "response_by",
"fieldtype": "Datetime",
"label": "Response By",
@ -174,6 +181,7 @@
"read_only": 1
},
{
"depends_on": "eval: doc.status != 'Replied';",
"fieldname": "resolution_by",
"fieldtype": "Datetime",
"label": "Resolution By",
@ -328,7 +336,7 @@
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement",
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "response_by_variance",
"fieldtype": "Float",
@ -336,7 +344,7 @@
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement",
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "resolution_by_variance",
"fieldtype": "Float",
@ -362,12 +370,48 @@
"label": "Issue Split From",
"options": "Issue",
"read_only": 1
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"bold": 1,
"fieldname": "avg_response_time",
"fieldtype": "Duration",
"label": "Average Response Time",
"read_only": 1
},
{
"fieldname": "resolution_time",
"fieldtype": "Duration",
"label": "Resolution Time",
"read_only": 1
},
{
"fieldname": "user_resolution_time",
"fieldtype": "Duration",
"label": "User Resolution Time",
"read_only": 1
},
{
"fieldname": "on_hold_since",
"fieldtype": "Datetime",
"hidden": 1,
"label": "On Hold Since",
"read_only": 1
},
{
"fieldname": "total_hold_time",
"fieldtype": "Duration",
"label": "Total Hold Time",
"read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
"modified": "2020-03-13 02:19:49.477928",
"modified": "2020-06-10 12:47:37.146914",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",

View File

@ -7,7 +7,7 @@ import json
from frappe import _
from frappe import utils
from frappe.model.document import Document
from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime
from frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime, time_diff_in_seconds, time_diff
from datetime import datetime, timedelta
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
@ -47,8 +47,8 @@ class Issue(Document):
self.contact = frappe.db.get_value("Contact", {"email_id": email_id})
if self.contact:
contact = frappe.get_doc('Contact', self.contact)
self.customer = contact.get_link_for('Customer')
contact = frappe.get_doc("Contact", self.contact)
self.customer = contact.get_link_for("Customer")
if not self.company:
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
@ -56,18 +56,70 @@ class Issue(Document):
def update_status(self):
status = frappe.db.get_value("Issue", self.name, "status")
if self.status!="Open" and status =="Open" and not self.first_responded_on:
if self.status != "Open" and status == "Open" and not self.first_responded_on:
self.first_responded_on = frappe.flags.current_time or now_datetime()
if self.status=="Closed" and status !="Closed":
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
self.resolution_date = frappe.flags.current_time or now_datetime()
if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing":
set_service_level_agreement_variance(issue=self.name)
self.update_agreement_status()
set_resolution_time(issue=self)
set_user_resolution_time(issue=self)
if self.status=="Open" and status !="Open":
if self.status == "Open" and status != "Open":
# if no date, it should be set as None and not a blank string "", as per mysql strict config
self.resolution_date = None
self.reset_issue_metrics()
# enable SLA and variance on Reopen
self.agreement_fulfilled = "Ongoing"
set_service_level_agreement_variance(issue=self.name)
self.handle_hold_time(status)
def handle_hold_time(self, status):
if self.service_level_agreement:
# set response and resolution variance as None as the issue is on Hold for status as Replied
pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
filters={"parent": self.service_level_agreement})
hold_statuses = [entry.status for entry in pause_sla_on]
update_values = {}
if self.status in hold_statuses and status not in hold_statuses:
update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
if not self.first_responded_on:
update_values['response_by'] = None
update_values['response_by_variance'] = 0
update_values['resolution_by'] = None
update_values['resolution_by_variance'] = 0
# calculate hold time when status is changed from Replied to any other status
if self.status not in hold_statuses and status in hold_statuses:
hold_time = self.total_hold_time if self.total_hold_time else 0
now_time = frappe.flags.current_time or now_datetime()
update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, self.on_hold_since)
# re-calculate SLA variables after issue changes from Replied to Open
# add hold time to SLA variables
if self.status == "Open" and status in hold_statuses:
start_date_time = get_datetime(self.service_level_agreement_creation)
priority = get_priority(self)
now_time = frappe.flags.current_time or now_datetime()
hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
if not self.first_responded_on:
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time))
response_by_variance = round(time_diff_in_hours(self.response_by, now_time))
update_values['response_by_variance'] = response_by_variance + (hold_time // 3600)
resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time))
resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time))
update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600)
update_values['on_hold_since'] = None
self.db_set(update_values)
def update_agreement_status(self):
if self.service_level_agreement and self.agreement_fulfilled == "Ongoing":
@ -128,6 +180,7 @@ class Issue(Document):
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
replicated_issue.resolution_by_variance = None
replicated_issue.reset_issue_metrics()
frappe.get_doc(replicated_issue).insert()
@ -137,7 +190,7 @@ class Issue(Document):
communications = frappe.get_all("Communication",
filters={"reference_doctype": "Issue",
"reference_name": comm_to_split_from.reference_name,
"creation": ('>=', comm_to_split_from.creation)})
"creation": (">=", comm_to_split_from.creation)})
for communication in communications:
doc = frappe.get_doc("Communication", communication.name)
@ -173,20 +226,15 @@ class Issue(Document):
self.service_level_agreement = service_level_agreement.name
self.priority = service_level_agreement.default_priority if not priority else priority
service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
priority = service_level_agreement.get_service_level_agreement_priority(self.priority)
priority.update({
"support_and_resolution": service_level_agreement.support_and_resolution,
"holiday_list": service_level_agreement.holiday_list
})
priority = get_priority(self)
if not self.creation:
self.creation = now_datetime()
self.service_level_agreement_creation = now_datetime()
start_date_time = get_datetime(self.service_level_agreement_creation)
self.response_by = get_expected_time_for(parameter='response', service_level=priority, start_date_time=start_date_time)
self.resolution_by = get_expected_time_for(parameter='resolution', service_level=priority, start_date_time=start_date_time)
self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()))
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()))
@ -221,36 +269,41 @@ class Issue(Document):
self.agreement_fulfilled = "Ongoing"
self.save()
def reset_issue_metrics(self):
self.db_set("resolution_time", None)
self.db_set("user_resolution_time", None)
def get_priority(issue):
service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement)
priority = service_level_agreement.get_service_level_agreement_priority(issue.priority)
priority.update({
"support_and_resolution": service_level_agreement.support_and_resolution,
"holiday_list": service_level_agreement.holiday_list
})
return priority
def get_expected_time_for(parameter, service_level, start_date_time):
current_date_time = start_date_time
expected_time = current_date_time
start_time = None
end_time = None
# lets assume response time is in days by default
if parameter == 'response':
allotted_days = service_level.get("response_time")
time_period = service_level.get("response_time_period")
elif parameter == 'resolution':
allotted_days = service_level.get("resolution_time")
time_period = service_level.get("resolution_time_period")
if parameter == "response":
allotted_seconds = service_level.get("response_time")
elif parameter == "resolution":
allotted_seconds = service_level.get("resolution_time")
else:
frappe.throw(_("{0} parameter is invalid").format(parameter))
allotted_hours = 0
if time_period == 'Hour':
allotted_hours = allotted_days
allotted_days = 0
elif time_period == 'Week':
allotted_days *= 7
expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0
expected_time_is_set = 0
support_days = {}
for service in service_level.get("support_and_resolution"):
support_days[service.workday] = frappe._dict({
'start_time': service.start_time,
'end_time': service.end_time,
"start_time": service.start_time,
"end_time": service.end_time,
})
holidays = get_holidays(service_level.get("holiday_list"))
@ -264,25 +317,22 @@ def get_expected_time_for(parameter, service_level, start_date_time):
if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \
else support_days[current_weekday].start_time
end_time = support_days[current_weekday].end_time
time_left_today = time_diff_in_hours(end_time, start_time)
time_left_today = time_diff_in_seconds(end_time, start_time)
# no time left for support today
if time_left_today < 0: pass
elif time_period == 'Hour':
if time_left_today >= allotted_hours:
if time_left_today <= 0: pass
elif allotted_seconds:
if time_left_today >= allotted_seconds:
expected_time = datetime.combine(getdate(current_date_time), get_time(start_time))
expected_time = add_to_date(expected_time, hours=allotted_hours)
expected_time = add_to_date(expected_time, seconds=allotted_seconds)
expected_time_is_set = 1
else:
allotted_hours = allotted_hours - time_left_today
else:
allotted_days -= 1
expected_time_is_set = allotted_days <= 0
allotted_seconds = allotted_seconds - time_left_today
if not expected_time_is_set:
current_date_time = add_to_date(current_date_time, days=1)
if end_time and time_period != 'Hour':
if end_time and allotted_seconds >= 86400:
current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time))
else:
current_date_time = expected_time
@ -311,6 +361,36 @@ def set_service_level_agreement_variance(issue=None):
if variance < 0:
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
def set_resolution_time(issue):
# total time taken from issue creation to closing
resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation)
issue.db_set("resolution_time", resolution_time)
def set_user_resolution_time(issue):
# total time taken by a user to close the issue apart from wait_time
communications = frappe.get_list("Communication", filters={
"reference_doctype": issue.doctype,
"reference_name": issue.name
},
fields=["sent_or_received", "name", "creation"],
order_by="creation"
)
pending_time = []
for i in range(len(communications)):
if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent":
wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation)
if wait_time > 0:
pending_time.append(wait_time)
total_pending_time = sum(pending_time)
resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation)
user_resolution_time = resolution_time_in_secs - total_pending_time
issue.db_set("user_resolution_time", user_resolution_time)
def get_list_context(context=None):
return {
"title": _("Issues"),
@ -318,7 +398,7 @@ def get_list_context(context=None):
"row_template": "templates/includes/issue_row.html",
"show_sidebar": True,
"show_search": True,
'no_breadcrumbs': True
"no_breadcrumbs": True
}
@ -326,12 +406,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
from frappe.www.list import get_list
user = frappe.session.user
contact = frappe.db.get_value('Contact', {'user': user}, 'name')
contact = frappe.db.get_value("Contact", {"user": user}, "name")
customer = None
if contact:
contact_doc = frappe.get_doc('Contact', contact)
customer = contact_doc.get_link_for('Customer')
contact_doc = frappe.get_doc("Contact", contact)
customer = contact_doc.get_link_for("Customer")
ignore_permissions = False
if is_website_user():

View File

@ -5,15 +5,18 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
from frappe.utils import now_datetime, get_datetime
from frappe.utils import now_datetime, get_datetime, flt
import datetime
from datetime import timedelta
class TestIssue(unittest.TestCase):
def test_response_time_and_resolution_time_based_on_different_sla(self):
def setUp(self):
frappe.db.sql("delete from `tabService Level Agreement`")
frappe.db.sql("delete from `tabEmployee`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_agreements_for_issues()
def test_response_time_and_resolution_time_based_on_different_sla(self):
creation = datetime.datetime(2019, 3, 4, 12, 0)
# make issue with customer specific SLA
@ -72,8 +75,67 @@ class TestIssue(unittest.TestCase):
self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
def make_issue(creation=None, customer=None, index=0):
def test_issue_metrics(self):
creation = datetime.datetime(2020, 3, 4, 4, 0)
issue = make_issue(creation, index=1)
create_communication(issue.name, "test@example.com", "Received", creation)
creation = datetime.datetime(2020, 3, 4, 4, 15)
create_communication(issue.name, "test@admin.com", "Sent", creation)
creation = datetime.datetime(2020, 3, 4, 5, 0)
create_communication(issue.name, "test@example.com", "Received", creation)
creation = datetime.datetime(2020, 3, 4, 5, 5)
create_communication(issue.name, "test@admin.com", "Sent", creation)
frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5)
issue.reload()
issue.status = 'Closed'
issue.save()
self.assertEqual(issue.avg_response_time, 600)
self.assertEqual(issue.resolution_time, 3900)
self.assertEqual(issue.user_resolution_time, 1200)
def test_hold_time_on_replied(self):
creation = datetime.datetime(2020, 3, 4, 4, 0)
issue = make_issue(creation, index=1)
create_communication(issue.name, "test@example.com", "Received", creation)
creation = datetime.datetime(2020, 3, 4, 4, 15)
create_communication(issue.name, "test@admin.com", "Sent", creation)
frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15)
issue.reload()
issue.status = 'Replied'
issue.save()
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
creation = datetime.datetime(2020, 3, 4, 5, 0)
frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 0)
create_communication(issue.name, "test@example.com", "Received", creation)
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
self.assertEqual(issue.resolution_by, datetime.datetime(2020, 3, 4, 16, 45))
creation = datetime.datetime(2020, 3, 4, 5, 5)
create_communication(issue.name, "test@admin.com", "Sent", creation)
frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5)
issue.reload()
issue.status = 'Closed'
issue.save()
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
def make_issue(creation=None, customer=None, index=0):
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),
@ -86,6 +148,7 @@ def make_issue(creation=None, customer=None, index=0):
return issue
def create_customer(name, customer_group, territory):
create_customer_group(customer_group)
@ -99,6 +162,7 @@ def create_customer(name, customer_group, territory):
"territory": territory
}).insert(ignore_permissions=True)
def create_customer_group(customer_group):
if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}):
@ -107,6 +171,7 @@ def create_customer_group(customer_group):
"customer_group_name": customer_group
}).insert(ignore_permissions=True)
def create_territory(territory):
if not frappe.db.exists("Territory", {"territory_name": territory}):
@ -114,3 +179,21 @@ def create_territory(territory):
"doctype": "Territory",
"territory_name": territory,
}).insert(ignore_permissions=True)
def create_communication(reference_name, sender, sent_or_received, creation):
issue = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"communication_medium": "Email",
"sent_or_received": sent_or_received,
"email_status": "Open",
"subject": "Test Issue",
"sender": sender,
"content": "Test",
"status": "Linked",
"reference_doctype": "Issue",
"creation": creation,
"reference_name": reference_name
})
issue.save()

View File

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2020-06-05 13:59:43.265588",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-06-05 15:15:29.986608",
"modified_by": "Administrator",
"module": "Support",
"name": "Pause SLA On Status",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PauseSLAOnStatus(Document):
pass

View File

@ -1,6 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Service Level', {
});

View File

@ -1,111 +0,0 @@
{
"autoname": "field:service_level",
"creation": "2018-11-19 12:44:30.407502",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"service_level",
"employee_group",
"column_break_2",
"holiday_list",
"default_priority",
"response_and_resoution_time",
"priorities",
"section_break_01",
"support_and_resolution"
],
"fields": [
{
"fieldname": "service_level",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Level",
"reqd": 1,
"unique": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "holiday_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Holiday List (ignored during SLA calculation)",
"options": "Holiday List",
"reqd": 1
},
{
"fieldname": "employee_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee Group",
"options": "Employee Group"
},
{
"fieldname": "response_and_resoution_time",
"fieldtype": "Section Break",
"label": "Response and Resoution Time"
},
{
"fieldname": "section_break_01",
"fieldtype": "Section Break",
"label": "Support Hours"
},
{
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"label": "Support and Resolution",
"options": "Service Day",
"reqd": 1
},
{
"fieldname": "priorities",
"fieldtype": "Table",
"label": "Priorities",
"options": "Service Level Priority",
"reqd": 1
},
{
"fieldname": "default_priority",
"fieldtype": "Link",
"label": "Default Priority",
"options": "Issue Priority",
"read_only": 1
}
],
"modified": "2019-06-06 12:58:03.464056",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -1,95 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from datetime import datetime
from frappe.utils import get_weekdays
class ServiceLevel(Document):
def validate(self):
self.check_priorities()
self.check_support_and_resolution()
def check_priorities(self):
default_priority = []
priorities = []
for priority in self.priorities:
# Check if response and resolution time is set for every priority
if not (priority.response_time or priority.resolution_time):
frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx))
priorities.append(priority.priority)
if priority.default_priority:
default_priority.append(priority.default_priority)
if priority.response_time_period == "Hour":
response = priority.response_time * 0.0416667
elif priority.response_time_period == "Day":
response = priority.response_time
elif priority.response_time_period == "Week":
response = priority.response_time * 7
if priority.resolution_time_period == "Hour":
resolution = priority.resolution_time * 0.0416667
elif priority.resolution_time_period == "Day":
resolution = priority.resolution_time
elif priority.resolution_time_period == "Week":
resolution = priority.resolution_time * 7
if response > resolution:
frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx))
# Check if repeated priority
if not len(set(priorities)) == len(priorities):
repeated_priority = get_repeated(priorities)
frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
# Check if repeated default priority
if not len(set(default_priority)) == len(default_priority):
frappe.throw(_("Select only one Priority as Default."))
# set default priority from priorities
try:
self.default_priority = next(d.priority for d in self.priorities if d.default_priority)
except Exception:
frappe.throw(_("Select a Default Priority."))
def check_support_and_resolution(self):
week = get_weekdays()
support_days = []
for support_and_resolution in self.support_and_resolution:
# Check if start and end time is set for every support day
if not (support_and_resolution.start_time or support_and_resolution.end_time):
frappe.throw(_("Set Start Time and End Time for \
Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx)))
support_days.append(support_and_resolution.workday)
support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
if support_and_resolution.start_time >= support_and_resolution.end_time:
frappe.throw(_("Start Time can't be greater than or equal to End Time \
for {0}.".format(support_and_resolution.workday)))
# Check for repeated workday
if not len(set(support_days)) == len(support_days):
repeated_days = get_repeated(support_days)
frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
def get_repeated(values):
unique_list = []
diff = []
for value in values:
if value not in unique_list:
unique_list.append(str(value))
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)

View File

@ -1,12 +0,0 @@
from frappe import _
def get_data():
return {
'fieldname': 'service_level',
'transactions': [
{
'label': _('Service Level Agreement'),
'items': ['Service Level Agreement']
}
]
}

View File

@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group
from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities
import frappe
import unittest
class TestServiceLevel(unittest.TestCase):
def test_service_level(self):
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
# Default Service Level
test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
get_make_service_level = get_service_level("__Test Service Level")
self.assertEqual(test_make_service_level.name, get_make_service_level.name)
self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
# Service Level
test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)
get_make_service_level = get_service_level("_Test Service Level")
self.assertEqual(test_make_service_level.name, get_make_service_level.name)
self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list)
self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group)
def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time):
sl = frappe.get_doc({
"doctype": "Service Level",
"service_level": service_level,
"holiday_list": holiday_list,
"employee_group": employee_group,
"priorities": [
{
"priority": "Low",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "Medium",
"response_time": response_time,
"default_priority": 1,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
},
{
"priority": "High",
"response_time": response_time,
"response_time_period": "Hour",
"resolution_time": resolution_time,
"resolution_time_period": "Hour",
}
],
"support_and_resolution": [
{
"workday": "Monday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Tuesday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Wednesday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Thursday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Friday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Saturday",
"start_time": "10:00:00",
"end_time": "18:00:00",
},
{
"workday": "Sunday",
"start_time": "10:00:00",
"end_time": "18:00:00",
}
]
})
sl_exists = frappe.db.exists("Service Level", {"service_level": service_level})
if not sl_exists:
sl.insert()
return sl
else:
return frappe.get_doc("Service Level", {"service_level": service_level})
def get_service_level(service_level):
return frappe.get_doc("Service Level", service_level)
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:
now = frappe.utils.now_datetime()
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": "__Test Holiday List",
"from_date": "2019-01-01",
"to_date": "2019-12-31",
"holidays": [
{
"description": "Test Holiday 1",
"holiday_date": "2019-03-05"
},
{
"description": "Test Holiday 2",
"holiday_date": "2019-03-07"
},
{
"description": "Test Holiday 3",
"holiday_date": "2019-02-11"
},
]
}).insert()
def create_service_level_for_sla():
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
# Default Service Level
create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6)
# Service Level
create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3)

View File

@ -2,28 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('Service Level Agreement', {
service_level: function(frm) {
frm.fields_dict.support_and_resolution.grid.remove_all();
frappe.call({
"method": "frappe.client.get",
args: {
doctype: "Service Level",
name: frm.doc.service_level
},
callback: function(data){
let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length);
let i = 0;
while (i < count){
if (data.message.priorities[i]) {
frm.add_child("priorities", data.message.priorities[i]);
}
if (data.message.support_and_resolution[i]) {
frm.add_child("support_and_resolution", data.message.support_and_resolution[i]);
}
i++;
}
frm.refresh();
}
setup: function(frm) {
let allow_statuses = [];
const exclude_statuses = ['Open', 'Closed', 'Resolved'];
frappe.model.with_doctype('Issue', () => {
let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options;
statuses = statuses.split('\n');
allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status));
frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses);
});
},
}
});

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "format:SLA-{service_level}-{####}",
"creation": "2018-12-26 21:08:15.448812",
"doctype": "DocType",
@ -6,12 +7,13 @@
"engine": "InnoDB",
"field_order": [
"enable",
"section_break_2",
"service_level",
"default_priority",
"default_service_level_agreement",
"holiday_list",
"column_break_2",
"employee_group",
"default_priority",
"holiday_list",
"entity_section",
"entity_type",
"column_break_10",
@ -21,49 +23,40 @@
"active",
"column_break_7",
"end_date",
"section_break_18",
"pause_sla_on",
"response_and_resolution_time_section",
"priorities",
"support_and_resolution_section_break",
"support_and_resolution"
],
"fields": [
{
"default": "0",
"depends_on": "eval: !doc.customer;",
"fieldname": "default_service_level_agreement",
"fieldtype": "Check",
"label": "Default Service Level Agreement"
},
{
"fieldname": "service_level",
"fieldtype": "Link",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Service Level",
"options": "Service Level",
"reqd": 1
},
{
"fetch_from": "service_level.holiday_list",
"fieldname": "holiday_list",
"fieldtype": "Link",
"label": "Holiday List",
"options": "Holiday List",
"read_only": 1
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fetch_from": "service_level.employee_group",
"fieldname": "employee_group",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee Group",
"options": "Employee Group",
"read_only": 1
"options": "Employee Group"
},
{
"fieldname": "agreement_details_section",
@ -103,21 +96,15 @@
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"label": "Support and Resolution",
"options": "Service Day"
"options": "Service Day",
"reqd": 1
},
{
"fieldname": "priorities",
"fieldtype": "Table",
"label": "Priorities",
"options": "Service Level Priority"
},
{
"fetch_from": "service_level.default_priority",
"fieldname": "default_priority",
"fieldtype": "Link",
"label": "Default Priority",
"options": "Issue Priority",
"read_only": 1
"options": "Service Level Priority",
"reqd": 1
},
{
"default": "1",
@ -156,9 +143,38 @@
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "default_service_level_agreement",
"fieldtype": "Check",
"label": "Default Service Level Agreement"
},
{
"fieldname": "default_priority",
"fieldtype": "Link",
"label": "Default Priority",
"options": "Issue Priority",
"read_only": 1
},
{
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "pause_sla_on",
"fieldtype": "Table",
"label": "Pause SLA On",
"options": "Pause SLA On Status"
}
],
"modified": "2019-07-09 17:22:16.402939",
"links": [],
"modified": "2020-06-10 12:30:15.050785",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",

View File

@ -6,11 +6,73 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import getdate
from frappe.utils import getdate, get_weekdays
class ServiceLevelAgreement(Document):
def validate(self):
self.validate_doc()
self.check_priorities()
self.check_support_and_resolution()
def check_priorities(self):
default_priority = []
priorities = []
for priority in self.priorities:
# Check if response and resolution time is set for every priority
if not (priority.response_time or priority.resolution_time):
frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx))
priorities.append(priority.priority)
if priority.default_priority:
default_priority.append(priority.default_priority)
response = priority.response_time
resolution = priority.resolution_time
if response > resolution:
frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx))
# Check if repeated priority
if not len(set(priorities)) == len(priorities):
repeated_priority = get_repeated(priorities)
frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
# Check if repeated default priority
if not len(set(default_priority)) == len(default_priority):
frappe.throw(_("Select only one Priority as Default."))
# set default priority from priorities
try:
self.default_priority = next(d.priority for d in self.priorities if d.default_priority)
except Exception:
frappe.throw(_("Select a Default Priority."))
def check_support_and_resolution(self):
week = get_weekdays()
support_days = []
for support_and_resolution in self.support_and_resolution:
# Check if start and end time is set for every support day
if not (support_and_resolution.start_time or support_and_resolution.end_time):
frappe.throw(_("Set Start Time and End Time for \
Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx)))
support_days.append(support_and_resolution.workday)
support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
if support_and_resolution.start_time >= support_and_resolution.end_time:
frappe.throw(_("Start Time can't be greater than or equal to End Time \
for {0}.".format(support_and_resolution.workday)))
# Check for repeated workday
if not len(set(support_days)) == len(support_days):
repeated_days = get_repeated(support_days)
frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
def validate_doc(self):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
frappe.throw(_("Service Level Agreement tracking is not enabled."))
@ -35,9 +97,7 @@ class ServiceLevelAgreement(Document):
return frappe._dict({
"priority": priority.priority,
"response_time": priority.response_time,
"response_time_period": priority.response_time_period,
"resolution_time": priority.resolution_time,
"resolution_time_period": priority.resolution_time_period
"resolution_time": priority.resolution_time
})
def check_agreement_status():
@ -111,3 +171,14 @@ def get_service_level_agreement_filters(name, customer=None):
"priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])],
"service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)]
}
def get_repeated(values):
unique_list = []
diff = []
for value in values:
if value not in unique_list:
unique_list.append(str(value))
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)

View File

@ -5,19 +5,20 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla
from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group
from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities
class TestServiceLevelAgreement(unittest.TestCase):
def test_service_level_agreement(self):
def setUp(self):
frappe.db.sql("delete from `tabService Level Agreement`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_for_sla()
def test_service_level_agreement(self):
# Default Service Level Agreement
create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1,
service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type=None, entity=None, response_time=4, resolution_time=6)
holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type=None, entity=None, response_time=14400, resolution_time=21600)
get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1)
self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name)
@ -28,8 +29,8 @@ class TestServiceLevelAgreement(unittest.TestCase):
# Service Level Agreement for Customer
customer = create_customer()
create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer", entity=customer, response_time=2, resolution_time=3)
holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800)
get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer)
self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name)
@ -40,8 +41,8 @@ class TestServiceLevelAgreement(unittest.TestCase):
# Service Level Agreement for Customer Group
customer_group = create_customer_group()
create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3)
holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800)
get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group)
self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name)
@ -52,8 +53,8 @@ class TestServiceLevelAgreement(unittest.TestCase):
# Service Level Agreement for Territory
territory = create_territory()
create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Territory", entity=territory, response_time=2, resolution_time=3)
holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800)
get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory)
self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name)
@ -71,14 +72,19 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ
service_level_agreement = frappe.get_doc("Service Level Agreement", filters)
return service_level_agreement
def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group,
def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group,
response_time, entity_type, entity, resolution_time):
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
service_level_agreement = frappe.get_doc({
"doctype": "Service Level Agreement",
"enable": 1,
"service_level": "__Test Service Level",
"default_service_level_agreement": default_service_level_agreement,
"service_level": service_level,
"default_priority": "Medium",
"holiday_list": holiday_list,
"employee_group": employee_group,
"entity_type": entity_type,
@ -109,6 +115,11 @@ def create_service_level_agreement(default_service_level_agreement, service_leve
"resolution_time_period": "Hour",
}
],
"pause_sla_on": [
{
"status": "Replied"
}
],
"support_and_resolution": [
{
"workday": "Monday",
@ -167,6 +178,7 @@ def create_service_level_agreement(default_service_level_agreement, service_leve
else:
return frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
def create_customer():
customer = frappe.get_doc({
"doctype": "Customer",
@ -206,23 +218,41 @@ def create_territory():
return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"})
def create_service_level_agreements_for_issues():
create_service_level_for_sla()
create_service_level_agreement(default_service_level_agreement=1,
service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type=None, entity=None, response_time=4, resolution_time=6)
create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List",
employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600)
create_customer()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3)
create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800)
create_customer_group()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3)
create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800)
create_territory()
create_service_level_agreement(default_service_level_agreement=0,
service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3)
create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": "__Test Holiday List",
"from_date": "2019-01-01",
"to_date": "2019-12-31",
"holidays": [
{
"description": "Test Holiday 1",
"holiday_date": "2019-03-05"
},
{
"description": "Test Holiday 2",
"holiday_date": "2019-03-07"
},
{
"description": "Test Holiday 3",
"holiday_date": "2019-02-11"
},
]
}).insert()

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-05-04 05:54:03.658991",
"doctype": "DocType",
"editable_grid": 1,
@ -9,10 +10,8 @@
"default_priority",
"sb_00",
"response_time",
"response_time_period",
"cb_00",
"resolution_time",
"resolution_time_period"
"resolution_time"
],
"fields": [
{
@ -28,16 +27,11 @@
"fieldtype": "Section Break"
},
{
"columns": 1,
"fieldname": "response_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Response Time"
},
{
"columns": 1,
"columns": 2,
"fieldname": "resolution_time",
"fieldtype": "Int",
"fieldtype": "Duration",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Resolution Time"
},
@ -45,36 +39,31 @@
"fieldname": "cb_00",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "response_time_period",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Response Time Period",
"options": "Hour\nDay\nWeek"
},
{
"columns": 2,
"fieldname": "resolution_time_period",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Resolution Time Period",
"options": "Hour\nDay\nWeek"
},
{
"fieldname": "cb_01",
"fieldtype": "Column Break"
},
{
"columns": 1,
"default": "0",
"fieldname": "default_priority",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Default Priority"
},
{
"columns": 2,
"fieldname": "response_time",
"fieldtype": "Duration",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "First Response Time"
}
],
"istable": 1,
"modified": "2019-05-21 06:54:42.674377",
"links": [],
"modified": "2020-06-10 12:45:47.545915",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Priority",

View File

@ -3,6 +3,6 @@
frappe.ui.form.on('Support Settings', {
refresh: function(frm) {
//
}
});

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2017-02-17 13:07:35.686409",
"doctype": "DocType",
"editable_grid": 1,
@ -122,13 +123,15 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_service_level_agreement;",
"fieldname": "allow_resetting_service_level_agreement",
"fieldtype": "Check",
"label": "Allow Resetting Service Level Agreement"
}
],
"issingle": 1,
"modified": "2019-07-10 22:52:39.663873",
"links": [],
"modified": "2020-06-05 17:56:17.491684",
"modified_by": "Administrator",
"module": "Support",
"name": "Support Settings",

View File

@ -193,14 +193,17 @@ class ItemConfigure {
filtered_items_count === 1 ?
filtered_items[0] : '';
// Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock.
const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock;
const add_to_cart = `<a href data-action="btn_add_to_cart" data-item-code="${one_item}">${__('Add to cart')}</a>`;
const product_action = in_stock ? add_to_cart : `<a style="color:#74808b;">${__('Not in Stock')}</a>`;
const item_add_to_cart = one_item ? `
<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
<div>
<div>${one_item} ${product_info && product_info.price ? '(' + product_info.price.formatted_price_sales_uom + ')' : ''}</div>
</div>
<a href data-action="btn_add_to_cart" data-item-code="${one_item}">
${__('Add to cart')}
</a>
${product_action}
</div>
`: '';

View File

@ -16,10 +16,10 @@
<div>{{ introduction }}</div>
{% endif %}
{%- if application_form_route -%}
{%- if doc.enable_admission_application -%}
<p>
<a class='btn btn-primary'
href='/{{ doc.application_form_route }}'>
href='/student-applicant'>
{{ _("Apply Now") }}</a>
</p>
{% endif %}

View File

@ -3,7 +3,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div class="card-body">
<h5 class="card-title">{{ address.name }}</h5>
<h5 class="card-title">{{ address.title }}</h5>
<p class="card-text text-muted">
{{ address.display }}
</p>

View File

@ -109,7 +109,7 @@ frappe.ready(() => {
reqd: 1
},
{
label: __('Pin Code'),
label: __('Postal Code'),
fieldname: 'pincode',
fieldtype: 'Data'
},