Merge branch 'develop' into so-days-taken-to-deliver

This commit is contained in:
Saqib 2021-12-15 10:36:47 +05:30 committed by GitHub
commit 8aa9481a11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 3270 additions and 1130 deletions

View File

@ -1,47 +0,0 @@
---
name: Bug report
about: Report a bug encountered while using ERPNext
labels: bug
---
<!--
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->
## Description of the issue
## Context information (for bug reports)
**Output of `bench version`**
```
(paste here)
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(paste here)
```
## Additional information
OS version / distribution, `ERPNext` install method, etc.

106
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,106 @@
name: Bug Report
description: Report a bug encountered while using ERPNext
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
- type: textarea
id: bug-info
attributes:
label: Information about bug
description: Also tell us, what did you expect to happen?
placeholder: Please provide as much information as possible.
validations:
required: true
- type: dropdown
id: version
attributes:
label: Version
description: Affected versions.
multiple: true
options:
- v12
- v13
- v14
- develop
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module
description: Select affected module of ERPNext.
multiple: true
options:
- accounts
- stock
- buying
- selling
- ecommerce
- manufacturing
- HR
- projects
- support
- assets
- integrations
- quality
- regional
- portal
- agriculture
- education
- non-profit
validations:
required: true
- type: textarea
id: exact-version
attributes:
label: Version
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext Verion -
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- docker
- easy-install
- manual install
- FrappeCloud
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output / Stack trace / Full Error Message.
description: Please copy and paste any relevant log output. This will be automatically formatted.
render: shell
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request
assignees: ''
---
<!--

View File

@ -1,17 +0,0 @@
---
name: Question about using ERPNext
about: This is not the appropriate channel
labels: invalid
---
Please post on our forums:
for questions about using `ERPNext`: https://discuss.erpnext.com
for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**

56
.github/stale.yml vendored
View File

@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking as stale
staleLabel: inactive
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed within a week if no further activity occurs, but it
only takes a comment to keep a contribution alive :) Also, even if it is closed,
you can always reopen the PR when you're ready. Thank you for contributing.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
limitPerRun: 10
# Limit to only `issues` or `pulls`
only: pulls
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
pulls:
daysUntilStale: 15
daysUntilClose: 3
exemptLabels:
- hotfix
markComment: >
This pull request has been automatically marked as inactive because it has
not had recent activity. It will be closed within 3 days if no further
activity occurs, but it only takes a comment to keep a contribution alive
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
markComment: >
This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be
closed within a week if no further activity occurs.

View File

@ -8,6 +8,16 @@ coverage:
target: auto
threshold: 0.5%
patch:
default:
target: 85%
threshold: 0%
base: auto
branches:
- develop
if_ci_failed: ignore
only_pulls: true
comment:
layout: "diff, files"
require_changes: true

View File

@ -297,8 +297,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
if not asset_category_account:
form_link = get_link_to_form('Asset Category', asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account")
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:

View File

@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date":
entry_date = row.due_date
# use posting date as a fallback for advances posted via journal and payment entry
# when ageing viewed by due date
entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date
else:

View File

@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted:
if tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name')
@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers:
supplier_map[d.name] = d

View File

@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment");
frm.trigger("create_asset_value_adjustment");
}, __("Manage"));
}
@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
});
},
create_asset_adjustment: function(frm) {
create_asset_value_adjustment: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_category": frm.doc.asset_category,
"company": frm.doc.company
},
method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
freeze: 1,
callback: function(r) {
var doclist = frappe.model.sync(r.message);

View File

@ -185,83 +185,84 @@ class Asset(AccountsController):
if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule()
start = self.clear_depreciation_schedule()
for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = flt(d.value_after_depreciation)
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(d)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
for n in range(start, number_of_pending_depreciations):
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_sale:
from_date = self.get_from_date(d.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
self.available_for_use_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
@ -272,10 +273,10 @@ class Asset(AccountsController):
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
@ -285,7 +286,7 @@ class Asset(AccountsController):
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else d.frequency_of_depreciation
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
@ -311,27 +312,52 @@ class Asset(AccountsController):
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
else:
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
# used when depreciation schedule needs to be modified due to increase in asset life
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = 0
for n in range(len(self.schedules)):
if not self.schedules[n].journal_entry:
del self.schedules[n:]
start = n
break
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
@ -469,7 +495,6 @@ class Asset(AccountsController):
asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation) -
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
if (row.expected_value_after_useful_life and
@ -731,14 +756,14 @@ def create_asset_repair(asset, asset_name):
return asset_repair
@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
def create_asset_value_adjustment(asset, asset_category, company):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_value_adjustment.update({
"asset": asset,
"company": company,
"asset_category": asset_category
})
return asset_maintenance
return asset_value_adjustment
@frappe.whitelist()
def transfer_asset(args):

View File

@ -955,6 +955,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",

View File

@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show()
},
schedule_date(frm) {
if(frm.doc.schedule_date){
frm.doc.items.forEach((item) => {
item.schedule_date = frm.doc.schedule_date;
})
}
refresh_field("items");
},
preview: (frm) => {
let dialog = new frappe.ui.Dialog({
title: __('Preview Email'),
@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show();
}
})
frappe.ui.form.on("Request for Quotation Item", {
items_add(frm, cdt, cdn) {
if (frm.doc.schedule_date) {
frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
}
}
});
frappe.ui.form.on("Request for Quotation Supplier",{
supplier: function(frm, cdt, cdn) {
var d = locals[cdt][cdn]

View File

@ -12,6 +12,7 @@
"vendor",
"column_break1",
"transaction_date",
"schedule_date",
"status",
"amended_from",
"suppliers_section",
@ -246,16 +247,22 @@
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required Date"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-05 22:04:29.017134",
"modified": "2021-11-24 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -17,6 +17,7 @@
"company",
"transaction_date",
"valid_till",
"quotation_number",
"amended_from",
"address_section",
"supplier_address",
@ -797,6 +798,11 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till"
},
{
"fieldname": "quotation_number",
"fieldtype": "Data",
"label": "Quotation Number"
}
],
"icon": "fa fa-shopping-cart",
@ -804,10 +810,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 00:58:20.995491",
"modified": "2021-12-11 06:43:20.924080",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -91,8 +91,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
"modified": "2021-11-03 09:00:36.883496",
"modified": "2021-11-03 10:00:36.883496",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",

View File

@ -510,8 +510,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
"modified": "2021-10-21 11:04:30.151379",
"modified": "2021-10-21 12:04:30.151379",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

@ -115,8 +115,7 @@
],
"issingle": 1,
"links": [],
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
"modified": "2021-11-30 11:17:24.647979",
"modified": "2021-11-30 12:17:24.647979",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",

View File

@ -234,7 +234,7 @@ doc_events = {
},
"Communication": {
"on_update": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
@ -343,8 +343,7 @@ scheduler_events = {
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"

View File

@ -21,7 +21,11 @@ def get_data():
},
{
'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
},
{
'label': _('Exit'),
'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
},
{
'label': _('Shift'),

View File

@ -0,0 +1,38 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Exit Interview', {
refresh: function(frm) {
if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) {
frm.add_custom_button(__('Send Exit Questionnaire'), function () {
frm.trigger('send_exit_questionnaire');
});
}
},
employee: function(frm) {
frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => {
if (!message.relieving_date) {
frappe.throw({
message: __('Please set the relieving date for employee {0}',
['<a href="/app/employee/' + frm.doc.employee +'">' + frm.doc.employee + '</a>']),
title: __('Relieving Date Missing')
});
}
});
},
send_exit_questionnaire: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
args: {
'interviews': [frm.doc]
},
callback: function(r) {
if (!r.exc) {
frm.refresh_field('questionnaire_email_sent');
}
}
});
}
});

View File

