Merge pull request #18104 from rohitwaghchaure/incorrect_value_of_accumualated_depreciation_in_the_sales_invoice_develop
fix: incorrect value booked in the accumulated depreciation account on sell of the asset
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js
index 9560b2a..70f193e 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -8,6 +8,7 @@
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
+ "reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index 27c7993..3661afe 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -8,6 +8,7 @@
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
+ "reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 7127663..f0769f6 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -541,10 +541,11 @@
conditions.append("""cost_center in (select name from `tabCost Center` where
lft >= {0} and rgt <= {1})""".format(lft, rgt))
- accounts = [d.name for d in frappe.get_all("Account",
- filters={"account_type": account_type, "company": self.filters.company})]
- conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts)))
- values += accounts
+ if self.filters.company:
+ accounts = [d.name for d in frappe.get_all("Account",
+ filters={"account_type": account_type, "company": self.filters.company})]
+ conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts)))
+ values += accounts
return " and ".join(conditions), values
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js
index f22f3a1..4bc29da 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.js
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js
@@ -10,4 +10,10 @@
"fieldtype": "Check",
"default": 1
});
+
+ frappe.query_reports["Balance Sheet"]["filters"].push({
+ "fieldname": "include_default_book_entries",
+ "label": __("Include Default Book Entries"),
+ "fieldtype": "Check"
+ });
});
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js
index 391f57b..0422111 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.js
+++ b/erpnext/accounts/report/cash_flow/cash_flow.js
@@ -15,4 +15,10 @@
"label": __("Accumulated Values"),
"fieldtype": "Check"
});
+
+ frappe.query_reports["Cash Flow"]["filters"].push({
+ "fieldname": "include_default_book_entries",
+ "label": __("Include Default Book Entries"),
+ "fieldtype": "Check"
+ });
});
\ No newline at end of file
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index f048c1a..75d99e7 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -14,8 +14,8 @@
if cint(frappe.db.get_single_value('Accounts Settings', 'use_custom_cash_flow')):
from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom
return execute_custom(filters=filters)
-
- period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
+
+ period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
filters.periodicity, filters.accumulated_values, filters.company)
cash_flow_accounts = get_cash_flow_accounts()
@@ -25,18 +25,18 @@
accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True)
expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters,
accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True)
-
+
net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company)
data = []
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
-
+
for cash_flow_account in cash_flow_accounts:
section_data = []
data.append({
- "account_name": cash_flow_account['section_header'],
+ "account_name": cash_flow_account['section_header'],
"parent_account": None,
- "indent": 0.0,
+ "indent": 0.0,
"account": cash_flow_account['section_header']
})
@@ -44,18 +44,18 @@
# add first net income in operations section
if net_profit_loss:
net_profit_loss.update({
- "indent": 1,
+ "indent": 1,
"parent_account": cash_flow_accounts[0]['section_header']
})
data.append(net_profit_loss)
section_data.append(net_profit_loss)
for account in cash_flow_account['account_types']:
- account_data = get_account_type_based_data(filters.company,
- account['account_type'], period_list, filters.accumulated_values)
+ account_data = get_account_type_based_data(filters.company,
+ account['account_type'], period_list, filters.accumulated_values, filters)
account_data.update({
"account_name": account['label'],
- "account": account['label'],
+ "account": account['label'],
"indent": 1,
"parent_account": cash_flow_account['section_header'],
"currency": company_currency
@@ -63,7 +63,7 @@
data.append(account_data)
section_data.append(account_data)
- add_total_row_account(data, section_data, cash_flow_account['section_footer'],
+ add_total_row_account(data, section_data, cash_flow_account['section_footer'],
period_list, company_currency)
add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency)
@@ -105,13 +105,15 @@
# combine all cash flow accounts for iteration
return [operation_accounts, investing_accounts, financing_accounts]
-def get_account_type_based_data(company, account_type, period_list, accumulated_values):
+def get_account_type_based_data(company, account_type, period_list, accumulated_values, filters):
data = {}
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
- amount = get_account_type_based_gl_data(company, start_date, period['to_date'], account_type)
+ amount = get_account_type_based_gl_data(company, start_date,
+ period['to_date'], account_type, filters)
+
if amount and account_type == "Depreciation":
amount *= -1
@@ -121,14 +123,24 @@
data["total"] = total
return data
-def get_account_type_based_gl_data(company, start_date, end_date, account_type):
+def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters):
+ cond = ""
+
+ if filters.finance_book:
+ cond = " and finance_book = %s" %(frappe.db.escape(filters.finance_book))
+ if filters.include_default_book_entries:
+ company_fb = frappe.db.get_value("Company", company, 'default_finance_book')
+
+ cond = """ and finance_book in (%s, %s)
+ """ %(frappe.db.escape(filters.finance_book), frappe.db.escape(company_fb))
+
gl_sum = frappe.db.sql_list("""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
and voucher_type != 'Period Closing Voucher'
- and account in ( SELECT name FROM tabAccount WHERE account_type = %s)
- """, (company, start_date, end_date, account_type))
+ and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
+ """.format(cond=cond), (company, start_date, end_date, account_type))
return gl_sum[0] if gl_sum and gl_sum[0] else 0
@@ -154,7 +166,7 @@
key = period if consolidated else period['key']
total_row.setdefault(key, 0.0)
total_row[key] += row.get(key, 0.0)
-
+
total_row.setdefault("total", 0.0)
total_row["total"] += row["total"]
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index 7b373f0..e69a993 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -55,5 +55,10 @@
"fieldtype": "Check",
"default": 0
},
+ {
+ "fieldname": "include_default_book_entries",
+ "label": __("Include Default Book Entries"),
+ "fieldtype": "Check"
+ }
]
}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 0024931..c40310b 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -356,7 +356,8 @@
"lft": root_lft,
"rgt": root_rgt,
"company": d.name,
- "finance_book": filters.get("finance_book")
+ "finance_book": filters.get("finance_book"),
+ "company_fb": frappe.db.get_value("Company", d.name, 'default_finance_book')
},
as_dict=True)
@@ -387,7 +388,10 @@
additional_conditions.append("gl.posting_date >= %(from_date)s")
if filters.get("finance_book"):
- additional_conditions.append("ifnull(finance_book, '') in (%(finance_book)s, '')")
+ if filters.get("include_default_book_entries"):
+ additional_conditions.append("finance_book in (%(finance_book)s, %(company_fb)s)")
+ else:
+ additional_conditions.append("finance_book in (%(finance_book)s)")
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index c06856a..7ef7e00 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -355,6 +355,10 @@
"to_date": to_date,
}
+ if filters.get("include_default_book_entries"):
+ gl_filters["company_fb"] = frappe.db.get_value("Company",
+ company, 'default_finance_book')
+
for key, value in filters.items():
if value:
gl_filters.update({
@@ -399,7 +403,10 @@
additional_conditions.append("cost_center in %(cost_center)s")
if filters.get("finance_book"):
- additional_conditions.append("ifnull(finance_book, '') in (%(finance_book)s, '')")
+ if filters.get("include_default_book_entries"):
+ additional_conditions.append("finance_book in (%(finance_book)s, %(company_fb)s)")
+ else:
+ additional_conditions.append("finance_book in (%(finance_book)s)")
if accounting_dimensions:
for dimension in accounting_dimensions:
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 5c98b24..61e21fd 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -151,6 +151,11 @@
"label": __("Show Opening Entries"),
"fieldtype": "Check"
},
+ {
+ "fieldname": "include_default_book_entries",
+ "label": __("Include Default Book Entries"),
+ "fieldtype": "Check"
+ }
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 1c5e089..f7c0250 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -131,6 +131,10 @@
sum(debit_in_account_currency) as debit_in_account_currency,
sum(credit_in_account_currency) as credit_in_account_currency"""
+ if filters.get("include_default_book_entries"):
+ filters['company_fb'] = frappe.db.get_value("Company",
+ filters.get("company"), 'default_finance_book')
+
gl_entries = frappe.db.sql(
"""
select
@@ -186,7 +190,10 @@
conditions.append("project in %(project)s")
if filters.get("finance_book"):
- conditions.append("ifnull(finance_book, '') in (%(finance_book)s, '')")
+ if filters.get("include_default_book_entries"):
+ conditions.append("finance_book in (%(finance_book)s, %(company_fb)s)")
+ else:
+ conditions.append("finance_book in (%(finance_book)s)")
from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("GL Entry")
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
index df5c982..a8362bf 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
@@ -19,6 +19,11 @@
"fieldname": "accumulated_values",
"label": __("Accumulated Values"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "include_default_book_entries",
+ "label": __("Include Default Book Entries"),
+ "fieldtype": "Check"
}
);
});
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index b5f0186..5758b0b 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -105,7 +105,7 @@
if filters.finance_book:
fb_conditions = " and finance_book = %(finance_book)s"
if filters.include_default_book_entries:
- fb_conditions = " and (finance_book in (%(finance_book)s, %(company_fb)s) or finance_book is null)"
+ fb_conditions = " and (finance_book in (%(finance_book)s, %(company_fb)s))"
additional_conditions += fb_conditions
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 72f5c62..45f7b30 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -291,16 +291,19 @@
def validate_expected_value_after_useful_life(self):
for row in self.get('finance_books'):
- accumulated_depreciation_after_full_schedule = max([d.accumulated_depreciation_amount
- for d in self.get("schedules") if cint(d.finance_book_id) == row.idx])
+ accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount
+ for d in self.get("schedules") if cint(d.finance_book_id) == row.idx]
- asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
- flt(accumulated_depreciation_after_full_schedule),
- self.precision('gross_purchase_amount'))
+ if accumulated_depreciation_after_full_schedule:
+ accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule)
- if row.expected_value_after_useful_life < asset_value_after_full_schedule:
- frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}")
- .format(row.idx, asset_value_after_full_schedule))
+ asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
+ flt(accumulated_depreciation_after_full_schedule),
+ self.precision('gross_purchase_amount'))
+
+ if row.expected_value_after_useful_life < asset_value_after_full_schedule:
+ frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}")
+ .format(row.idx, asset_value_after_full_schedule))
def validate_cancellation(self):
if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"):
diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js
index 8e262f1..3b95a17 100644
--- a/erpnext/assets/doctype/asset/asset_list.js
+++ b/erpnext/assets/doctype/asset/asset_list.js
@@ -1,3 +1,37 @@
frappe.listview_settings['Asset'] = {
- add_fields: ['image']
+ add_fields: ['status'],
+ get_indicator: function (doc) {
+ if (doc.status === "Fully Depreciated") {
+ return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"];
+
+ } else if (doc.status === "Partially Depreciated") {
+ return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"];
+
+ } else if (doc.status === "Sold") {
+ return [__("Sold"), "green", "status,=,Sold"];
+
+ } else if (doc.status === "Scrapped") {
+ return [__("Scrapped"), "grey", "status,=,Scrapped"];
+
+ } else if (doc.status === "In Maintenance") {
+ return [__("In Maintenance"), "orange", "status,=,In Maintenance"];
+
+ } else if (doc.status === "Out of Order") {
+ return [__("Out of Order"), "grey", "status,=,Out of Order"];
+
+ } else if (doc.status === "Issue") {
+ return [__("Issue"), "orange", "status,=,Issue"];
+
+ } else if (doc.status === "Receipt") {
+ return [__("Receipt"), "green", "status,=,Receipt"];
+
+ } else if (doc.status === "Submitted") {
+ return [__("Submitted"), "blue", "status,=,Submitted"];
+
+ } else if (doc.status === "Draft") {
+ return [__("Draft"), "red", "status,=,Draft"];
+
+ }
+
+ },
}
\ No newline at end of file
diff --git a/erpnext/communication/__init__.py b/erpnext/communication/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/__init__.py
diff --git a/erpnext/communication/doctype/__init__.py b/erpnext/communication/doctype/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/__init__.py
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/communication/doctype/call_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/__init__.py
diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json
new file mode 100644
index 0000000..110030d
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/call_log.json
@@ -0,0 +1,106 @@
+{
+ "autoname": "field:id",
+ "creation": "2019-06-05 12:07:02.634534",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "id",
+ "from",
+ "to",
+ "column_break_3",
+ "medium",
+ "section_break_5",
+ "status",
+ "duration",
+ "recording_url",
+ "summary"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "id",
+ "fieldtype": "Data",
+ "label": "ID",
+ "read_only": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "from",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "From",
+ "read_only": 1
+ },
+ {
+ "fieldname": "to",
+ "fieldtype": "Data",
+ "label": "To",
+ "read_only": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Ringing\nIn Progress\nCompleted\nMissed",
+ "read_only": 1
+ },
+ {
+ "description": "Call Duration in seconds",
+ "fieldname": "duration",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Duration",
+ "read_only": 1
+ },
+ {
+ "fieldname": "summary",
+ "fieldtype": "Data",
+ "label": "Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "recording_url",
+ "fieldtype": "Data",
+ "label": "Recording URL",
+ "read_only": 1
+ },
+ {
+ "fieldname": "medium",
+ "fieldtype": "Data",
+ "label": "Medium",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "modified": "2019-07-01 09:09:48.516722",
+ "modified_by": "Administrator",
+ "module": "Communication",
+ "name": "Call Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "title_field": "from",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py
new file mode 100644
index 0000000..66f1064
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/call_log.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, 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
+from erpnext.crm.doctype.utils import get_employee_emails_for_popup
+
+class CallLog(Document):
+ def after_insert(self):
+ employee_emails = get_employee_emails_for_popup(self.medium)
+ for email in employee_emails:
+ frappe.publish_realtime('show_call_popup', self, user=email)
+
+ def on_update(self):
+ doc_before_save = self.get_doc_before_save()
+ if doc_before_save and doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']:
+ frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self)
diff --git a/erpnext/communication/doctype/communication_medium/__init__.py b/erpnext/communication/doctype/communication_medium/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/__init__.py
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json
new file mode 100644
index 0000000..f009b38
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.json
@@ -0,0 +1,81 @@
+{
+ "autoname": "Prompt",
+ "creation": "2019-06-05 11:48:30.572795",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "communication_medium_type",
+ "catch_all",
+ "column_break_3",
+ "provider",
+ "disabled",
+ "timeslots_section",
+ "timeslots"
+ ],
+ "fields": [
+ {
+ "fieldname": "communication_medium_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Communication Medium Type",
+ "options": "Voice\nEmail\nChat",
+ "reqd": 1
+ },
+ {
+ "description": "If there is no assigned timeslot, then communication will be handled by this group",
+ "fieldname": "catch_all",
+ "fieldtype": "Link",
+ "label": "Catch All",
+ "options": "Employee Group"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "provider",
+ "fieldtype": "Link",
+ "label": "Provider",
+ "options": "Supplier"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "timeslots_section",
+ "fieldtype": "Section Break",
+ "label": "Timeslots"
+ },
+ {
+ "fieldname": "timeslots",
+ "fieldtype": "Table",
+ "label": "Timeslots",
+ "options": "Communication Medium Timeslot"
+ }
+ ],
+ "modified": "2019-06-05 11:49:30.769006",
+ "modified_by": "Administrator",
+ "module": "Communication",
+ "name": "Communication Medium",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.py b/erpnext/communication/doctype/communication_medium/communication_medium.py
new file mode 100644
index 0000000..f233da0
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, 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 CommunicationMedium(Document):
+ pass
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/__init__.py b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json
new file mode 100644
index 0000000..b278ca0
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json
@@ -0,0 +1,56 @@
+{
+ "creation": "2019-06-05 11:43:38.897272",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "day_of_week",
+ "from_time",
+ "to_time",
+ "employee_group"
+ ],
+ "fields": [
+ {
+ "fieldname": "day_of_week",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Day of Week",
+ "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
+ "reqd": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "From Time",
+ "reqd": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "To Time",
+ "reqd": 1
+ },
+ {
+ "fieldname": "employee_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee Group",
+ "options": "Employee Group",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "modified": "2019-06-05 12:19:59.994979",
+ "modified_by": "Administrator",
+ "module": "Communication",
+ "name": "Communication Medium Timeslot",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py
new file mode 100644
index 0000000..d68d2d6
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, 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 CommunicationMediumTimeslot(Document):
+ pass
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1b8dd57..f6914b6 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -392,7 +392,7 @@
def validate_qty_is_not_zero(self):
for item in self.items:
if not item.qty:
- frappe.throw("Item quantity can not be zero")
+ frappe.throw(_("Item quantity can not be zero"))
def validate_account_currency(self, account, account_currency=None):
valid_currency = [self.company_currency]
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index adac8f5..afea4a1 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -9,7 +9,6 @@
from erpnext.setup.utils import get_exchange_rate
from erpnext.utilities.transaction_base import TransactionBase
from erpnext.accounts.party import get_party_account_currency
-from frappe.desk.form import assign_to
from frappe.email.inbox import link_communication_to_document
subject_field = "title"
@@ -155,9 +154,6 @@
def on_update(self):
self.add_calendar_event()
- # assign to customer account manager or lead owner
- assign_to_user(self, subject_field)
-
def add_calendar_event(self, opts=None, force=False):
if not opts:
opts = frappe._dict()
@@ -335,21 +331,6 @@
doc.flags.ignore_mandatory = True
doc.save()
-def assign_to_user(doc, subject_field):
- assign_user = None
- if doc.customer:
- assign_user = frappe.db.get_value('Customer', doc.customer, 'account_manager')
- elif doc.lead:
- assign_user = frappe.db.get_value('Lead', doc.lead, 'lead_owner')
-
- if assign_user and assign_user not in ['Administrator', 'Guest']:
- if not assign_to.get(dict(doctype = doc.doctype, name = doc.name)):
- assign_to.add({
- "assign_to": assign_user,
- "doctype": doc.doctype,
- "name": doc.name,
- "description": doc.get(subject_field)
- })
@frappe.whitelist()
def make_opportunity_from_communication(communication, ignore_communication_links=False):
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py
index 0dab01d..9cbbb86 100644
--- a/erpnext/crm/doctype/opportunity/test_opportunity.py
+++ b/erpnext/crm/doctype/opportunity/test_opportunity.py
@@ -7,7 +7,6 @@
from erpnext.crm.doctype.lead.lead import make_customer
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
import unittest
-from frappe.desk.form import assign_to
test_records = frappe.get_test_records('Opportunity')
@@ -61,20 +60,6 @@
self.assertEqual(opp_doc.enquiry_from, "Customer")
self.assertEqual(opp_doc.customer, customer.name)
- def test_assignment(self):
- # assign cutomer account manager
- frappe.db.set_value('Customer', '_Test Customer', 'account_manager', 'test1@example.com')
- doc = make_opportunity(with_items=0)
-
- self.assertEqual(assign_to.get(dict(doctype = doc.doctype, name = doc.name))[0].get('owner'), 'test1@example.com')
-
- # assign lead owner
- frappe.db.set_value('Customer', '_Test Customer', 'account_manager', '')
- frappe.db.set_value('Lead', '_T-Lead-00001', 'lead_owner', 'test2@example.com')
- doc = make_opportunity(with_items=0, enquiry_from='Lead')
-
- self.assertEqual(assign_to.get(dict(doctype = doc.doctype, name = doc.name))[0].get('owner'), 'test2@example.com')
-
def make_opportunity(**args):
args = frappe._dict(args)
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
new file mode 100644
index 0000000..bd8b678
--- /dev/null
+++ b/erpnext/crm/doctype/utils.py
@@ -0,0 +1,100 @@
+import frappe
+from frappe import _
+import json
+
+@frappe.whitelist()
+def get_document_with_phone_number(number):
+ # finds contacts and leads
+ if not number: return
+ number = number.lstrip('0')
+ number_filter = {
+ 'phone': ['like', '%{}'.format(number)],
+ 'mobile_no': ['like', '%{}'.format(number)]
+ }
+ contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1)
+
+ if contacts:
+ return frappe.get_doc('Contact', contacts[0].name)
+
+ leads = frappe.get_all('Lead', or_filters=number_filter, limit=1)
+
+ if leads:
+ return frappe.get_doc('Lead', leads[0].name)
+
+@frappe.whitelist()
+def get_last_interaction(number, reference_doc):
+ reference_doc = json.loads(reference_doc) if reference_doc else get_document_with_phone_number(number)
+
+ if not reference_doc: return
+
+ reference_doc = frappe._dict(reference_doc)
+
+ last_communication = {}
+ last_issue = {}
+ if reference_doc.doctype == 'Contact':
+ customer_name = ''
+ query_condition = ''
+ for link in reference_doc.links:
+ link = frappe._dict(link)
+ if link.link_doctype == 'Customer':
+ customer_name = link.link_name
+ query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name)
+
+ if query_condition:
+ query_condition = query_condition[:-2]
+ last_communication = frappe.db.sql("""
+ SELECT `name`, `content`
+ FROM `tabCommunication`
+ WHERE {}
+ ORDER BY `modified`
+ LIMIT 1
+ """.format(query_condition)) # nosec
+
+ if customer_name:
+ last_issue = frappe.get_all('Issue', {
+ 'customer': customer_name
+ }, ['name', 'subject', 'customer'], limit=1)
+
+ elif reference_doc.doctype == 'Lead':
+ last_communication = frappe.get_all('Communication', filters={
+ 'reference_doctype': reference_doc.doctype,
+ 'reference_name': reference_doc.name,
+ 'sent_or_received': 'Received'
+ }, fields=['name', 'content'], limit=1)
+
+ return {
+ 'last_communication': last_communication[0] if last_communication else None,
+ 'last_issue': last_issue[0] if last_issue else None
+ }
+
+@frappe.whitelist()
+def add_call_summary(docname, summary):
+ call_log = frappe.get_doc('Call Log', docname)
+ summary = _('Call Summary by {0}: {1}').format(
+ frappe.utils.get_fullname(frappe.session.user), summary)
+ if not call_log.summary:
+ call_log.summary = summary
+ else:
+ call_log.summary += '<br>' + summary
+ call_log.save(ignore_permissions=True)
+
+def get_employee_emails_for_popup(communication_medium):
+ now_time = frappe.utils.nowtime()
+ weekday = frappe.utils.get_weekday()
+
+ available_employee_groups = frappe.get_all("Communication Medium Timeslot", filters={
+ 'day_of_week': weekday,
+ 'parent': communication_medium,
+ 'from_time': ['<=', now_time],
+ 'to_time': ['>=', now_time],
+ }, fields=['employee_group'], debug=1)
+
+ available_employee_groups = tuple([emp.employee_group for emp in available_employee_groups])
+
+ employees = frappe.get_all('Employee Group Table', filters={
+ 'parent': ['in', available_employee_groups]
+ }, fields=['user_id'])
+
+ employee_emails = set([employee.user_id for employee in employees])
+
+ return employee_emails
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
new file mode 100644
index 0000000..72f47b5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
@@ -0,0 +1,61 @@
+{
+ "creation": "2019-05-21 07:41:53.536536",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "enabled",
+ "section_break_2",
+ "account_sid",
+ "api_key",
+ "api_token"
+ ],
+ "fields": [
+ {
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "account_sid",
+ "fieldtype": "Data",
+ "label": "Account SID"
+ },
+ {
+ "fieldname": "api_token",
+ "fieldtype": "Data",
+ "label": "API Token"
+ },
+ {
+ "fieldname": "api_key",
+ "fieldtype": "Data",
+ "label": "API Key"
+ }
+ ],
+ "issingle": 1,
+ "modified": "2019-05-22 06:25:18.026997",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Exotel Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
new file mode 100644
index 0000000..77de84c
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, 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
+import requests
+import frappe
+from frappe import _
+
+class ExotelSettings(Document):
+ def validate(self):
+ self.verify_credentials()
+
+ def verify_credentials(self):
+ if self.enabled:
+ response = requests.get('https://api.exotel.com/v1/Accounts/{sid}'
+ .format(sid = self.account_sid), auth=(self.api_key, self.api_token))
+ if response.status_code != 200:
+ frappe.throw(_("Invalid credentials"))
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
new file mode 100644
index 0000000..c04cedc
--- /dev/null
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -0,0 +1,101 @@
+import frappe
+import requests
+
+# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call
+# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call
+# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call
+
+@frappe.whitelist(allow_guest=True)
+def handle_incoming_call(**kwargs):
+ exotel_settings = get_exotel_settings()
+ if not exotel_settings.enabled: return
+
+ call_payload = kwargs
+ status = call_payload.get('Status')
+ if status == 'free':
+ return
+
+ call_log = get_call_log(call_payload)
+ if not call_log:
+ create_call_log(call_payload)
+
+@frappe.whitelist(allow_guest=True)
+def handle_end_call(**kwargs):
+ update_call_log(kwargs, 'Completed')
+
+@frappe.whitelist(allow_guest=True)
+def handle_missed_call(**kwargs):
+ update_call_log(kwargs, 'Missed')
+
+def update_call_log(call_payload, status):
+ call_log = get_call_log(call_payload)
+ if call_log:
+ call_log.status = status
+ call_log.duration = call_payload.get('DialCallDuration') or 0
+ call_log.recording_url = call_payload.get('RecordingUrl')
+ call_log.save(ignore_permissions=True)
+ frappe.db.commit()
+ return call_log
+
+def get_call_log(call_payload):
+ call_log = frappe.get_all('Call Log', {
+ 'id': call_payload.get('CallSid'),
+ }, limit=1)
+
+ if call_log:
+ return frappe.get_doc('Call Log', call_log[0].name)
+
+def create_call_log(call_payload):
+ call_log = frappe.new_doc('Call Log')
+ call_log.id = call_payload.get('CallSid')
+ call_log.to = call_payload.get('CallTo')
+ call_log.medium = call_payload.get('To')
+ call_log.status = 'Ringing'
+ setattr(call_log, 'from', call_payload.get('CallFrom'))
+ call_log.save(ignore_permissions=True)
+ frappe.db.commit()
+ return call_log
+
+@frappe.whitelist()
+def get_call_status(call_id):
+ endpoint = get_exotel_endpoint('Calls/{call_id}.json'.format(call_id=call_id))
+ response = requests.get(endpoint)
+ status = response.json().get('Call', {}).get('Status')
+ return status
+
+@frappe.whitelist()
+def make_a_call(from_number, to_number, caller_id):
+ endpoint = get_exotel_endpoint('Calls/connect.json?details=true')
+ response = requests.post(endpoint, data={
+ 'From': from_number,
+ 'To': to_number,
+ 'CallerId': caller_id
+ })
+
+ return response.json()
+
+def get_exotel_settings():
+ return frappe.get_single('Exotel Settings')
+
+def whitelist_numbers(numbers, caller_id):
+ endpoint = get_exotel_endpoint('CustomerWhitelist')
+ response = requests.post(endpoint, data={
+ 'VirtualNumber': caller_id,
+ 'Number': numbers,
+ })
+
+ return response
+
+def get_all_exophones():
+ endpoint = get_exotel_endpoint('IncomingPhoneNumbers')
+ response = requests.post(endpoint)
+ return response
+
+def get_exotel_endpoint(action):
+ settings = get_exotel_settings()
+ return 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}'.format(
+ api_key=settings.api_key,
+ api_token=settings.api_token,
+ sid=settings.account_sid,
+ action=action
+ )
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 6ce75bb..756f6ba 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -169,6 +169,11 @@
{'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'},
]
+sounds = [
+ {"name": "incoming-call", "src": "/assets/erpnext/sounds/incoming-call.mp3", "volume": 0.2},
+ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
+]
+
has_website_permission = {
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
diff --git a/erpnext/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
index f2e7770..4e0045c 100644
--- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json
+++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
@@ -1,109 +1,45 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-11-19 12:39:46.153061",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "creation": "2018-11-19 12:39:46.153061",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "user_id"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "employee",
- "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": "Employee",
- "length": 0,
- "no_copy": 0,
- "options": "Employee",
- "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
- },
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.first_name",
- "fieldname": "employee_name",
- "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": "Employee Name",
- "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
+ "fetch_from": "employee.first_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Employee Name"
+ },
+ {
+ "fetch_from": "employee.user_id",
+ "fieldname": "user_id",
+ "fieldtype": "Data",
+ "label": "ERPNext User ID",
+ "read_only": 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-19 13:18:17.281656",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Employee Group Table",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "modified": "2019-06-06 10:41:20.313756",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Group Table",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 9ef8937..316d6de 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -22,4 +22,5 @@
Non Profit
Hotels
Hub Node
-Quality Management
\ No newline at end of file
+Quality Management
+Communication
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index fa51638..36a31cf 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -607,3 +607,4 @@
erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.set_quotation_status
erpnext.patches.v12_0.set_priority_for_support
+erpnext.patches.v12_0.delete_priority_property_setter
diff --git a/erpnext/patches/v12_0/delete_priority_property_setter.py b/erpnext/patches/v12_0/delete_priority_property_setter.py
new file mode 100644
index 0000000..5927267
--- /dev/null
+++ b/erpnext/patches/v12_0/delete_priority_property_setter.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ frappe.db.sql("""
+ DELETE FROM `tabProperty Setter`
+ WHERE `tabProperty Setter`.doc_type='Issue'
+ AND `tabProperty Setter`.field_name='priority'
+ AND `tabProperty Setter`.property='options'
+ """)
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index be7189b..f6137d5 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -1,7 +1,8 @@
{
"css/erpnext.css": [
"public/less/erpnext.less",
- "public/less/hub.less"
+ "public/less/hub.less",
+ "public/less/call_popup.less"
],
"css/marketplace.css": [
"public/less/hub.less"
@@ -49,6 +50,7 @@
"public/js/education/student_button.html",
"public/js/education/assessment_result_tool.html",
"public/js/hub/hub_factory.js",
+ "public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js"
],
"js/item-dashboard.min.js": [
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
new file mode 100644
index 0000000..91dfe80
--- /dev/null
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -0,0 +1,212 @@
+class CallPopup {
+ constructor(call_log) {
+ this.caller_number = call_log.from;
+ this.call_log = call_log;
+ this.setup_listener();
+ this.make();
+ }
+
+ make() {
+ this.dialog = new frappe.ui.Dialog({
+ 'static': true,
+ 'minimizable': true,
+ 'fields': [{
+ 'fieldname': 'caller_info',
+ 'fieldtype': 'HTML'
+ }, {
+ 'fielname': 'last_interaction',
+ 'fieldtype': 'Section Break',
+ 'label': __('Activity'),
+ }, {
+ 'fieldtype': 'Small Text',
+ 'label': __('Last Communication'),
+ 'fieldname': 'last_communication',
+ 'read_only': true,
+ 'default': `<i class="text-muted">${__('No communication found.')}<i>`
+ }, {
+ 'fieldtype': 'Small Text',
+ 'label': __('Last Issue'),
+ 'fieldname': 'last_issue',
+ 'read_only': true,
+ 'default': `<i class="text-muted">${__('No issue raised by the customer.')}<i>`
+ }, {
+ 'fieldtype': 'Column Break',
+ }, {
+ 'fieldtype': 'Small Text',
+ 'label': __('Call Summary'),
+ 'fieldname': 'call_summary',
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Save'),
+ 'click': () => {
+ const call_summary = this.dialog.get_value('call_summary');
+ if (!call_summary) return;
+ frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', {
+ 'docname': this.call_log.id,
+ 'summary': call_summary,
+ }).then(() => {
+ this.close_modal();
+ frappe.show_alert({
+ message: `${__('Call Summary Saved')}<br><a class="text-small text-muted" href="#Form/Call Log/${this.call_log.name}">${__('View call log')}</a>`,
+ indicator: 'green'
+ });
+ });
+ }
+ }],
+ });
+ this.set_call_status();
+ this.make_caller_info_section();
+ this.dialog.get_close_btn().show();
+ this.dialog.$body.addClass('call-popup');
+ this.dialog.set_secondary_action(this.close_modal.bind(this));
+ frappe.utils.play_sound('incoming-call');
+ this.dialog.show();
+ }
+
+ make_caller_info_section() {
+ const wrapper = this.dialog.get_field('caller_info').$wrapper;
+ wrapper.append(`<div class="text-muted"> ${__("Loading...")} </div>`);
+ frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', {
+ 'number': this.caller_number
+ }).then(contact_doc => {
+ wrapper.empty();
+ const contact = this.contact = contact_doc;
+ if (!contact) {
+ this.setup_unknown_caller(wrapper);
+ } else {
+ this.setup_known_caller(wrapper);
+ this.set_call_status();
+ this.make_last_interaction_section();
+ }
+ });
+ }
+
+ setup_unknown_caller(wrapper) {
+ wrapper.append(`
+ <div class="caller-info">
+ <b>${__('Unknown Number')}:</b> ${this.caller_number}
+ <button
+ class="margin-left btn btn-new btn-default btn-xs"
+ data-doctype="Contact"
+ title=${__("Make New Contact")}>
+ <i class="octicon octicon-plus text-medium"></i>
+ </button>
+ </div>
+ `).find('button').click(
+ () => frappe.set_route(`Form/Contact/New Contact?phone=${this.caller_number}`)
+ );
+ }
+
+ setup_known_caller(wrapper) {
+ const contact = this.contact;
+ const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, this.get_caller_name());
+ const links = contact.links ? contact.links : [];
+
+ let contact_links = '';
+
+ links.forEach(link => {
+ contact_links += `<div>${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}</div>`;
+ });
+ wrapper.append(`
+ <div class="caller-info flex">
+ ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)}
+ <div>
+ <h5>${contact_name}</h5>
+ <div>${contact.mobile_no || ''}</div>
+ <div>${contact.phone_no || ''}</div>
+ ${contact_links}
+ </div>
+ </div>
+ `);
+ }
+
+ set_indicator(color, blink=false) {
+ let classes = `indicator ${color} ${blink ? 'blink': ''}`;
+ this.dialog.header.find('.indicator').attr('class', classes);
+ }
+
+ set_call_status(call_status) {
+ let title = '';
+ call_status = call_status || this.call_log.status;
+ if (['Ringing'].includes(call_status) || !call_status) {
+ title = __('Incoming call from {0}', [this.get_caller_name()]);
+ this.set_indicator('blue', true);
+ } else if (call_status === 'In Progress') {
+ title = __('Call Connected');
+ this.set_indicator('yellow');
+ } else if (call_status === 'Missed') {
+ this.set_indicator('red');
+ title = __('Call Missed');
+ } else if (['Completed', 'Disconnected'].includes(call_status)) {
+ this.set_indicator('red');
+ title = __('Call Disconnected');
+ } else {
+ this.set_indicator('blue');
+ title = call_status;
+ }
+ this.dialog.set_title(title);
+ }
+
+ update_call_log(call_log) {
+ this.call_log = call_log;
+ this.set_call_status();
+ }
+
+ close_modal() {
+ this.dialog.hide();
+ delete erpnext.call_popup;
+ }
+
+ call_disconnected(call_log) {
+ frappe.utils.play_sound('call-disconnect');
+ this.update_call_log(call_log);
+ setTimeout(() => {
+ if (!this.dialog.get_value('call_summary')) {
+ this.close_modal();
+ }
+ }, 10000);
+ }
+
+ make_last_interaction_section() {
+ frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
+ 'number': this.caller_number,
+ 'reference_doc': this.contact
+ }).then(data => {
+ const comm_field = this.dialog.get_field('last_communication');
+ if (data.last_communication) {
+ const comm = data.last_communication;
+ comm_field.set_value(comm.content);
+ }
+
+ if (data.last_issue) {
+ const issue = data.last_issue;
+ const issue_field = this.dialog.get_field("last_issue");
+ issue_field.set_value(issue.subject);
+ issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
+ ${__('View all issues from {0}', [issue.customer])}
+ </a>`);
+ }
+ });
+ }
+ get_caller_name() {
+ return this.contact ? this.contact.lead_name || this.contact.name || '' : this.caller_number;
+ }
+ setup_listener() {
+ frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
+ this.call_disconnected(call_log);
+ // Remove call disconnect listener after the call is disconnected
+ frappe.realtime.off(`call_${this.call_log.id}_disconnected`);
+ });
+ }
+}
+
+$(document).on('app_ready', function () {
+ frappe.realtime.on('show_call_popup', call_log => {
+ if (!erpnext.call_popup) {
+ erpnext.call_popup = new CallPopup(call_log);
+ } else {
+ erpnext.call_popup.update_call_log(call_log);
+ erpnext.call_popup.dialog.show();
+ }
+ });
+});
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 64b96b5..dd8abfb 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -595,6 +595,7 @@
}
erpnext.utils.clear_duplicates = function() {
+ if(!cur_frm.doc.items) return;
const unique_items = new Map();
/*
Create a Map of items with
diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less
new file mode 100644
index 0000000..32e85ce
--- /dev/null
+++ b/erpnext/public/less/call_popup.less
@@ -0,0 +1,9 @@
+.call-popup {
+ a:hover {
+ text-decoration: underline;
+ }
+ .for-description {
+ max-height: 250px;
+ overflow: scroll;
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/sounds/call-disconnect.mp3 b/erpnext/public/sounds/call-disconnect.mp3
new file mode 100644
index 0000000..1202273
--- /dev/null
+++ b/erpnext/public/sounds/call-disconnect.mp3
Binary files differ
diff --git a/erpnext/public/sounds/incoming-call.mp3 b/erpnext/public/sounds/incoming-call.mp3
new file mode 100644
index 0000000..60431e3
--- /dev/null
+++ b/erpnext/public/sounds/incoming-call.mp3
Binary files differ
diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js
index fc86989..da21c6b 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -1,7 +1,7 @@
frappe.pages['stock-balance'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
- title: 'Stock Summary',
+ title: __('Stock Summary'),
single_column: true
});
page.start = 0;
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index ba54edc..1a272d1 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -73,27 +73,6 @@
}
},
- priority: function(frm) {
- if (frm.doc.service_level_agreement) {
- frm.call('change_service_level_agreement_and_priority', {
- "priority": frm.doc.priority,
- "service_level_agreement": frm.doc.service_level_agreement
- }).then(() => {
- frappe.msgprint(__("Issue Priority changed to {0}.", [frm.doc.priority]));
- frm.refresh();
- });
- }
- },
-
- service_level_agreement: function(frm) {
- frm.call('change_service_level_agreement_and_priority', {
- "service_level_agreement": frm.doc.service_level_agreement
- }).then(() => {
- frappe.msgprint(__("Service Level Agreement changed to {0}.", [frm.doc.service_level_agreement]));
- frm.refresh();
- });
- },
-
timeline_refresh: function(frm) {
// create button for "Help Article"
if(frappe.model.can_create('Help Article')) {
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 70430b1..93f13f1 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -12,7 +12,6 @@
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
from erpnext.support.doctype.service_level_agreement.service_level_agreement import get_active_service_level_agreement_for
-from erpnext.crm.doctype.opportunity.opportunity import assign_to_user
from frappe.email.inbox import link_communication_to_document
sender_field = "raised_by"
@@ -29,6 +28,7 @@
if not self.raised_by:
self.raised_by = frappe.session.user
+ self.change_service_level_agreement_and_priority()
self.update_status()
self.set_lead_contact(self.raised_by)
@@ -38,9 +38,6 @@
self.create_communication()
self.flags.communication_created = None
- # assign to customer account manager or lead owner
- assign_to_user(self, 'subject')
-
def set_lead_contact(self, email_id):
import email.utils
@@ -173,9 +170,14 @@
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()))
- def change_service_level_agreement_and_priority(self, priority=None, service_level_agreement=None):
- self.set_response_and_resolution_time(priority=priority, service_level_agreement=service_level_agreement)
- self.save(ignore_permissions=True)
+ def change_service_level_agreement_and_priority(self):
+ if not self.priority == frappe.db.get_value("Issue", self.name, "priority"):
+ self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ frappe.msgprint("Priority has been updated.")
+
+ if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"):
+ self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ frappe.msgprint("Service Level Agreement has been updated.")
def get_expected_time_for(parameter, service_level, start_date_time):
current_date_time = start_date_time
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index 1296b36..75d70b1 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -8,15 +8,8 @@
from frappe.utils import now_datetime, get_datetime
import datetime
from datetime import timedelta
-from frappe.desk.form import assign_to
class TestIssue(unittest.TestCase):
- def test_assignment(self):
- frappe.db.set_value('Customer', '_Test Customer', 'account_manager', 'test1@example.com')
- doc = make_issue(customer='_Test Customer')
- self.assertEqual(assign_to.get(dict(doctype = doc.doctype, name = doc.name))[0].get('owner'), 'test1@example.com')
-
-
def test_response_time_and_resolution_time_based_on_different_sla(self):
create_service_level_agreements_for_issues()
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 332bf63..82c0ffb 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -85,8 +85,9 @@
["Service Level Agreement", "default_service_level_agreement", "=", 1]
]
else:
+ # Include SLA with No Entity and Entity Type
or_filters = [
- ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), "IS NULL"]],
+ ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]],
["Service Level Agreement", "default_service_level_agreement", "=", 1]
]