@ -0,0 +1,246 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2021-12-05 13:56:36.241690",
"doctype": "DocType",
"editable_grid": 1,
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"employee",
"employee_name",
"email",
"column_break_5",
"company",
"status",
"date",
"employee_details_section",
"department",
"designation",
"reports_to",
"column_break_9",
"date_of_joining",
"relieving_date",
"exit_questionnaire_section",
"ref_doctype",
"questionnaire_email_sent",
"column_break_10",
"reference_document_name",
"interview_summary_section",
"interviewers",
"interview_summary",
"employee_status_section",
"employee_status",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fetch_from": "employee.relieving_date",
"fieldname": "relieving_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Relieving Date",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"mandatory_depends_on": "eval:doc.status==='Scheduled';"
},
{
"fieldname": "exit_questionnaire_section",
"fieldtype": "Section Break",
"label": "Exit Questionnaire"
},
{
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_document_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document Name",
"options": "ref_doctype"
},
{
"fieldname": "interview_summary_section",
"fieldtype": "Section Break",
"label": "Interview Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "interviewers",
"fieldtype": "Table MultiSelect",
"label": "Interviewers",
"mandatory_depends_on": "eval:doc.status==='Scheduled';",
"options": "Interviewer"
},
{
"fetch_from": "employee.date_of_joining",
"fieldname": "date_of_joining",
"fieldtype": "Date",
"label": "Date of Joining",
"read_only": 1
},
{
"fetch_from": "employee.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "employee_details_section",
"fieldtype": "Section Break",
"label": "Employee Details"
},
{
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HR-EXIT-INT-"
},
{
"default": "0",
"fieldname": "questionnaire_email_sent",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Questionnaire Email Sent",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email ID",
"options": "Email",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nScheduled\nCompleted\nCancelled",
"reqd": 1
},
{
"fieldname": "employee_status_section",
"fieldtype": "Section Break"
},
{
"fieldname": "employee_status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Final Decision",
"mandatory_depends_on": "eval:doc.status==='Completed';",
"options": "\nEmployee Retained\nExit Confirmed"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Exit Interview",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "interview_summary",
"fieldtype": "Text Editor",
"label": "Interview Summary"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-12-07 23:39:22.645401",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sender_field": "email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@ -0,0 +1,131 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from erpnext.hr.doctype.employee.employee import get_employee_email
class ExitInterview(Document):
def validate(self):
self.validate_relieving_date()
self.validate_duplicate_interview()
self.set_employee_email()
def validate_relieving_date(self):
if not frappe.db.get_value('Employee', self.employee, 'relieving_date'):
frappe.throw(_('Please set the relieving date for employee {0}').format(
get_link_to_form('Employee', self.employee)),
title=_('Relieving Date Missing'))
def validate_duplicate_interview(self):
doc = frappe.db.exists('Exit Interview', {
'employee': self.employee,
'name': ('!=', self.name),
'docstatus': ('!=', 2)
})
if doc:
frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format(
get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)),
frappe.DuplicateEntryError)
def set_employee_email(self):
employee = frappe.get_doc('Employee', self.employee)
self.email = get_employee_email(employee)
def on_submit(self):
if self.status != 'Completed':
frappe.throw(_('Only Completed documents can be submitted'))
self.update_interview_date_in_employee()
def on_cancel(self):
self.update_interview_date_in_employee()
self.db_set('status', 'Cancelled')
def update_interview_date_in_employee(self):
if self.docstatus == 1:
frappe.db.set_value('Employee', self.employee, 'held_on', self.date)
elif self.docstatus == 2:
frappe.db.set_value('Employee', self.employee, 'held_on', None)
@frappe.whitelist()
def send_exit_questionnaire(interviews):
interviews = get_interviews(interviews)
validate_questionnaire_settings()
email_success = []
email_failure = []
for exit_interview in interviews:
interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
if interview.get('questionnaire_email_sent'):
continue
employee = frappe.get_doc('Employee', interview.employee)
email = get_employee_email(employee)
context = interview.as_dict()
context.update(employee.as_dict())
template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
template = frappe.get_doc('Email Template', template_name)
if email:
frappe.sendmail(
recipients=email,
subject=template.subject,
message=frappe.render_template(template.response, context),
reference_doctype=interview.doctype,
reference_name=interview.name
)
interview.db_set('questionnaire_email_sent', True)
interview.notify_update()
email_success.append(email)
else:
email_failure.append(get_link_to_form('Employee', employee.name))
show_email_summary(email_success, email_failure)
def get_interviews(interviews):
import json
if isinstance(interviews, str):
interviews = json.loads(interviews)
if not len(interviews):
frappe.throw(_('Atleast one interview has to be selected.'))
return interviews
def validate_questionnaire_settings():
settings = frappe.db.get_value('HR Settings', 'HR Settings',
['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
frappe.throw(
_('Please set {0} and {1} in {2}.').format(
frappe.bold('Exit Questionnaire Web Form'),
frappe.bold('Notification Template'),
get_link_to_form('HR Settings', 'HR Settings')),
title=_('Settings Missing')
)
def show_email_summary(email_success, email_failure):
message = ''
if email_success:
message += _('{0}: {1}').format(
frappe.bold('Sent Successfully'), ', '.join(email_success))
if message and email_failure:
message += '<br><br>'
if email_failure:
message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure))
frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)

View File

@ -0,0 +1,27 @@
frappe.listview_settings['Exit Interview'] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
let status_color = {
'Pending': 'orange',
'Scheduled': 'yellow',
'Completed': 'green',
'Cancelled': 'red',
};
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
},
onload: function(listview) {
if (frappe.boot.user.can_write.includes('Exit Interview')) {
listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
const interviews = listview.get_checked_items();
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
freeze: true,
args: {
'interviews': interviews
}
});
});
}
}
};

View File

@ -0,0 +1,16 @@
<h2>Exit Questionnaire</h2>
<br>
<p>
Dear {{ employee_name }},
<br><br>
Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us.
Request you to take out a few minutes to fill up this Exit Questionnaire.
{% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %}
{% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %}
<br><br>
<a class="btn btn-primary" href="{{ web_form_link }}" target="_blank">{{ _('Submit Now') }}</a>
</p>

View File

@ -0,0 +1,118 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import os
import unittest
import frappe
from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.test_webform import create_custom_doctype, create_webform
from frappe.utils import getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire
class TestExitInterview(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabExit Interview`')
def test_duplicate_interview(self):
employee = make_employee('employeeexitint1@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
doc = frappe.copy_doc(interview)
self.assertRaises(frappe.DuplicateEntryError, doc.save)
def test_relieving_date_validation(self):
employee = make_employee('employeeexitint2@example.com')
# unset relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', None)
interview = create_exit_interview(employee, save=False)
self.assertRaises(frappe.ValidationError, interview.save)
# set relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
self.assertTrue(interview.name)
def test_interview_date_updated_in_employee_master(self):
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
interview.status = 'Completed'
interview.employee_status = 'Exit Confirmed'
# exit interview date updated on submit
interview.submit()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date)
# exit interview reset on cancel
interview.reload()
interview.cancel()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None)
def test_send_exit_questionnaire(self):
create_custom_doctype()
create_webform()
template = create_notification_template()
webform = frappe.db.get_all('Web Form', limit=1)
frappe.db.set_value('HR Settings', 'HR Settings', {
'exit_questionnaire_web_form': webform[0].name,
'exit_questionnaire_notification_template': template
})
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
send_exit_questionnaire([interview])
email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1)
self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message)
def tearDown(self):
frappe.db.rollback()
def create_exit_interview(employee, save=True):
interviewer = create_user('test_exit_interviewer@example.com')
doc = frappe.get_doc({
'doctype': 'Exit Interview',
'employee': employee,
'company': '_Test Company',
'status': 'Pending',
'date': getdate(),
'interviewers': [{
'interviewer': interviewer.name
}],
'interview_summary': 'Test'
})
if save:
return doc.insert()
return doc
def create_notification_template():
template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification'))
if not template:
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
template = frappe.get_doc({
'doctype': 'Email Template',
'name': _('Exit Questionnaire Notification'),
'response': response,
'subject': _('Exit Questionnaire Notification'),
'owner': frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
return template

View File

@ -36,7 +36,11 @@
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
"feedback_reminder_notification_template",
"employee_exit_section",
"exit_questionnaire_web_form",
"column_break_34",
"exit_questionnaire_notification_template"
],
"fields": [
{
@ -226,13 +230,34 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"fieldname": "employee_exit_section",
"fieldtype": "Section Break",
"label": "Employee Exit Settings"
},
{
"fieldname": "exit_questionnaire_web_form",
"fieldtype": "Link",
"label": "Exit Questionnaire Web Form",
"options": "Web Form"
},
{
"fieldname": "exit_questionnaire_notification_template",
"fieldtype": "Link",
"label": "Exit Questionnaire Notification Template",
"options": "Email Template"
},
{
"fieldname": "column_break_34",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-01 23:46:11.098236",
"modified": "2021-12-05 14:48:10.884253",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -0,0 +1,29 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'",
"creation": "2021-12-05 22:11:47.263933",
"date_changed": "date",
"days_in_advance": 1,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Exit Interview",
"enabled": 1,
"event": "Days Before",
"idx": 0,
"is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div class=\"text-medium text-muted\">\n\t\t\t\t<span>{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}</span>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div>\n\t\t\t\t<ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n\t\t\t\t\t<li>{{_(\"Employee\")}}: <b>{{ doc.employee }} - {{ doc.employee_name }}</b></li>\n\t\t\t\t\t<li>{{_(\"Date\")}}: <b>{{ doc.date }}</b></li>\n\t\t\t\t\t<li> {{_(\"Interviewers\")}}: </li>\n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>{{ entry.user }}</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t<li>{{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n",
"modified": "2021-12-05 22:26:57.096159",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview Scheduled",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "email"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 1,
"subject": "Exit Interview Scheduled: {{ doc.name }}"
}

View File

@ -0,0 +1,37 @@
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div class="text-medium text-muted">
<h2>{{_("Exit Interview Scheduled:")}} {{ doc.name }}</h2>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div>
<ul class="list-unstyled" style="line-height: 1.7">
<li><b>{{_("Employee")}}: </b>{{ doc.employee }} - {{ doc.employee_name }}</li>
<li><b>{{_("Date")}}: </b>{{ frappe.utils.formatdate(doc.date) }}</li>
<li><b>{{_("Interviewers")}}:</b> </li>
{% for entry in doc.interviewers %}
<ul>
<li>{{ entry.user }}</li>
</ul>
{% endfor %}
<li><b>{{ _("Interview Document") }}:</b> {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
</ul>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>

View File

@ -0,0 +1,6 @@
# import frappe
def get_context(context):
# do your magic here
pass

View File

@ -0,0 +1,77 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Employee Exits"] = {
filters: [
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.nowdate()
},
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company"
},
{
"fieldname": "department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department"
},
{
"fieldname": "designation",
"label": __("Designation"),
"fieldtype": "Link",
"options": "Designation"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "reports_to",
"label": __("Reports To"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "interview_status",
"label": __("Interview Status"),
"fieldtype": "Select",
"options": ["", "Pending", "Scheduled", "Completed"]
},
{
"fieldname": "final_decision",
"label": __("Final Decision"),
"fieldtype": "Select",
"options": ["", "Employee Retained", "Exit Confirmed"]
},
{
"fieldname": "exit_interview_pending",
"label": __("Exit Interview Pending"),
"fieldtype": "Check"
},
{
"fieldname": "questionnaire_pending",
"label": __("Exit Questionnaire Pending"),
"fieldtype": "Check"
},
{
"fieldname": "fnf_pending",
"label": __("FnF Pending"),
"fieldtype": "Check"
}
]
};

View File

@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-12-05 19:47:18.332319",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "Test",
"modified": "2021-12-05 19:47:18.332319",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Exits",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Exit Interview",
"report_name": "Employee Exits",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "HR Manager"
},
{
"role": "HR User"
}
]
}

View File

@ -0,0 +1,230 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.utils import getdate
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
chart = get_chart_data(data)
report_summary = get_report_summary(data)
return columns, data, None, chart, report_summary
def get_columns():
return [
{
'label': _('Employee'),
'fieldname': 'employee',
'fieldtype': 'Link',
'options': 'Employee',
'width': 150
},
{
'label': _('Employee Name'),
'fieldname': 'employee_name',
'fieldtype': 'Data',
'width': 150
},
{
'label': _('Date of Joining'),
'fieldname': 'date_of_joining',
'fieldtype': 'Date',
'width': 120
},
{
'label': _('Relieving Date'),
'fieldname': 'relieving_date',
'fieldtype': 'Date',
'width': 120
},
{
'label': _('Exit Interview'),
'fieldname': 'exit_interview',
'fieldtype': 'Link',
'options': 'Exit Interview',
'width': 150
},
{
'label': _('Interview Status'),
'fieldname': 'interview_status',
'fieldtype': 'Data',
'width': 130
},
{
'label': _('Final Decision'),
'fieldname': 'employee_status',
'fieldtype': 'Data',
'width': 150
},
{
'label': _('Full and Final Statement'),
'fieldname': 'full_and_final_statement',
'fieldtype': 'Link',
'options': 'Full and Final Statement',
'width': 180
},
{
'label': _('Department'),
'fieldname': 'department',
'fieldtype': 'Link',
'options': 'Department',
'width': 120
},
{
'label': _('Designation'),
'fieldname': 'designation',
'fieldtype': 'Link',
'options': 'Designation',
'width': 120
},
{
'label': _('Reports To'),
'fieldname': 'reports_to',
'fieldtype': 'Link',
'options': 'Employee',
'width': 120
}
]
def get_data(filters):
employee = frappe.qb.DocType('Employee')
interview = frappe.qb.DocType('Exit Interview')
fnf = frappe.qb.DocType('Full and Final Statement')
query = (
frappe.qb.from_(employee)
.left_join(interview).on(interview.employee == employee.name)
.left_join(fnf).on(fnf.employee == employee.name)
.select(
employee.name.as_('employee'), employee.employee_name.as_('employee_name'),
employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'),
employee.department.as_('department'), employee.designation.as_('designation'),
employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'),
interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'),
interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement'))
.distinct()
.where(
((employee.relieving_date.isnotnull()) | (employee.relieving_date != ''))
& ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
& ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2)))
).orderby(employee.relieving_date, order=Order.asc)
)
query = get_conditions(filters, query, employee, interview, fnf)
result = query.run(as_dict=True)
return result
def get_conditions(filters, query, employee, interview, fnf):
if filters.get('from_date') and filters.get('to_date'):
query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))])
elif filters.get('from_date'):
query = query.where(employee.relieving_date >= filters.get('from_date'))
elif filters.get('to_date'):
query = query.where(employee.relieving_date <= filters.get('to_date'))
if filters.get('company'):
query = query.where(employee.company == filters.get('company'))
if filters.get('department'):
query = query.where(employee.department == filters.get('department'))
if filters.get('designation'):
query = query.where(employee.designation == filters.get('designation'))
if filters.get('employee'):
query = query.where(employee.name == filters.get('employee'))
if filters.get('reports_to'):
query = query.where(employee.reports_to == filters.get('reports_to'))
if filters.get('interview_status'):
query = query.where(interview.status == filters.get('interview_status'))
if filters.get('final_decision'):
query = query.where(interview.employee_status == filters.get('final_decision'))
if filters.get('exit_interview_pending'):
query = query.where((interview.name == '') | (interview.name.isnull()))
if filters.get('questionnaire_pending'):
query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull()))
if filters.get('fnf_pending'):
query = query.where((fnf.name == '') | (fnf.name.isnull()))
return query
def get_chart_data(data):
if not data:
return None
retained = 0
exit_confirmed = 0
pending = 0
for entry in data:
if entry.employee_status == 'Employee Retained':
retained += 1
elif entry.employee_status == 'Exit Confirmed':
exit_confirmed += 1
else:
pending += 1
chart = {
'data': {
'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')],
'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}]
},
'type': 'donut',
'colors': ['green', 'red', 'blue'],
}
return chart
def get_report_summary(data):
if not data:
return None
total_resignations = len(data)
interviews_pending = len([entry.name for entry in data if not entry.exit_interview])
fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement])
questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire])
return [
{
'value': total_resignations,
'label': _('Total Resignations'),
'indicator': 'Red' if total_resignations > 0 else 'Green',
'datatype': 'Int',
},
{
'value': interviews_pending,
'label': _('Pending Interviews'),
'indicator': 'Blue' if interviews_pending > 0 else 'Green',
'datatype': 'Int',
},
{
'value': fnf_pending,
'label': _('Pending FnF'),
'indicator': 'Blue' if fnf_pending > 0 else 'Green',
'datatype': 'Int',
},
{
'value': questionnaires_pending,
'label': _('Pending Questionnaires'),
'indicator': 'Blue' if questionnaires_pending > 0 else 'Green',
'datatype': 'Int'
},
]

View File

@ -0,0 +1,242 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview
from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import (
create_full_and_final_statement,
)
from erpnext.hr.report.employee_exits.employee_exits import execute
class TestEmployeeExits(unittest.TestCase):
@classmethod
def setUpClass(cls):
create_company()
frappe.db.sql("delete from `tabEmployee` where company='Test Company'")
frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'")
frappe.db.sql("delete from `tabExit Interview` where company='Test Company'")
cls.create_records()
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
@classmethod
def create_records(cls):
cls.emp1 = make_employee(
'employeeexit1@example.com',
company='Test Company',
date_of_joining=getdate('01-10-2021'),
relieving_date=add_days(getdate(), 14),
designation='Accountant'
)
cls.emp2 = make_employee(
'employeeexit2@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 15),
designation='Accountant'
)
cls.emp3 = make_employee(
'employeeexit3@example.com',
company='Test Company',
date_of_joining=getdate('02-12-2021'),
relieving_date=add_days(getdate(), 29),
designation='Engineer'
)
cls.emp4 = make_employee(
'employeeexit4@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 30),
designation='Engineer'
)
# exit interview for 3 employees only
cls.interview1 = create_exit_interview(cls.emp1)
cls.interview2 = create_exit_interview(cls.emp2)
cls.interview3 = create_exit_interview(cls.emp3)
# create fnf for some records
cls.fnf1 = create_full_and_final_statement(cls.emp1)
cls.fnf2 = create_full_and_final_statement(cls.emp2)
# link questionnaire for a few records
# setting employee doctype as reference instead of creating a questionnaire
# since this is just for a test
frappe.db.set_value('Exit Interview', cls.interview1.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp1
})
frappe.db.set_value('Exit Interview', cls.interview2.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp2
})
frappe.db.set_value('Exit Interview', cls.interview3.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp3
})
def test_employee_exits_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 15),
'designation': 'Accountant'
}
report = execute(filters)
employee1 = frappe.get_doc('Employee', self.emp1)
employee2 = frappe.get_doc('Employee', self.emp2)
expected_data = [
{
'employee': employee1.name,
'employee_name': employee1.employee_name,
'date_of_joining': employee1.date_of_joining,
'relieving_date': employee1.relieving_date,
'department': employee1.department,
'designation': employee1.designation,
'reports_to': None,
'exit_interview': self.interview1.name,
'interview_status': self.interview1.status,
'employee_status': '',
'questionnaire': employee1.name,
'full_and_final_statement': self.fnf1.name
},
{
'employee': employee2.name,
'employee_name': employee2.employee_name,
'date_of_joining': employee2.date_of_joining,
'relieving_date': employee2.relieving_date,
'department': employee2.department,
'designation': employee2.designation,
'reports_to': None,
'exit_interview': self.interview2.name,
'interview_status': self.interview2.status,
'employee_status': '',
'questionnaire': employee2.name,
'full_and_final_statement': self.fnf2.name
}
]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_interviews_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'exit_interview_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_questionnaire_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'questionnaire_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_fnf_summary(self):
filters = {
'company': 'Test Company',
'fnf_pending': 1
}
report = execute(filters)
employee3 = frappe.get_doc('Employee', self.emp3)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [
{
'employee': employee3.name,
'employee_name': employee3.employee_name,
'date_of_joining': employee3.date_of_joining,
'relieving_date': employee3.relieving_date,
'department': employee3.department,
'designation': employee3.designation,
'reports_to': None,
'exit_interview': self.interview3.name,
'interview_status': self.interview3.status,
'employee_status': '',
'questionnaire': employee3.name,
'full_and_final_statement': None
},
{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}
]
self.assertEqual(expected_data, report[1]) # rows
def create_company():
if not frappe.db.exists('Company', 'Test Company'):
frappe.get_doc({
'doctype': 'Company',
'company_name': 'Test Company',
'default_currency': 'INR',
'country': 'India'
}).insert()

View File

@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports &amp; Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:58.322521",
"docstatus": 0,
"doctype": "Workspace",
@ -15,14 +15,6 @@
"idx": 0,
"label": "HR",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -111,14 +103,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
@ -227,14 +211,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -268,14 +244,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -386,14 +354,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -449,14 +409,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -489,14 +441,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -530,14 +474,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
@ -581,14 +517,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -808,14 +736,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
@ -933,9 +853,796 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding",
"link_count": 0,
"link_to": "Employee Onboarding",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Skill Map",
"link_count": 0,
"link_to": "Employee Skill Map",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Promotion",
"link_count": 0,
"link_to": "Employee Promotion",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Transfer",
"link_count": 0,
"link_to": "Employee Transfer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_count": 0,
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_count": 0,
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding Template",
"link_count": 0,
"link_to": "Employee Onboarding Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exit",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation Template",
"link_count": 0,
"link_to": "Employee Separation Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation",
"link_count": 0,
"link_to": "Employee Separation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Full and Final Statement",
"link_count": 0,
"link_to": "Full and Final Statement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Exit Interview",
"link_count": 0,
"link_to": "Exit Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 8,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"link_to": "Employee",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employment Type",
"link_count": 0,
"link_to": "Employment Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Branch",
"link_count": 0,
"link_to": "Branch",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Department",
"link_count": 0,
"link_to": "Department",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Designation",
"link_count": 0,
"link_to": "Designation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grade",
"link_count": 0,
"link_to": "Employee Grade",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Group",
"link_count": 0,
"link_to": "Employee Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Health Insurance",
"link_count": 0,
"link_to": "Employee Health Insurance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
"is_query_report": 1,
"label": "Monthly Attendance Sheet",
"link_count": 0,
"link_to": "Monthly Attendance Sheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Staffing Plan",
"hidden": 0,
"is_query_report": 1,
"label": "Recruitment Analytics",
"link_count": 0,
"link_to": "Recruitment Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Analytics",
"link_count": 0,
"link_to": "Employee Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance",
"link_count": 0,
"link_to": "Employee Leave Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance Summary",
"link_count": 0,
"link_to": "Employee Leave Balance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee Advance",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Advance Summary",
"link_count": 0,
"link_to": "Employee Advance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exits",
"link_count": 0,
"link_to": "Employee Exits",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 11,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Opening",
"link_count": 0,
"link_to": "Job Opening",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Referral",
"link_count": 0,
"link_to": "Employee Referral",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Applicant",
"link_count": 0,
"link_to": "Job Applicant",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Offer",
"link_count": 0,
"link_to": "Job Offer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Staffing Plan",
"link_count": 0,
"link_to": "Staffing Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter",
"link_count": 0,
"link_to": "Appointment Letter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter Template",
"link_count": 0,
"link_to": "Appointment Letter Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Type",
"link_count": 0,
"link_to": "Interview Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Round",
"link_count": 0,
"link_to": "Interview Round",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview",
"link_count": 0,
"link_to": "Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Feedback",
"link_count": 0,
"link_to": "Interview Feedback",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Driver",
"link_count": 0,
"link_to": "Driver",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle",
"link_count": 0,
"link_to": "Vehicle",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle Log",
"link_count": 0,
"link_to": "Vehicle Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Vehicle",
"hidden": 0,
"is_query_report": 1,
"label": "Vehicle Expenses",
"link_count": 0,
"link_to": "Vehicle Expenses",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "HR Settings",
"link_count": 0,
"link_to": "HR Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Daily Work Summary Group",
"link_count": 0,
"link_to": "Daily Work Summary Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Team Updates",
"link_count": 0,
"link_to": "team-updates",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claim",
"link_count": 0,
"link_to": "Expense Claim",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Advance",
"link_count": 0,
"link_to": "Employee Advance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Travel Request",
"link_count": 0,
"link_to": "Travel Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Attendance Tool",
"link_count": 0,
"link_to": "Employee Attendance Tool",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"link_to": "Attendance",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance Request",
"link_count": 0,
"link_to": "Attendance Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Upload Attendance",
"link_count": 0,
"link_to": "Upload Attendance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Checkin",
"link_count": 0,
"link_to": "Employee Checkin",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 10,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Holiday List",
"link_count": 0,
"link_to": "Holiday List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Type",
"link_count": 0,
"link_to": "Leave Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Period",
"link_count": 0,
"link_to": "Leave Period",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Type",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy",
"link_count": 0,
"link_to": "Leave Policy",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Policy",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy Assignment",
"link_count": 0,
"link_to": "Leave Policy Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Application",
"link_count": 0,
"link_to": "Leave Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Allocation",
"link_count": 0,
"link_to": "Leave Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Encashment",
"link_count": 0,
"link_to": "Leave Encashment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Block List",
"link_count": 0,
"link_to": "Leave Block List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Compensatory Leave Request",
"link_count": 0,
"link_to": "Compensatory Leave Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Type",
"link_count": 0,
"link_to": "Shift Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Request",
"link_count": 0,
"link_to": "Shift Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Assignment",
"link_count": 0,
"link_to": "Shift Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2021-08-31 12:18:59.842919",
"modified": "2021-12-05 22:05:13.004462",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

View File

@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_months, today
from erpnext import get_company_currency
from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order
class TestBlanketOrder(unittest.TestCase):
class TestBlanketOrder(ERPNextTestCase):
def setUp(self):
frappe.flags.args = frappe._dict()

View File

@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
import unittest
from collections import deque
from functools import partial
@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
class TestBOM(unittest.TestCase):
class TestBOM(ERPNextTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')

View File

@ -1,19 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
class TestBOMUpdateTool(unittest.TestCase):
class TestBOMUpdateTool(ERPNextTestCase):
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"

View File

@ -75,6 +75,15 @@ frappe.ui.form.on('Job Card', {
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
if (frm.doc.work_order) {
frappe.db.get_value('Work Order', frm.doc.work_order,
'transfer_material_against').then((r) => {
if (r.message.transfer_material_against == 'Work Order') {
frm.set_df_property('items', 'hidden', 1);
}
});
}
},
setup_corrective_job_card: function(frm) {

View File

@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import random_string
@ -12,9 +11,10 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
class TestJobCard(unittest.TestCase):
class TestJobCard(ERPNextTestCase):
def setUp(self):
make_bom_for_jc_tests()
@ -329,4 +329,4 @@ def make_bom_for_jc_tests():
bom.rm_cost_as_per = "Valuation Rate"
bom.items[0].uom = "_Test UOM 1"
bom.items[0].conversion_factor = 5
bom.insert()
bom.insert()

View File

@ -1,8 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_to_date, flt, now_datetime, nowdate
@ -17,9 +14,10 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.tests.utils import ERPNextTestCase
class TestProductionPlan(unittest.TestCase):
class TestProductionPlan(ERPNextTestCase):
def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:

View File

@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
class TestRouting(unittest.TestCase):
class TestRouting(ERPNextTestCase):
@classmethod
def setUpClass(cls):
cls.item_code = "Test Routing Item - A"

View File

@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.utils import add_months, cint, flt, now, today
@ -29,6 +28,9 @@ class TestWorkOrder(ERPNextTestCase):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
def tearDown(self):
frappe.db.rollback()
def check_planned_qty(self):
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
@ -92,7 +94,7 @@ class TestWorkOrder(ERPNextTestCase):
def test_reserved_qty_for_partial_completion(self):
item = "_Test Item"
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
warehouse = "_Test Warehouse - _TC"
bin1_at_start = get_bin(item, warehouse)

View File

@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
import unittest
import frappe
from frappe.test_runner import make_test_records
@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
WorkstationHolidayError,
check_if_within_operating_hours,
)
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
class TestWorkstation(unittest.TestCase):
class TestWorkstation(ERPNextTestCase):
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")

View File

@ -165,6 +165,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
erpnext.patches.v12_0.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.rename_pos_closing_doctype
@ -287,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.fix_invoice_statuses
@ -313,6 +313,8 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template

View File

@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.utils.rename_field import rename_field
@ -12,5 +13,20 @@ def execute():
if frappe.db.exists('DocType', 'Sales Invoice'):
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
# rename_field method assumes that the field already exists or the doc is synced
if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
create_custom_fields({
'Sales Invoice': [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
]
})
if frappe.db.has_column('Sales Invoice', 'qr_code'):
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")

View File

@ -0,0 +1,27 @@
import os
import frappe
from frappe import _
def execute():
frappe.reload_doc("email", "doctype", "email_template")
frappe.reload_doc("hr", "doctype", "hr_settings")
template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
if not template:
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html"))
template = frappe.get_doc({
"doctype": "Email Template",
"name": _("Exit Questionnaire Notification"),
"response": response,
"subject": _("Exit Questionnaire Notification"),
"owner": frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
hr_settings = frappe.get_doc("HR Settings")
hr_settings.exit_questionnaire_notification_template = template
hr_settings.save()

View File

@ -0,0 +1,27 @@
import frappe
def execute():
active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])]
for doctype in active_sla_documents:
doctype = frappe.qb.DocType(doctype)
try:
frappe.qb.update(
doctype
).set(
doctype.agreement_status, 'First Response Due'
).where(
doctype.first_responded_on.isnull()
).run()
frappe.qb.update(
doctype
).set(
doctype.agreement_status, 'Resolution Due'
).where(
doctype.agreement_status == 'Ongoing'
).run()
except Exception:
frappe.log_error(title='Failed to Patch SLA Status')

View File

@ -940,10 +940,12 @@ class SalarySlip(TransactionBase):
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days)
and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
and (not self.salary_slip_based_on_timesheet or
and (row.salary_component != timesheet_component or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
@ -952,7 +954,7 @@ class SalarySlip(TransactionBase):
amount = flt((flt(row.default_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
amount, additional_amount = 0, 0
elif not row.amount:
amount = flt(row.default_amount) + flt(row.additional_amount)

View File

@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import (
make_salary_structure_for_timesheet,
make_timesheet,
)
from erpnext.projects.doctype.timesheet.timesheet import (
make_salary_slip as make_salary_slip_for_timesheet,
)
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
# mark attendance
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""", (month_start_date, month_end_date))[0][0]
mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
# salary structure based on timesheet
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
salary_slip.start_date = month_start_date
salary_slip.end_date = month_end_date
salary_slip.save()
salary_slip.submit()
no_of_days = self.get_no_of_days()
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
# gross pay calculation based on attendance (payment days)
gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days))
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (

View File

@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
frappe.db.sql("delete from `tab%s`" % dt)
if not frappe.db.exists("Salary Component", "Timesheet Component"):
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
def test_timesheet_billing_amount(self):
emp = make_employee("test_employee_6@salary.com")
@ -160,6 +156,9 @@ def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly"
if not frappe.db.exists("Salary Component", "Timesheet Component"):
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1

View File

@ -835,7 +835,7 @@ $(document).on('app_ready', function() {
refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
&& frm.doc.agreement_status === 'Ongoing') {
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
frappe.call({
'method': 'frappe.client.get',
args: {
@ -888,8 +888,8 @@ $(document).on('app_ready', function() {
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
frm.dashboard.clear_headline();
let time_to_respond = get_status(frm.doc.response_by_variance);
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
let time_to_respond = get_status(frm.doc.response_by);
if (!frm.doc.first_responded_on) {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}
@ -903,8 +903,8 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
if (apply_sla_for_resolution) {
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
let time_to_resolve = get_status(frm.doc.resolution_by);
if (!frm.doc.resolution_date) {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}
@ -928,8 +928,9 @@ function get_time_left(timestamp, agreement_status) {
return {'diff_display': diff_display, 'indicator': indicator};
}
function get_status(variance) {
if (variance > 0) {
function get_status(timestamp) {
const time_left = moment(timestamp).diff(moment());
if (time_left >= 0) {
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
} else {
return {'diff_display': 'Failed', 'indicator': 'red'};

View File

@ -114,9 +114,11 @@ def get_items(filters):
items = frappe.db.sql("""
select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.stock_uom,
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
@ -124,6 +126,8 @@ def get_items(filters):
and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
group by
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
""" % (conditions, match_conditions), filters, as_dict=1)

View File

@ -0,0 +1,89 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from unittest import TestCase
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_company as setup_company,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_customers as setup_customers,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
set_account_heads as setup_gst_settings,
)
from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import (
execute as run_report,
)
from erpnext.stock.doctype.item.test_item import make_item
class TestHSNWiseSummaryReport(TestCase):
@classmethod
def setUpClass(cls):
setup_company()
setup_customers()
setup_gst_settings()
make_item("Golf Car", properties={ "gst_hsn_code": "999900" })
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def test_hsn_summary_for_invoice_with_duplicate_items(self):
si = create_sales_invoice(
company="_Test Company GST",
customer = "_Test GST Customer",
currency = "INR",
warehouse = "Finished Goods - _GST",
debit_to = "Debtors - _GST",
income_account = "Sales - _GST",
expense_account = "Cost of Goods Sold - _GST",
cost_center = "Main - _GST",
do_not_save=1
)
si.items = []
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "120",
"cost_center": "Main - _GST"
})
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "140",
"cost_center": "Main - _GST"
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
})
si.posting_date = "2020-11-17"
si.submit()
si.reload()
[columns, data] = run_report(filters=frappe._dict({
"company": "_Test Company GST",
"gst_hsn_code": "999900",
"company_gstin": si.company_gstin,
"from_date": si.posting_date,
"to_date": si.posting_date
}))
filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data))
self.assertTrue(filtered_rows)
hsn_row = filtered_rows[0]
self.assertEquals(hsn_row['stock_qty'], 2.0)
self.assertEquals(hsn_row['total_amount'], 306.8)

View File

@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import flt
@ -11,7 +9,7 @@ from frappe.utils import flt
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
from erpnext.tests.utils import create_test_contact_and_address
from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
test_ignore = ["Price List"]
test_dependencies = ['Payment Term', 'Payment Terms Template']
@ -19,7 +17,7 @@ test_records = frappe.get_test_records('Customer')
class TestCustomer(unittest.TestCase):
class TestCustomer(ERPNextTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')

View File

@ -6,6 +6,7 @@ import unittest
import frappe
from erpnext.controllers.queries import item_query
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ['Item', 'Customer', 'Supplier']
@ -17,7 +18,7 @@ def create_party_specific_item(**args):
psi.based_on_value = args.get('based_on_value')
psi.insert()
class TestPartySpecificItem(unittest.TestCase):
class TestPartySpecificItem(ERPNextTestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")

View File

@ -1,15 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.utils import add_days, add_months, flt, getdate, nowdate
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Product Bundle"]
class TestQuotation(unittest.TestCase):
class TestQuotation(ERPNextTestCase):
def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get('payment_schedule'))

View File

@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
import json
import unittest
import frappe
import frappe.permissions
@ -28,12 +27,14 @@ from erpnext.selling.doctype.sales_order.sales_order import (
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
class TestSalesOrder(unittest.TestCase):
class TestSalesOrder(ERPNextTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order"))
@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase):
# reset config to previous state
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
super().tearDownClass()
def tearDown(self):
frappe.set_user("Administrator")

View File

@ -2,8 +2,6 @@
# For license information, please see license.txt
import unittest
from frappe.utils import add_months, nowdate
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
@ -11,9 +9,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
execute,
)
from erpnext.tests.utils import ERPNextTestCase
class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
def test_result_for_partial_material_request(self):
so = make_sales_order()
mr=make_material_request(so.name)

View File

@ -2,15 +2,14 @@
# For license information, please see license.txt
import unittest
import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.sales_analytics.sales_analytics import execute
from erpnext.tests.utils import ERPNextTestCase
class TestAnalytics(unittest.TestCase):
class TestAnalytics(ERPNextTestCase):
def test_sales_analytics(self):
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")

View File

@ -68,6 +68,8 @@ def set_default_settings(args):
hr_settings.send_interview_feedback_reminder = 1
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():

View File

@ -278,6 +278,11 @@ def install(country=None):
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response,
'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}]
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))

View File

@ -43,9 +43,9 @@ class Bin(Document):
frappe.qb
.from_(wo)
.from_(wo_item)
.select(Case()
.when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty))
.else_(Sum(wo_item.required_qty - wo_item.consumed_qty))
.select(Sum(Case()
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
.else_(wo_item.required_qty - wo_item.consumed_qty))
)
.where(
(wo_item.item_code == self.item_code)

View File

@ -361,8 +361,7 @@
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Valuation Method",
"options": "\nFIFO\nMoving Average",
"set_only_once": 1
"options": "\nFIFO\nMoving Average"
},
{
"depends_on": "is_stock_item",
@ -1035,7 +1034,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-03 08:32:03.869294",
"modified": "2021-12-14 04:13:16.857534",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@ -1,451 +1,140 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2013-04-08 13:10:16",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "hash",
"creation": "2013-04-08 13:10:16",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"column_break_2",
"item_name",
"batch_no",
"desc_section",
"description",
"quantity_section",
"qty",
"net_weight",
"column_break_10",
"stock_uom",
"weight_uom",
"page_break",
"dn_detail"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"print_width": "100px",
"reqd": 1,
"width": "100px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"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
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_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": "Item Name",
"length": 0,
"no_copy": 0,
"options": "item_code.item_name",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "200px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Name",
"print_width": "200px",
"read_only": 1,
"width": "200px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "batch_no",
"fieldtype": "Link",
"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": "Batch No",
"length": 0,
"no_copy": 0,
"options": "Batch",
"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
},
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "desc_section",
"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": "Description",
"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
},
"collapsible": 1,
"fieldname": "desc_section",
"fieldtype": "Section Break",
"label": "Description"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"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": "Description",
"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
},
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "quantity_section",
"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": "Quantity",
"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
},
"fieldname": "quantity_section",
"fieldtype": "Section Break",
"label": "Quantity"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty",
"fieldtype": "Float",
"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": "Quantity",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"print_width": "100px",
"reqd": 1,
"width": "100px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "net_weight",
"fieldtype": "Float",
"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": "Net Weight",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "net_weight",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Net Weight",
"print_width": "100px",
"width": "100px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"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
},
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "stock_uom",
"fieldtype": "Link",
"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": "UOM",
"length": 0,
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"print_width": "100px",
"read_only": 1,
"width": "100px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "weight_uom",
"fieldtype": "Link",
"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": "Weight UOM",
"length": 0,
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "weight_uom",
"fieldtype": "Link",
"label": "Weight UOM",
"options": "UOM",
"print_width": "100px",
"width": "100px"
},
},
{
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "page_break",
"fieldtype": "Check",
"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": "Page Break",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
"allow_on_submit": 1,
"default": "0",
"fieldname": "page_break",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Page Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "dn_detail",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "DN Detail",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
"fieldname": "dn_detail",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "DN Detail"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-06-01 07:21:58.220980",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packing Slip Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-12-14 01:22:00.715935",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packing Slip Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -48,6 +48,7 @@ def get_item_info(filters):
conditions = [get_item_group_condition(filters.get("item_group"))]
if filters.get("brand"):
conditions.append("item.brand=%(brand)s")
conditions.append("is_stock_item = 1")
return frappe.db.sql("""select name, item_name, description, brand, item_group,
safety_stock, lead_time_days from `tabItem` item where {}"""

View File

@ -24,12 +24,10 @@
"service_level_section",
"service_level_agreement",
"response_by",
"response_by_variance",
"reset_service_level_agreement",
"cb",
"agreement_status",
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
@ -123,7 +121,6 @@
"search_index": 1
},
{
"default": "Medium",
"fieldname": "priority",
"fieldtype": "Link",
"in_list_view": 1,
@ -318,22 +315,6 @@
"fieldtype": "Check",
"label": "Via Customer Portal"
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"fieldname": "response_by_variance",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Response By Variance",
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"fieldname": "resolution_by_variance",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Resolution By Variance",
"read_only": 1
},
{
"fieldname": "service_level_agreement_creation",
"fieldtype": "Datetime",
@ -391,12 +372,12 @@
"read_only": 1
},
{
"default": "Ongoing",
"default": "First Response Due",
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "agreement_status",
"fieldtype": "Select",
"label": "Service Level Agreement Status",
"options": "Ongoing\nFulfilled\nFailed",
"options": "First Response Due\nResolution Due\nFulfilled\nFailed",
"read_only": 1
},
{
@ -410,10 +391,11 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
"modified": "2021-06-10 03:22:27.098898",
"modified": "2021-11-24 13:13:10.276630",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -87,11 +87,9 @@ class Issue(Document):
if replicated_issue.service_level_agreement:
replicated_issue.service_level_agreement_creation = now_datetime()
replicated_issue.service_level_agreement = None
replicated_issue.agreement_status = "Ongoing"
replicated_issue.agreement_status = "First Response Due"
replicated_issue.response_by = None
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()

View File

@ -18,7 +18,6 @@ frappe.listview_settings['Issue'] = {
},
get_indicator: function(doc) {
if (doc.status === 'Open') {
if (!doc.priority) doc.priority = 'Medium';
const color = {
'Low': 'yellow',
'Medium': 'orange',

View File

@ -1,10 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
import datetime
import unittest
import frappe
from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.utils import flt, get_datetime
@ -83,30 +83,6 @@ class TestIssue(TestSetUp):
self.assertEqual(issue.agreement_status, 'Fulfilled')
def test_issue_metrics(self):
creation = get_datetime("2020-03-04 4:00")
issue = make_issue(creation, index=1)
create_communication(issue.name, "test@example.com", "Received", creation)
creation = get_datetime("2020-03-04 4:15")
create_communication(issue.name, "test@admin.com", "Sent", creation)
creation = get_datetime("2020-03-04 5:00")
create_communication(issue.name, "test@example.com", "Received", creation)
creation = get_datetime("2020-03-04 5:05")
create_communication(issue.name, "test@admin.com", "Sent", creation)
frappe.flags.current_time = get_datetime("2020-03-04 5:05")
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 = get_datetime("2020-03-04 4:00")
@ -142,6 +118,142 @@ class TestIssue(TestSetUp):
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
def test_issue_close_after_on_hold(self):
frappe.flags.current_time = get_datetime("2021-11-01 19:00")
issue = make_issue(frappe.flags.current_time, index=1)
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
# send a reply within SLA
frappe.flags.current_time = get_datetime("2021-11-02 11:00")
create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
issue.reload()
issue.status = 'Replied'
issue.save()
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
# close the issue after being on hold for 20 days
frappe.flags.current_time = get_datetime("2021-11-22 01:00")
issue.status = 'Closed'
issue.save()
self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00'))
self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00'))
self.assertEqual(issue.agreement_status, 'Fulfilled')
def test_issue_open_after_closed(self):
# Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs
frappe.flags.current_time = get_datetime("2021-11-01 13:00")
issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
self.assertEquals(issue.agreement_status, 'First Response Due')
self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00"))
self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00"))
# Replied on → 2 pm
frappe.flags.current_time = get_datetime("2021-11-01 14:00")
create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
issue.reload()
issue.status = 'Replied'
issue.save()
self.assertEquals(issue.agreement_status, 'Resolution Due')
self.assertEquals(issue.on_hold_since, frappe.flags.current_time)
self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
# Customer Replied → 3 pm
frappe.flags.current_time = get_datetime("2021-11-01 15:00")
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
issue.reload()
self.assertEquals(issue.status, 'Open')
# Hold Time + 1 Hrs
self.assertEquals(issue.total_hold_time, 3600)
# Resolution By should increase by one hrs
self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00"))
# Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm
frappe.flags.current_time = get_datetime("2021-11-01 16:00")
create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
issue.reload()
issue.status = 'Replied'
issue.save()
self.assertEquals(issue.agreement_status, 'Resolution Due')
# Customer Closed → 10 pm
frappe.flags.current_time = get_datetime("2021-11-01 22:00")
issue.status = 'Closed'
issue.save()
# Hold Time + 6 Hrs
self.assertEquals(issue.total_hold_time, 3600 + 21600)
# Resolution By should increase by 6 hrs
self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00"))
self.assertEquals(issue.agreement_status, 'Fulfilled')
self.assertEquals(issue.resolution_date, frappe.flags.current_time)
# Customer Open → 3 am i.e after resolution by is crossed
frappe.flags.current_time = get_datetime("2021-11-02 03:00")
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
issue.reload()
# Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm)
self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000)
# Resolution By should increase by 5 hrs
self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
self.assertEquals(issue.agreement_status, 'Resolution Due')
self.assertFalse(issue.resolution_date)
# We Closed → 4 am, SLA should be Fulfilled
frappe.flags.current_time = get_datetime("2021-11-02 04:00")
issue.status = 'Closed'
issue.save()
self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
self.assertEquals(issue.agreement_status, 'Fulfilled')
self.assertEquals(issue.resolution_date, frappe.flags.current_time)
def test_recording_of_assignment_on_first_reponse_failure(self):
from frappe.desk.form.assign_to import add as add_assignment
frappe.flags.current_time = get_datetime("2021-11-01 19:00")
issue = make_issue(frappe.flags.current_time, index=1)
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
add_assignment({
'doctype': issue.doctype,
'name': issue.name,
'assign_to': ['test@admin.com']
})
issue.reload()
# send a reply failing response SLA
frappe.flags.current_time = get_datetime("2021-11-02 15:00")
create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
# assert if a new timeline item has been added
# to record the assignment
comment = frappe.db.exists('Comment', {
'reference_doctype': 'Issue',
'reference_name': issue.name,
'comment_type': 'Assigned',
'content': _('First Response SLA Failed by {}').format('test')
})
self.assertTrue(comment)
def test_agreement_status_on_response(self):
frappe.flags.current_time = get_datetime("2021-11-01 19:00")
issue = make_issue(frappe.flags.current_time, index=1)
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
self.assertTrue(issue.status == 'Open')
# send a reply within response SLA
frappe.flags.current_time = get_datetime("2021-11-02 11:00")
create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
issue.reload()
self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
self.assertEquals(issue.agreement_status, 'Resolution Due')
class TestFirstResponseTime(TestSetUp):
# working hours used in all cases: Mon-Fri, 10am to 6pm
# all dates are in the mm-dd-yyyy format
@ -355,12 +467,18 @@ class TestFirstResponseTime(TestSetUp):
def create_issue_and_communication(issue_creation, first_responded_on):
issue = make_issue(issue_creation, index=1)
sender = create_user("test@admin.com")
frappe.flags.current_time = first_responded_on
create_communication(issue.name, sender.email, "Sent", first_responded_on)
issue.reload()
return issue
def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
if issue_type and not frappe.db.exists('Issue Type', issue_type):
doc = frappe.new_doc('Issue Type')
doc.name = issue_type
doc.insert()
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),

View File

@ -22,10 +22,41 @@ frappe.ui.form.on('Service Level Agreement', {
refresh: function(frm) {
frm.trigger('fetch_status_fields');
frm.trigger('toggle_resolution_fields');
frm.trigger('default_service_level_agreement');
frm.trigger('entity');
},
default_service_level_agreement: function(frm) {
const field = frm.get_field('default_service_level_agreement');
if (frm.doc.default_service_level_agreement) {
field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type]));
} else {
field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type]));
}
},
document_type: function(frm) {
frm.trigger('fetch_status_fields');
frm.trigger('default_service_level_agreement');
},
entity_type: function(frm) {
frm.set_value('entity', undefined);
},
entity: function(frm) {
const field = frm.get_field('entity');
if (frm.doc.entity) {
const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : '';
field.set_description(
__('SLA will be applied if {1} is set as {2}{3}', [
frm.doc.document_type, frm.doc.entity_type,
frm.doc.entity, and_descendants
])
);
} else {
field.set_description('');
}
},
fetch_status_fields: function(frm) {

View File

@ -6,22 +6,17 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"document_type",
"default_service_level_agreement",
"default_priority",
"column_break_2",
"service_level",
"holiday_list",
"entity_section",
"entity_type",
"column_break_10",
"entity",
"enabled",
"filters_section",
"condition",
"default_service_level_agreement",
"entity_type",
"entity",
"column_break_15",
"condition_description",
"condition",
"agreement_details_section",
"start_date",
"column_break_7",
@ -31,8 +26,10 @@
"priorities",
"status_details",
"sla_fulfilled_on",
"column_break_22",
"pause_sla_on",
"support_and_resolution_section_break",
"holiday_list",
"support_and_resolution"
],
"fields": [
@ -42,7 +39,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Service Level Name",
"reqd": 1
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "holiday_list",
@ -56,10 +54,10 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"depends_on": "eval: doc.document_type",
"fieldname": "agreement_details_section",
"fieldtype": "Section Break",
"label": "Agreement Details"
"label": "Valid From"
},
{
"fieldname": "start_date",
@ -72,7 +70,6 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
@ -80,7 +77,7 @@
{
"fieldname": "response_and_resolution_time_section",
"fieldtype": "Section Break",
"label": "Response and Resolution Time"
"label": "Response and Resolution"
},
{
"fieldname": "support_and_resolution_section_break",
@ -90,6 +87,7 @@
{
"fieldname": "support_and_resolution",
"fieldtype": "Table",
"label": "Working Hours",
"options": "Service Day",
"reqd": 1
},
@ -101,10 +99,7 @@
"reqd": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "entity",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@ -114,22 +109,12 @@
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "entity_section",
"fieldtype": "Section Break",
"label": "Entity"
},
{
"fieldname": "entity_type",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Entity Type",
"options": "\nCustomer\nCustomer Group\nTerritory"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"default": "0",
"fieldname": "default_service_level_agreement",
@ -152,7 +137,7 @@
{
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type",
"label": "Apply On",
"options": "DocType",
"reqd": 1,
"set_only_once": 1
@ -164,6 +149,7 @@
"label": "Enabled"
},
{
"depends_on": "document_type",
"fieldname": "status_details",
"fieldtype": "Section Break",
"label": "Status Details"
@ -182,28 +168,31 @@
"label": "Apply SLA for Resolution Time"
},
{
"depends_on": "document_type",
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Assignment Condition"
"label": "Assignment Conditions"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
"description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition",
"options": "Python"
"max_height": "7rem",
"options": "PythonExpression"
},
{
"fieldname": "condition_description",
"fieldtype": "HTML",
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total &gt; 40000\n</pre>"
"fieldname": "column_break_22",
"fieldtype": "Column Break"
}
],
"links": [],
"modified": "2021-10-02 11:32:55.556024",
"modified": "2021-11-26 15:45:33.289911",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",

View File

@ -10,7 +10,6 @@ from frappe.core.utils import get_parent_doc
from frappe.model.document import Document
from frappe.utils import (
add_to_date,
cint,
get_datetime,
get_datetime_str,
get_link_to_form,
@ -22,6 +21,7 @@ from frappe.utils import (
time_diff_in_seconds,
to_timedelta,
)
from frappe.utils.nestedset import get_ancestors_of
from frappe.utils.safe_exec import get_safe_globals
from erpnext.support.doctype.issue.issue import get_holidays
@ -248,7 +248,7 @@ def get_active_service_level_agreement_for(doc):
customer = doc.get('customer')
or_filters.append(
["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)]
)
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
@ -275,11 +275,23 @@ def get_context(doc):
return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
def get_customer_group(customer):
return frappe.db.get_value("Customer", customer, "customer_group") if customer else None
customer_groups = []
customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None
if customer_group:
ancestors = get_ancestors_of("Customer Group", customer_group)
customer_groups = [customer_group] + ancestors
return customer_groups
def get_customer_territory(customer):
return frappe.db.get_value("Customer", customer, "territory") if customer else None
customer_territories = []
customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None
if customer_territory:
ancestors = get_ancestors_of("Territory", customer_territory)
customer_territories = [customer_territory] + ancestors
return customer_territories
@frappe.whitelist()
@ -299,7 +311,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None):
if customer:
# Include SLA with No Entity and Entity Type
or_filters.append(
["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]]
["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)]
)
return {
@ -337,84 +349,135 @@ def set_documents_with_active_service_level_agreement():
def apply(doc, method=None):
# Applies SLA to document on validate
if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \
doc.doctype not in get_documents_with_active_service_level_agreement():
if (
frappe.flags.in_patch
or frappe.flags.in_migrate
or frappe.flags.in_install
or frappe.flags.in_setup_wizard
or doc.doctype not in get_documents_with_active_service_level_agreement()
):
return
service_level_agreement = get_active_service_level_agreement_for(doc)
sla = get_active_service_level_agreement_for(doc)
if not service_level_agreement:
if not sla:
return
set_sla_properties(doc, service_level_agreement)
process_sla(doc, sla)
def set_sla_properties(doc, service_level_agreement):
if frappe.db.exists(doc.doctype, doc.name):
from_db = frappe.get_doc(doc.doctype, doc.name)
else:
from_db = frappe._dict({})
meta = frappe.get_meta(doc.doctype)
if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \
not service_level_agreement.customer == doc.get("customer"):
frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name,
service_level_agreement.customer))
doc.service_level_agreement = service_level_agreement.name
doc.priority = doc.get("priority") or service_level_agreement.default_priority
priority = get_priority(doc)
def process_sla(doc, sla):
if not doc.creation:
doc.creation = now_datetime(doc.get("owner"))
if meta.has_field("service_level_agreement_creation"):
if doc.meta.has_field("service_level_agreement_creation"):
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
doc.service_level_agreement = sla.name
doc.priority = doc.get("priority") or sla.default_priority
handle_status_change(doc, sla.apply_sla_for_resolution)
update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution)
update_agreement_status(doc, sla.apply_sla_for_resolution)
def handle_status_change(doc, apply_sla_for_resolution):
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status')
hold_statuses = get_hold_statuses(doc.service_level_agreement)
fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement)
def is_hold_status(status):
return status in hold_statuses
def is_fulfilled_status(status):
return status in fulfillment_statuses
def is_open_status(status):
return status not in hold_statuses and status not in fulfillment_statuses
def set_first_response():
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
doc.first_responded_on = now_time
if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')):
record_assigned_users_on_failure(doc)
def calculate_hold_hours():
# In case issue was closed and after few days it has been opened
# The hold time should be calculated from resolution_date
on_hold_since = doc.resolution_date or doc.on_hold_since
if on_hold_since:
current_hold_hours = time_diff_in_seconds(now_time, on_hold_since)
doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours
doc.on_hold_since = None
if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply):
set_first_response()
# Open to Replied
if is_open_status(prev_status) and is_hold_status(doc.status):
# Issue is on hold -> Set on_hold_since
doc.on_hold_since = now_time
reset_expected_response_and_resolution(doc)
# Replied to Open
if is_hold_status(prev_status) and is_open_status(doc.status):
# Issue was on hold -> Calculate Total Hold Time
calculate_hold_hours()
# Issue is open -> reset resolution_date
reset_resolution_metrics(doc)
# Open to Closed
if is_open_status(prev_status) and is_fulfilled_status(doc.status):
# Issue is closed -> Set resolution_date
doc.resolution_date = now_time
set_resolution_time(doc)
# Closed to Open
if is_fulfilled_status(prev_status) and is_open_status(doc.status):
# Issue was closed -> Calculate Total Hold Time from resolution_date
calculate_hold_hours()
# Issue is open -> reset resolution_date
reset_resolution_metrics(doc)
# Closed to Replied
if is_fulfilled_status(prev_status) and is_hold_status(doc.status):
# Issue was closed -> Calculate Total Hold Time from resolution_date
calculate_hold_hours()
# Issue is on hold -> Set on_hold_since
doc.on_hold_since = now_time
reset_expected_response_and_resolution(doc)
# Replied to Closed
if is_hold_status(prev_status) and is_fulfilled_status(doc.status):
# Issue was on hold -> Calculate Total Hold Time
calculate_hold_hours()
# Issue is closed -> Set resolution_date
if apply_sla_for_resolution:
doc.resolution_date = now_time
set_resolution_time(doc)
def get_fulfillment_statuses(service_level_agreement):
return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
"parent": service_level_agreement
}, fields=["status"])]
def get_hold_statuses(service_level_agreement):
return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
"parent": service_level_agreement
}, fields=["status"])]
def update_response_and_resolution_metrics(doc, apply_sla_for_resolution):
priority = get_response_and_resolution_duration(doc)
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
set_response_by_and_variance(doc, meta, start_date_time, priority)
if service_level_agreement.apply_sla_for_resolution:
set_resolution_by_and_variance(doc, meta, start_date_time, priority)
update_status(doc, from_db, meta)
def update_status(doc, from_db, meta):
if meta.has_field("status"):
if meta.has_field("first_responded_on") and doc.status != "Open" and \
from_db.status == "Open" and not doc.first_responded_on:
doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner"))
if meta.has_field("service_level_agreement") and doc.service_level_agreement:
# mark sla status as fulfilled based on the configuration
fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
"parent": doc.service_level_agreement
}, fields=["status"])]
if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses:
apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
"apply_sla_for_resolution")
if apply_sla_for_resolution and meta.has_field("resolution_date"):
doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner"))
if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing":
set_service_level_agreement_variance(doc.doctype, doc.name)
update_agreement_status(doc, meta)
if apply_sla_for_resolution:
set_resolution_time(doc, meta)
set_user_resolution_time(doc, meta)
if doc.status == "Open" and from_db.status != "Open":
# if no date, it should be set as None and not a blank string "", as per mysql strict config
# enable SLA and variance on Reopen
reset_metrics(doc, meta)
set_service_level_agreement_variance(doc.doctype, doc.name)
handle_hold_time(doc, meta, from_db.status)
set_response_by(doc, start_date_time, priority)
if apply_sla_for_resolution:
set_resolution_by(doc, start_date_time, priority)
def get_expected_time_for(parameter, service_level, start_date_time):
@ -485,37 +548,13 @@ def get_support_days(service_level):
return support_days
def set_service_level_agreement_variance(doctype, doc=None):
def set_resolution_time(doc):
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
if doc.meta.has_field("resolution_time"):
doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time)
filters = {"status": "Open", "agreement_status": "Ongoing"}
if doc:
filters = {"name": doc}
for entry in frappe.get_all(doctype, filters=filters):
current_doc = frappe.get_doc(doctype, entry.name)
current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner"))
apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement,
"apply_sla_for_resolution")
if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer
variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2)
frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False)
if variance < 0:
frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed
variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2)
frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False)
if variance < 0:
frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
def set_user_resolution_time(doc, meta):
# total time taken by a user to close the issue apart from wait_time
if not meta.has_field("user_resolution_time"):
if not doc.meta.has_field("user_resolution_time"):
return
communications = frappe.get_all("Communication", filters={
@ -531,7 +570,7 @@ def set_user_resolution_time(doc, meta):
pending_time.append(wait_time)
total_pending_time = sum(pending_time)
resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation)
resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time)
doc.user_resolution_time = resolution_time_in_secs - total_pending_time
@ -548,12 +587,12 @@ def change_service_level_agreement_and_priority(self):
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
def get_priority(doc):
service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
priority = service_level_agreement.get_service_level_agreement_priority(doc.priority)
def get_response_and_resolution_duration(doc):
sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
priority = sla.get_service_level_agreement_priority(doc.priority)
priority.update({
"support_and_resolution": service_level_agreement.support_and_resolution,
"holiday_list": service_level_agreement.holiday_list
"support_and_resolution": sla.support_and_resolution,
"holiday_list": sla.holiday_list
})
return priority
@ -572,120 +611,102 @@ def reset_service_level_agreement(doc, reason, user):
}).insert(ignore_permissions=True)
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement)
doc.agreement_status = "Ongoing"
doc.save()
def reset_metrics(doc, meta):
if meta.has_field("resolution_date"):
def reset_resolution_metrics(doc):
if doc.meta.has_field("resolution_date"):
doc.resolution_date = None
if not meta.has_field("resolution_time"):
if doc.meta.has_field("resolution_time"):
doc.resolution_time = None
if not meta.has_field("user_resolution_time"):
if doc.meta.has_field("user_resolution_time"):
doc.user_resolution_time = None
if meta.has_field("agreement_status"):
doc.agreement_status = "Ongoing"
def set_resolution_time(doc, meta):
# total time taken from issue creation to closing
if not meta.has_field("resolution_time"):
return
doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation)
if doc.meta.has_field("agreement_status"):
doc.agreement_status = "First Response Due"
# called via hooks on communication update
def update_hold_time(doc, status):
def on_communication_update(doc, status):
if doc.communication_type == "Comment":
return
parent = get_parent_doc(doc)
if not parent:
return
if doc.communication_type == "Comment":
if not parent.meta.has_field('service_level_agreement'):
return
status_field = parent.meta.get_field("status")
if status_field:
options = (status_field.options or "").splitlines()
if (
doc.sent_or_received == "Received" # a reply is received
and parent.get('status') == 'Open' # issue status is set as open from communication.py
and parent.get_doc_before_save()
and parent.get('status') != parent._doc_before_save.get('status') # status changed
):
# undo the status change in db
# since prev status is fetched from db
frappe.db.set_value(
parent.doctype, parent.name,
'status', parent._doc_before_save.get('status'),
update_modified=False
)
# if status has a "Replied" option, then handle hold time
if ("Replied" in options) and doc.sent_or_received == "Received":
meta = frappe.get_meta(parent.doctype)
handle_hold_time(parent, meta, 'Replied')
elif (
doc.sent_or_received == "Sent" # a reply is sent
and parent.get('first_responded_on') # first_responded_on is set from communication.py
and parent.get_doc_before_save()
and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set
):
# reset first_responded_on since it will be handled/set later on
parent.first_responded_on = None
parent.flags.on_first_reply = True
else:
return
for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution')
handle_status_change(parent, for_resolution)
update_response_and_resolution_metrics(parent, for_resolution)
update_agreement_status(parent, for_resolution)
parent.save()
def handle_hold_time(doc, meta, status):
if meta.has_field("service_level_agreement") and doc.service_level_agreement:
# set response and resolution variance as None as the issue is on Hold for status as Replied
hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
"parent": doc.service_level_agreement
}, fields=["status"])]
if not hold_statuses:
return
if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses:
apply_hold_status(doc, meta)
# calculate hold time when status is changed from any hold status to any non-hold status
if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses:
reset_hold_status_and_update_hold_time(doc, meta)
def reset_expected_response_and_resolution(doc):
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
doc.response_by = None
if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'):
doc.resolution_by = None
def apply_hold_status(doc, meta):
update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))}
if meta.has_field("first_responded_on") and not doc.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
doc.db_set(update_values)
def set_response_by(doc, start_date_time, priority):
if doc.meta.has_field("response_by"):
doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'):
doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time')))
def reset_hold_status_and_update_hold_time(doc, meta):
hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
last_hold_time = 0
update_values = {}
def set_resolution_by(doc, start_date_time, priority):
if doc.meta.has_field("resolution_by"):
doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'):
doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time')))
if meta.has_field("on_hold_since") and doc.on_hold_since:
# last_hold_time will be added to the sla variables
last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since)
update_values['total_hold_time'] = hold_time + last_hold_time
# re-calculate SLA variables after issue changes from any hold status to any non-hold status
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
priority = get_priority(doc)
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
# add hold time to response by variance
if meta.has_field("first_responded_on") and not doc.first_responded_on:
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
response_by = add_to_date(response_by, seconds=round(last_hold_time))
response_by_variance = round(time_diff_in_seconds(response_by, now_time))
update_values['response_by'] = response_by
update_values['response_by_variance'] = response_by_variance + last_hold_time
# add hold time to resolution by variance
if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"):
resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
update_values['resolution_by'] = resolution_by
update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
update_values['on_hold_since'] = None
doc.db_set(update_values)
def record_assigned_users_on_failure(doc):
assigned_users = doc.get_assigned_users()
if assigned_users:
from frappe.utils import get_fullname
assigned_users = ', '.join((get_fullname(user) for user in assigned_users))
message = _('First Response SLA Failed by {}').format(assigned_users)
doc.add_comment(
comment_type='Assigned',
text=message
)
def get_service_level_agreement_fields():
@ -714,17 +735,11 @@ def get_service_level_agreement_fields():
"label": "Response By",
"read_only": 1
},
{
"fieldname": "response_by_variance",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Response By Variance",
"read_only": 1
},
{
"fieldname": "first_responded_on",
"fieldtype": "Datetime",
"label": "First Responded On",
"no_copy": 1,
"read_only": 1
},
{
@ -746,11 +761,11 @@ def get_service_level_agreement_fields():
"read_only": 1
},
{
"default": "Ongoing",
"default": "First Response Due",
"fieldname": "agreement_status",
"fieldtype": "Select",
"label": "Service Level Agreement Status",
"options": "Ongoing\nFulfilled\nFailed",
"options": "First Response Due\nResolution Due\nFulfilled\nFailed",
"read_only": 1
},
{
@ -759,13 +774,6 @@ def get_service_level_agreement_fields():
"label": "Resolution By",
"read_only": 1
},
{
"fieldname": "resolution_by_variance",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Resolution By Variance",
"read_only": 1
},
{
"fieldname": "service_level_agreement_creation",
"fieldtype": "Datetime",
@ -786,43 +794,28 @@ def get_service_level_agreement_fields():
def update_agreement_status_on_custom_status(doc):
# Update Agreement Fulfilled status using Custom Scripts for Custom Status
meta = frappe.get_meta(doc.doctype)
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
if meta.has_field("first_responded_on") and not doc.first_responded_on:
# first_responded_on set when first reply is sent to customer
doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
if meta.has_field("resolution_date") and not doc.resolution_date:
# resolution_date set when issue has been closed
doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
if meta.has_field("agreement_status"):
doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed"
update_agreement_status(doc)
def update_agreement_status(doc, meta):
if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \
doc.service_level_agreement and doc.agreement_status == "Ongoing":
apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
"apply_sla_for_resolution")
def update_agreement_status(doc, apply_sla_for_resolution):
if (doc.meta.has_field("agreement_status")):
# if SLA is applied for resolution check for response and resolution, else only response
if apply_sla_for_resolution:
if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"):
if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \
cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0:
doc.agreement_status = "Failed"
else:
doc.agreement_status = "Fulfilled"
else:
if meta.has_field("response_by_variance") and \
cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0:
doc.agreement_status = "Failed"
else:
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
doc.agreement_status = "First Response Due"
elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'):
doc.agreement_status = "Resolution Due"
elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')):
doc.agreement_status = "Fulfilled"
else:
doc.agreement_status = "Failed"
else:
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
doc.agreement_status = "First Response Due"
elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')):
doc.agreement_status = "Fulfilled"
else:
doc.agreement_status = "Failed"
def is_holiday(date, holidays):
@ -835,23 +828,6 @@ def get_time_in_timedelta(time):
return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
def set_response_by_and_variance(doc, meta, start_date_time, priority):
if meta.has_field("response_by"):
doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
if meta.has_field("response_by_variance") and not doc.get('first_responded_on'):
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
def set_resolution_by_and_variance(doc, meta, start_date_time, priority):
if meta.has_field("resolution_by"):
doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"):
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
def now_datetime(user):
dt = convert_utc_to_user_timezone(datetime.utcnow(), user)
return dt.replace(tzinfo=None)

View File

@ -220,42 +220,6 @@ class TestServiceLevelAgreement(unittest.TestCase):
lead.reload()
self.assertEqual(lead.agreement_status, 'Fulfilled')
def test_changing_of_variance_after_response(self):
# create lead
doctype = "Lead"
lead_sla = create_service_level_agreement(
default_service_level_agreement=1,
holiday_list="__Test Holiday List",
entity_type=None, entity=None,
response_time=14400,
doctype=doctype,
sla_fulfilled_on=[{"status": "Replied"}],
apply_sla_for_resolution=0
)
creation = datetime.datetime(2019, 3, 4, 12, 0)
lead = make_lead(creation=creation, index=2)
self.assertEqual(lead.service_level_agreement, lead_sla.name)
# set lead as replied to set first responded on
frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30)
lead.reload()
lead.status = 'Replied'
lead.save()
lead.reload()
self.assertEqual(lead.agreement_status, 'Fulfilled')
# check response_by_variance
self.assertEqual(lead.first_responded_on, frappe.flags.current_time)
self.assertEqual(lead.response_by_variance, 1800.0)
# make a change on the document &
# check response_by_variance is unchanged
frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30)
lead.status = 'Open'
lead.save()
lead.reload()
self.assertEqual(lead.response_by_variance, 1800.0)
def test_service_level_agreement_filters(self):
doctype = "Lead"
lead_sla = create_service_level_agreement(
@ -295,7 +259,8 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ
return service_level_agreement
def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type,
entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1):
entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1,
service_level=None, start_time="10:00:00", end_time="18:00:00"):
make_holiday_list()
make_priorities()
@ -312,7 +277,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
"doctype": "Service Level Agreement",
"enabled": 1,
"document_type": doctype,
"service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"),
"service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"),
"default_service_level_agreement": default_service_level_agreement,
"condition": condition,
"default_priority": "Medium",
@ -345,28 +310,28 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
"support_and_resolution": [
{
"workday": "Monday",
"start_time": "10:00:00",
"end_time": "18:00:00",
"start_time": start_time,
"end_time": end_time,
},
{
"workday": "Tuesday",
"start_time": "10:00:00",
"end_time": "18:00:00",
"start_time": start_time,
"end_time": end_time,
},
{
"workday": "Wednesday",
"start_time": "10:00:00",
"end_time": "18:00:00",
"start_time": start_time,
"end_time": end_time,
},
{
"workday": "Thursday",
"start_time": "10:00:00",
"end_time": "18:00:00",
"start_time": start_time,
"end_time": end_time,
},
{
"workday": "Friday",
"start_time": "10:00:00",
"end_time": "18:00:00",
"start_time": start_time,
"end_time": end_time,
}
]
})
@ -386,7 +351,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
if sla:
frappe.delete_doc("Service Level Agreement", sla, force=1)
return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True)
def create_customer():
@ -443,6 +408,13 @@ def create_service_level_agreements_for_issues():
create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
create_service_level_agreement(
default_service_level_agreement=0, holiday_list="__Test Holiday List",
entity_type=None, entity=None, response_time=14400, resolution_time=21600,
service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59",
condition="doc.issue_type == 'Critical'"
)
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:

View File

@ -82,7 +82,8 @@ class IssueSummary(object):
self.sla_status_map = {
'SLA Failed': 'failed',
'SLA Fulfilled': 'fulfilled',
'SLA Ongoing': 'ongoing'
'First Response Due': 'first_response_due',
'Resolution Due': 'resolution_due'
}
for label, fieldname in self.sla_status_map.items():