Merge branch 'develop' into so-days-taken-to-deliver
This commit is contained in:
commit
8aa9481a11
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
106
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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
|
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea to improve ERPNext
|
about: Suggest an idea to improve ERPNext
|
||||||
|
title: ''
|
||||||
labels: feature-request
|
labels: feature-request
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -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
56
.github/stale.yml
vendored
@ -1,34 +1,36 @@
|
|||||||
# Configuration for probot-stale - https://github.com/probot/stale
|
# 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
|
# Label to use when marking as stale
|
||||||
staleLabel: inactive
|
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
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
limitPerRun: 30
|
limitPerRun: 10
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
only: pulls
|
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.
|
||||||
|
10
codecov.yml
10
codecov.yml
@ -8,6 +8,16 @@ coverage:
|
|||||||
target: auto
|
target: auto
|
||||||
threshold: 0.5%
|
threshold: 0.5%
|
||||||
|
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 85%
|
||||||
|
threshold: 0%
|
||||||
|
base: auto
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
if_ci_failed: ignore
|
||||||
|
only_pulls: true
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
layout: "diff, files"
|
layout: "diff, files"
|
||||||
require_changes: true
|
require_changes: true
|
||||||
|
@ -297,8 +297,15 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
|
|
||||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
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)
|
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:
|
elif item.is_fixed_asset and item.pr_detail:
|
||||||
item.expense_account = asset_received_but_not_billed
|
item.expense_account = asset_received_but_not_billed
|
||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
|
@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def set_ageing(self, row):
|
def set_ageing(self, row):
|
||||||
if self.filters.ageing_based_on == "Due Date":
|
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":
|
elif self.filters.ageing_based_on == "Supplier Invoice Date":
|
||||||
entry_date = row.bill_date
|
entry_date = row.bill_date
|
||||||
else:
|
else:
|
||||||
|
@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
|||||||
posting_date = entry.posting_date
|
posting_date = entry.posting_date
|
||||||
voucher_type = entry.voucher_type
|
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:
|
if entry.account in tds_accounts:
|
||||||
tds_deducted += (entry.credit - entry.debit)
|
tds_deducted += (entry.credit - entry.debit)
|
||||||
|
|
||||||
total_amount_credited += (entry.credit - entry.debit)
|
total_amount_credited += (entry.credit - entry.debit)
|
||||||
|
|
||||||
if rate and tds_deducted:
|
if tds_deducted:
|
||||||
row = {
|
row = {
|
||||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
'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():
|
def get_supplier_pan_map():
|
||||||
supplier_map = frappe._dict()
|
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:
|
for d in suppliers:
|
||||||
supplier_map[d.name] = d
|
supplier_map[d.name] = d
|
||||||
|
@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
if (frm.doc.status != 'Fully Depreciated') {
|
if (frm.doc.status != 'Fully Depreciated') {
|
||||||
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
||||||
frm.trigger("create_asset_adjustment");
|
frm.trigger("create_asset_value_adjustment");
|
||||||
}, __("Manage"));
|
}, __("Manage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_asset_adjustment: function(frm) {
|
create_asset_value_adjustment: function(frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
args: {
|
args: {
|
||||||
"asset": frm.doc.name,
|
"asset": frm.doc.name,
|
||||||
"asset_category": frm.doc.asset_category,
|
"asset_category": frm.doc.asset_category,
|
||||||
"company": frm.doc.company
|
"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,
|
freeze: 1,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
var doclist = frappe.model.sync(r.message);
|
var doclist = frappe.model.sync(r.message);
|
||||||
|
@ -185,83 +185,84 @@ class Asset(AccountsController):
|
|||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.get('finance_books'):
|
start = self.clear_depreciation_schedule()
|
||||||
self.validate_asset_finance_books(d)
|
|
||||||
|
|
||||||
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
|
# value_after_depreciation - current Asset value
|
||||||
if self.docstatus == 1 and d.value_after_depreciation:
|
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||||
value_after_depreciation = flt(d.value_after_depreciation)
|
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||||
else:
|
else:
|
||||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||||
flt(self.opening_accumulated_depreciation))
|
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)
|
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:
|
if has_pro_rata:
|
||||||
number_of_pending_depreciations += 1
|
number_of_pending_depreciations += 1
|
||||||
|
|
||||||
skip_row = False
|
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 depreciation is already completed (for double declining balance)
|
||||||
if skip_row: continue
|
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:
|
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||||
schedule_date = add_months(d.depreciation_start_date,
|
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||||
n * cint(d.frequency_of_depreciation))
|
n * cint(finance_book.frequency_of_depreciation))
|
||||||
|
|
||||||
# schedule date will be a year later from start date
|
# schedule date will be a year later from start date
|
||||||
# so monthly schedule date is calculated by removing 11 months from it
|
# 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 asset is being sold
|
||||||
if date_of_sale:
|
if date_of_sale:
|
||||||
from_date = self.get_from_date(d.finance_book)
|
from_date = self.get_from_date(finance_book.finance_book)
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
from_date, date_of_sale)
|
from_date, date_of_sale)
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": date_of_sale,
|
"schedule_date": date_of_sale,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"finance_book_id": finance_book.idx
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# For first row
|
# For first row
|
||||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
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,
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
self.available_for_use_date, d.depreciation_start_date)
|
self.available_for_use_date, finance_book.depreciation_start_date)
|
||||||
|
|
||||||
# For first depr schedule date will be the 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
|
# 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
|
# For last row
|
||||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||||
if not self.flags.increase_in_asset_life:
|
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
|
# 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,
|
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_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, schedule_date, self.to_date)
|
||||||
|
|
||||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
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)
|
monthly_schedule_date = add_months(schedule_date, 1)
|
||||||
schedule_date = add_days(schedule_date, days)
|
schedule_date = add_days(schedule_date, days)
|
||||||
@ -272,10 +273,10 @@ class Asset(AccountsController):
|
|||||||
self.precision("gross_purchase_amount"))
|
self.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
# 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
|
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||||
and value_after_depreciation != d.expected_value_after_useful_life)
|
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||||
or value_after_depreciation < d.expected_value_after_useful_life):
|
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||||
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
|
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||||
skip_row = True
|
skip_row = True
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
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
|
# In pro rata case, for first and last depreciation, month range would be different
|
||||||
month_range = months \
|
month_range = months \
|
||||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
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):
|
for r in range(month_range):
|
||||||
if (has_pro_rata and n == 0):
|
if (has_pro_rata and n == 0):
|
||||||
@ -311,27 +312,52 @@ class Asset(AccountsController):
|
|||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": date,
|
"schedule_date": date,
|
||||||
"depreciation_amount": amount,
|
"depreciation_amount": amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"finance_book_id": finance_book.idx
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": schedule_date,
|
"schedule_date": schedule_date,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"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):
|
def clear_depreciation_schedule(self):
|
||||||
start = 0
|
start = []
|
||||||
for n in range(len(self.schedules)):
|
num_of_depreciations_completed = 0
|
||||||
if not self.schedules[n].journal_entry:
|
depr_schedule = []
|
||||||
del self.schedules[n:]
|
|
||||||
start = n
|
for schedule in self.get('schedules'):
|
||||||
break
|
|
||||||
|
# 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
|
return start
|
||||||
|
|
||||||
def get_from_date(self, finance_book):
|
def get_from_date(self, finance_book):
|
||||||
@ -469,7 +495,6 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
asset_value_after_full_schedule = flt(
|
asset_value_after_full_schedule = flt(
|
||||||
flt(self.gross_purchase_amount) -
|
flt(self.gross_purchase_amount) -
|
||||||
flt(self.opening_accumulated_depreciation) -
|
|
||||||
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
||||||
|
|
||||||
if (row.expected_value_after_useful_life and
|
if (row.expected_value_after_useful_life and
|
||||||
@ -731,14 +756,14 @@ def create_asset_repair(asset, asset_name):
|
|||||||
return asset_repair
|
return asset_repair
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_adjustment(asset, asset_category, company):
|
def create_asset_value_adjustment(asset, asset_category, company):
|
||||||
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
|
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||||
asset_maintenance.update({
|
asset_value_adjustment.update({
|
||||||
"asset": asset,
|
"asset": asset,
|
||||||
"company": company,
|
"company": company,
|
||||||
"asset_category": asset_category
|
"asset_category": asset_category
|
||||||
})
|
})
|
||||||
return asset_maintenance
|
return asset_value_adjustment
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def transfer_asset(args):
|
def transfer_asset(args):
|
||||||
|
@ -955,6 +955,82 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
|
|
||||||
self.assertEqual(len(asset.schedules), 1)
|
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):
|
def test_depreciation_entry_cancellation(self):
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code = "Macbook Pro",
|
item_code = "Macbook Pro",
|
||||||
|
@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
dialog.show()
|
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) => {
|
preview: (frm) => {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __('Preview Email'),
|
title: __('Preview Email'),
|
||||||
@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
dialog.show();
|
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",{
|
frappe.ui.form.on("Request for Quotation Supplier",{
|
||||||
supplier: function(frm, cdt, cdn) {
|
supplier: function(frm, cdt, cdn) {
|
||||||
var d = locals[cdt][cdn]
|
var d = locals[cdt][cdn]
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"vendor",
|
"vendor",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"schedule_date",
|
||||||
"status",
|
"status",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"suppliers_section",
|
"suppliers_section",
|
||||||
@ -246,16 +247,22 @@
|
|||||||
"fieldname": "sec_break_email_2",
|
"fieldname": "sec_break_email_2",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hide_border": 1
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "schedule_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Required Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-05 22:04:29.017134",
|
"modified": "2021-11-24 17:47:49.909000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"company",
|
"company",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"valid_till",
|
"valid_till",
|
||||||
|
"quotation_number",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"address_section",
|
"address_section",
|
||||||
"supplier_address",
|
"supplier_address",
|
||||||
@ -797,6 +798,11 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Valid Till"
|
"label": "Valid Till"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quotation_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Quotation Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
@ -804,10 +810,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 00:58:20.995491",
|
"modified": "2021-12-11 06:43:20.924080",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation",
|
"name": "Supplier Quotation",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -91,8 +91,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
|
"modified": "2021-11-03 10:00:36.883496",
|
||||||
"modified": "2021-11-03 09:00:36.883496",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM Settings",
|
"name": "CRM Settings",
|
||||||
|
@ -510,8 +510,7 @@
|
|||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
|
"modified": "2021-10-21 12:04:30.151379",
|
||||||
"modified": "2021-10-21 11:04:30.151379",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
@ -115,8 +115,7 @@
|
|||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
|
"modified": "2021-11-30 12:17:24.647979",
|
||||||
"modified": "2021-11-30 11:17:24.647979",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "ERPNext Integrations",
|
"module": "ERPNext Integrations",
|
||||||
"name": "TaxJar Settings",
|
"name": "TaxJar Settings",
|
||||||
|
@ -234,7 +234,7 @@ doc_events = {
|
|||||||
},
|
},
|
||||||
"Communication": {
|
"Communication": {
|
||||||
"on_update": [
|
"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"
|
"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.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
"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"
|
|
||||||
],
|
],
|
||||||
"hourly_long": [
|
"hourly_long": [
|
||||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||||
|
@ -21,7 +21,11 @@ def get_data():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Lifecycle'),
|
'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'),
|
'label': _('Shift'),
|
||||||
|
0
erpnext/hr/doctype/exit_interview/__init__.py
Normal file
0
erpnext/hr/doctype/exit_interview/__init__.py
Normal file
38
erpnext/hr/doctype/exit_interview/exit_interview.js
Normal file
38
erpnext/hr/doctype/exit_interview/exit_interview.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
246
erpnext/hr/doctype/exit_interview/exit_interview.json
Normal file
246
erpnext/hr/doctype/exit_interview/exit_interview.json
Normal 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
|
||||||
|
}
|
131
erpnext/hr/doctype/exit_interview/exit_interview.py
Normal file
131
erpnext/hr/doctype/exit_interview/exit_interview.py
Normal 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)
|
27
erpnext/hr/doctype/exit_interview/exit_interview_list.js
Normal file
27
erpnext/hr/doctype/exit_interview/exit_interview_list.js
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -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>
|
118
erpnext/hr/doctype/exit_interview/test_exit_interview.py
Normal file
118
erpnext/hr/doctype/exit_interview/test_exit_interview.py
Normal 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
|
@ -36,7 +36,11 @@
|
|||||||
"remind_before",
|
"remind_before",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"send_interview_feedback_reminder",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -226,13 +230,34 @@
|
|||||||
"fieldname": "check_vacancies",
|
"fieldname": "check_vacancies",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Vacancies On Job Offer Creation"
|
"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",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-01 23:46:11.098236",
|
"modified": "2021-12-05 14:48:10.884253",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"name": "HR Settings",
|
||||||
|
@ -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 }}"
|
||||||
|
}
|
@ -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>
|
@ -0,0 +1,6 @@
|
|||||||
|
# import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
# do your magic here
|
||||||
|
pass
|
0
erpnext/hr/report/employee_exits/__init__.py
Normal file
0
erpnext/hr/report/employee_exits/__init__.py
Normal file
77
erpnext/hr/report/employee_exits/employee_exits.js
Normal file
77
erpnext/hr/report/employee_exits/employee_exits.js
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
33
erpnext/hr/report/employee_exits/employee_exits.json
Normal file
33
erpnext/hr/report/employee_exits/employee_exits.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
230
erpnext/hr/report/employee_exits/employee_exits.py
Normal file
230
erpnext/hr/report/employee_exits/employee_exits.py
Normal 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'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
242
erpnext/hr/report/employee_exits/test_employee_exits.py
Normal file
242
erpnext/hr/report/employee_exits/test_employee_exits.py
Normal 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()
|
@ -5,7 +5,7 @@
|
|||||||
"label": "Outgoing Salary"
|
"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 & 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",
|
"creation": "2020-03-02 15:48:58.322521",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workspace",
|
"doctype": "Workspace",
|
||||||
@ -15,14 +15,6 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"label": "HR",
|
"label": "HR",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Employee",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -111,14 +103,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Employee Lifecycle",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Job Applicant",
|
"dependencies": "Job Applicant",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -227,14 +211,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Shift Management",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -268,14 +244,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Leaves",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -386,14 +354,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Attendance",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Employee",
|
"dependencies": "Employee",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -449,14 +409,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Expense Claims",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Employee",
|
"dependencies": "Employee",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -489,14 +441,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -530,14 +474,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Fleet Management",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
@ -581,14 +517,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Recruitment",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -808,14 +736,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Key Reports",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Attendance",
|
"dependencies": "Attendance",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -933,9 +853,796 @@
|
|||||||
"link_type": "Report",
|
"link_type": "Report",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"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",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR",
|
"name": "HR",
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, today
|
from frappe.utils import add_months, today
|
||||||
|
|
||||||
from erpnext import get_company_currency
|
from erpnext import get_company_currency
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
from .blanket_order import make_order
|
from .blanket_order import make_order
|
||||||
|
|
||||||
|
|
||||||
class TestBlanketOrder(unittest.TestCase):
|
class TestBlanketOrder(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.flags.args = frappe._dict()
|
frappe.flags.args = frappe._dict()
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_records = frappe.get_test_records('BOM')
|
test_records = frappe.get_test_records('BOM')
|
||||||
|
|
||||||
class TestBOM(unittest.TestCase):
|
class TestBOM(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if not frappe.get_value('Item', '_Test Item'):
|
if not frappe.get_value('Item', '_Test Item'):
|
||||||
make_test_records('Item')
|
make_test_records('Item')
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
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.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_records = frappe.get_test_records('BOM')
|
test_records = frappe.get_test_records('BOM')
|
||||||
|
|
||||||
class TestBOMUpdateTool(unittest.TestCase):
|
class TestBOMUpdateTool(ERPNextTestCase):
|
||||||
def test_replace_bom(self):
|
def test_replace_bom(self):
|
||||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||||
|
|
||||||
|
@ -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.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||||
frm.trigger("prepare_timer_buttons");
|
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) {
|
setup_corrective_job_card: function(frm) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string
|
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.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
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.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):
|
def setUp(self):
|
||||||
make_bom_for_jc_tests()
|
make_bom_for_jc_tests()
|
||||||
|
|
||||||
@ -329,4 +329,4 @@ def make_bom_for_jc_tests():
|
|||||||
bom.rm_cost_as_per = "Valuation Rate"
|
bom.rm_cost_as_per = "Valuation Rate"
|
||||||
bom.items[0].uom = "_Test UOM 1"
|
bom.items[0].uom = "_Test UOM 1"
|
||||||
bom.items[0].conversion_factor = 5
|
bom.items[0].conversion_factor = 5
|
||||||
bom.insert()
|
bom.insert()
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_to_date, flt, now_datetime, nowdate
|
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 (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProductionPlan(unittest.TestCase):
|
class TestProductionPlan(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
||||||
'Raw Material Item 1', 'Raw Material Item 2']:
|
'Raw Material Item 1', 'Raw Material Item 2']:
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
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.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.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestRouting(unittest.TestCase):
|
class TestRouting(ERPNextTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.item_code = "Test Routing Item - A"
|
cls.item_code = "Test Routing Item - A"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, cint, flt, now, today
|
from frappe.utils import add_months, cint, flt, now, today
|
||||||
@ -29,6 +28,9 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
self.warehouse = '_Test Warehouse 2 - _TC'
|
self.warehouse = '_Test Warehouse 2 - _TC'
|
||||||
self.item = '_Test Item'
|
self.item = '_Test Item'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def check_planned_qty(self):
|
def check_planned_qty(self):
|
||||||
|
|
||||||
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
|
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):
|
def test_reserved_qty_for_partial_completion(self):
|
||||||
item = "_Test Item"
|
item = "_Test Item"
|
||||||
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
bin1_at_start = get_bin(item, warehouse)
|
bin1_at_start = get_bin(item, warehouse)
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
|
||||||
@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
|
|||||||
WorkstationHolidayError,
|
WorkstationHolidayError,
|
||||||
check_if_within_operating_hours,
|
check_if_within_operating_hours,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ["Warehouse"]
|
test_dependencies = ["Warehouse"]
|
||||||
test_records = frappe.get_test_records('Workstation')
|
test_records = frappe.get_test_records('Workstation')
|
||||||
make_test_records('Workstation')
|
make_test_records('Workstation')
|
||||||
|
|
||||||
class TestWorkstation(unittest.TestCase):
|
class TestWorkstation(ERPNextTestCase):
|
||||||
def test_validate_timings(self):
|
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 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")
|
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
||||||
|
@ -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.set_default_payroll_based_on
|
||||||
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
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.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.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.fix_quotation_expired_status
|
||||||
erpnext.patches.v12_0.rename_pos_closing_doctype
|
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.v14_0.delete_einvoicing_doctypes
|
||||||
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
|
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.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.v13_0.create_gst_payment_entry_fields #27-11-2021
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||||
erpnext.patches.v13_0.fix_invoice_statuses
|
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.v13_0.create_pan_field_for_india #2
|
||||||
erpnext.patches.v14_0.delete_hub_doctypes
|
erpnext.patches.v14_0.delete_hub_doctypes
|
||||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
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.v14_0.migrate_crm_settings
|
||||||
erpnext.patches.v13_0.rename_ksa_qr_field
|
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others
|
erpnext.patches.v13_0.disable_ksa_print_format_for_others
|
||||||
|
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
@ -2,6 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
from frappe.model.utils.rename_field import rename_field
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
|
||||||
@ -12,5 +13,20 @@ def execute():
|
|||||||
|
|
||||||
if frappe.db.exists('DocType', 'Sales Invoice'):
|
if frappe.db.exists('DocType', 'Sales Invoice'):
|
||||||
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
|
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'):
|
if frappe.db.has_column('Sales Invoice', 'qr_code'):
|
||||||
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
|
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
|
||||||
|
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")
|
||||||
|
@ -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()
|
@ -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')
|
@ -940,10 +940,12 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
|
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
|
||||||
amount, additional_amount = row.amount, row.additional_amount
|
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
|
if (self.salary_structure and
|
||||||
cint(row.depends_on_payment_days) and cint(self.total_working_days)
|
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 (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
|
getdate(self.start_date) < joining_date or
|
||||||
(relieving_date and getdate(self.end_date) > relieving_date)
|
(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)
|
amount = flt((flt(row.default_amount) * flt(self.payment_days)
|
||||||
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
|
/ 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
|
amount, additional_amount = 0, 0
|
||||||
elif not row.amount:
|
elif not row.amount:
|
||||||
amount = flt(row.default_amount) + flt(row.additional_amount)
|
amount = flt(row.default_amount) + flt(row.additional_amount)
|
||||||
|
@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
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):
|
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||||
|
@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
|
|||||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
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):
|
def test_timesheet_billing_amount(self):
|
||||||
emp = make_employee("test_employee_6@salary.com")
|
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"
|
salary_structure_name = "Timesheet Salary Structure Test"
|
||||||
frequency = "Monthly"
|
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 = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
|
||||||
salary_structure.salary_component = "Timesheet Component"
|
salary_structure.salary_component = "Timesheet Component"
|
||||||
salary_structure.salary_slip_based_on_timesheet = 1
|
salary_structure.salary_slip_based_on_timesheet = 1
|
||||||
|
@ -835,7 +835,7 @@ $(document).on('app_ready', function() {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
|
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({
|
frappe.call({
|
||||||
'method': 'frappe.client.get',
|
'method': 'frappe.client.get',
|
||||||
args: {
|
args: {
|
||||||
@ -888,8 +888,8 @@ $(document).on('app_ready', function() {
|
|||||||
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||||
frm.dashboard.clear_headline();
|
frm.dashboard.clear_headline();
|
||||||
|
|
||||||
let time_to_respond = get_status(frm.doc.response_by_variance);
|
let time_to_respond = get_status(frm.doc.response_by);
|
||||||
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
|
if (!frm.doc.first_responded_on) {
|
||||||
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
|
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) {
|
if (apply_sla_for_resolution) {
|
||||||
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
|
let time_to_resolve = get_status(frm.doc.resolution_by);
|
||||||
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
|
if (!frm.doc.resolution_date) {
|
||||||
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
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};
|
return {'diff_display': diff_display, 'indicator': indicator};
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_status(variance) {
|
function get_status(timestamp) {
|
||||||
if (variance > 0) {
|
const time_left = moment(timestamp).diff(moment());
|
||||||
|
if (time_left >= 0) {
|
||||||
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
|
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
|
||||||
} else {
|
} else {
|
||||||
return {'diff_display': 'Failed', 'indicator': 'red'};
|
return {'diff_display': 'Failed', 'indicator': 'red'};
|
||||||
|
@ -114,9 +114,11 @@ def get_items(filters):
|
|||||||
|
|
||||||
items = frappe.db.sql("""
|
items = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
|
`tabSales Invoice Item`.gst_hsn_code,
|
||||||
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
|
`tabSales Invoice Item`.stock_uom,
|
||||||
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
|
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,
|
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
||||||
`tabGST HSN Code`.description
|
`tabGST HSN Code`.description
|
||||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
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`.docstatus = 1
|
||||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
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)
|
""" % (conditions, match_conditions), filters, as_dict=1)
|
||||||
|
|
||||||
|
@ -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)
|
@ -2,8 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
@ -11,7 +9,7 @@ from frappe.utils import flt
|
|||||||
from erpnext.accounts.party import get_due_date
|
from erpnext.accounts.party import get_due_date
|
||||||
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
||||||
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
|
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_ignore = ["Price List"]
|
||||||
test_dependencies = ['Payment Term', 'Payment Terms Template']
|
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):
|
def setUp(self):
|
||||||
if not frappe.get_value('Item', '_Test Item'):
|
if not frappe.get_value('Item', '_Test Item'):
|
||||||
make_test_records('Item')
|
make_test_records('Item')
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.controllers.queries import item_query
|
from erpnext.controllers.queries import item_query
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ['Item', 'Customer', 'Supplier']
|
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.based_on_value = args.get('based_on_value')
|
||||||
psi.insert()
|
psi.insert()
|
||||||
|
|
||||||
class TestPartySpecificItem(unittest.TestCase):
|
class TestPartySpecificItem(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.customer = frappe.get_last_doc("Customer")
|
self.customer = frappe.get_last_doc("Customer")
|
||||||
self.supplier = frappe.get_last_doc("Supplier")
|
self.supplier = frappe.get_last_doc("Supplier")
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ["Product Bundle"]
|
test_dependencies = ["Product Bundle"]
|
||||||
|
|
||||||
|
|
||||||
class TestQuotation(unittest.TestCase):
|
class TestQuotation(ERPNextTestCase):
|
||||||
def test_make_quotation_without_terms(self):
|
def test_make_quotation_without_terms(self):
|
||||||
quotation = make_quotation(do_not_save=1)
|
quotation = make_quotation(do_not_save=1)
|
||||||
self.assertFalse(quotation.get('payment_schedule'))
|
self.assertFalse(quotation.get('payment_schedule'))
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.permissions
|
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.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
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
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
||||||
"unlink_advance_payment_on_cancelation_of_order"))
|
"unlink_advance_payment_on_cancelation_of_order"))
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
# reset config to previous state
|
# reset config to previous state
|
||||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||||
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
|
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from frappe.utils import add_months, nowdate
|
from frappe.utils import add_months, nowdate
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
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 (
|
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
|
||||||
execute,
|
execute,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
|
class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
|
||||||
def test_result_for_partial_material_request(self):
|
def test_result_for_partial_material_request(self):
|
||||||
so = make_sales_order()
|
so = make_sales_order()
|
||||||
mr=make_material_request(so.name)
|
mr=make_material_request(so.name)
|
||||||
|
@ -2,15 +2,14 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
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.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):
|
def test_sales_analytics(self):
|
||||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||||
|
|
||||||
|
@ -68,6 +68,8 @@ def set_default_settings(args):
|
|||||||
|
|
||||||
hr_settings.send_interview_feedback_reminder = 1
|
hr_settings.send_interview_feedback_reminder = 1
|
||||||
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
|
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
|
||||||
|
|
||||||
|
hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
|
||||||
def set_no_copy_fields_in_variant_settings():
|
def set_no_copy_fields_in_variant_settings():
|
||||||
|
@ -278,6 +278,11 @@ def install(country=None):
|
|||||||
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
|
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
|
||||||
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
|
'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")
|
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
|
||||||
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
|
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
|
||||||
|
|
||||||
|
@ -43,9 +43,9 @@ class Bin(Document):
|
|||||||
frappe.qb
|
frappe.qb
|
||||||
.from_(wo)
|
.from_(wo)
|
||||||
.from_(wo_item)
|
.from_(wo_item)
|
||||||
.select(Case()
|
.select(Sum(Case()
|
||||||
.when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty))
|
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
|
||||||
.else_(Sum(wo_item.required_qty - wo_item.consumed_qty))
|
.else_(wo_item.required_qty - wo_item.consumed_qty))
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(wo_item.item_code == self.item_code)
|
(wo_item.item_code == self.item_code)
|
||||||
|
@ -361,8 +361,7 @@
|
|||||||
"fieldname": "valuation_method",
|
"fieldname": "valuation_method",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Valuation Method",
|
"label": "Valuation Method",
|
||||||
"options": "\nFIFO\nMoving Average",
|
"options": "\nFIFO\nMoving Average"
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "is_stock_item",
|
"depends_on": "is_stock_item",
|
||||||
@ -1035,7 +1034,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-12-03 08:32:03.869294",
|
"modified": "2021-12-14 04:13:16.857534",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
@ -1,451 +1,140 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
"autoname": "hash",
|
||||||
"allow_rename": 0,
|
"creation": "2013-04-08 13:10:16",
|
||||||
"autoname": "hash",
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"document_type": "Document",
|
||||||
"creation": "2013-04-08 13:10:16",
|
"editable_grid": 1,
|
||||||
"custom": 0,
|
"engine": "InnoDB",
|
||||||
"docstatus": 0,
|
"field_order": [
|
||||||
"doctype": "DocType",
|
"item_code",
|
||||||
"document_type": "Document",
|
"column_break_2",
|
||||||
"editable_grid": 1,
|
"item_name",
|
||||||
"engine": "InnoDB",
|
"batch_no",
|
||||||
|
"desc_section",
|
||||||
|
"description",
|
||||||
|
"quantity_section",
|
||||||
|
"qty",
|
||||||
|
"net_weight",
|
||||||
|
"column_break_10",
|
||||||
|
"stock_uom",
|
||||||
|
"weight_uom",
|
||||||
|
"page_break",
|
||||||
|
"dn_detail"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "item_code",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"in_global_search": 1,
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "item_code",
|
"label": "Item Code",
|
||||||
"fieldtype": "Link",
|
"options": "Item",
|
||||||
"hidden": 0,
|
"print_width": "100px",
|
||||||
"ignore_user_permissions": 0,
|
"reqd": 1,
|
||||||
"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,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "column_break_2",
|
||||||
"bold": 0,
|
"fieldtype": "Column Break"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fetch_from": "item_code.item_name",
|
||||||
"bold": 0,
|
"fieldname": "item_name",
|
||||||
"collapsible": 0,
|
"fieldtype": "Data",
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "item_name",
|
"label": "Item Name",
|
||||||
"fieldtype": "Data",
|
"print_width": "200px",
|
||||||
"hidden": 0,
|
"read_only": 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": "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,
|
|
||||||
"width": "200px"
|
"width": "200px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "batch_no",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "Batch No",
|
||||||
"columns": 0,
|
"options": "Batch"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"collapsible": 1,
|
||||||
"bold": 0,
|
"fieldname": "desc_section",
|
||||||
"collapsible": 1,
|
"fieldtype": "Section Break",
|
||||||
"columns": 0,
|
"label": "Description"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "description",
|
||||||
"bold": 0,
|
"fieldtype": "Text Editor",
|
||||||
"collapsible": 0,
|
"label": "Description"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "quantity_section",
|
||||||
"bold": 0,
|
"fieldtype": "Section Break",
|
||||||
"collapsible": 0,
|
"label": "Quantity"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "qty",
|
||||||
"bold": 0,
|
"fieldtype": "Float",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Quantity",
|
||||||
"fieldname": "qty",
|
"print_width": "100px",
|
||||||
"fieldtype": "Float",
|
"reqd": 1,
|
||||||
"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,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "net_weight",
|
||||||
"bold": 0,
|
"fieldtype": "Float",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Net Weight",
|
||||||
"fieldname": "net_weight",
|
"print_width": "100px",
|
||||||
"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,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "column_break_10",
|
||||||
"bold": 0,
|
"fieldtype": "Column Break"
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "stock_uom",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "UOM",
|
||||||
"columns": 0,
|
"options": "UOM",
|
||||||
"fieldname": "stock_uom",
|
"print_width": "100px",
|
||||||
"fieldtype": "Link",
|
"read_only": 1,
|
||||||
"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,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "weight_uom",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "Weight UOM",
|
||||||
"columns": 0,
|
"options": "UOM",
|
||||||
"fieldname": "weight_uom",
|
"print_width": "100px",
|
||||||
"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,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
"default": "0",
|
||||||
"collapsible": 0,
|
"fieldname": "page_break",
|
||||||
"columns": 0,
|
"fieldtype": "Check",
|
||||||
"fieldname": "page_break",
|
"in_list_view": 1,
|
||||||
"fieldtype": "Check",
|
"label": "Page Break"
|
||||||
"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": 0,
|
"fieldname": "dn_detail",
|
||||||
"bold": 0,
|
"fieldtype": "Data",
|
||||||
"collapsible": 0,
|
"hidden": 1,
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "dn_detail",
|
"label": "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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
"idx": 1,
|
||||||
"hide_toolbar": 0,
|
"istable": 1,
|
||||||
"idx": 1,
|
"links": [],
|
||||||
"image_view": 0,
|
"modified": "2021-12-14 01:22:00.715935",
|
||||||
"in_create": 0,
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
"is_submittable": 0,
|
"name": "Packing Slip Item",
|
||||||
"issingle": 0,
|
"naming_rule": "Random",
|
||||||
"istable": 1,
|
"owner": "Administrator",
|
||||||
"max_attachments": 0,
|
"permissions": [],
|
||||||
"modified": "2018-06-01 07:21:58.220980",
|
"sort_field": "modified",
|
||||||
"modified_by": "Administrator",
|
"sort_order": "DESC",
|
||||||
"module": "Stock",
|
"track_changes": 1
|
||||||
"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
|
|
||||||
}
|
}
|
@ -48,6 +48,7 @@ def get_item_info(filters):
|
|||||||
conditions = [get_item_group_condition(filters.get("item_group"))]
|
conditions = [get_item_group_condition(filters.get("item_group"))]
|
||||||
if filters.get("brand"):
|
if filters.get("brand"):
|
||||||
conditions.append("item.brand=%(brand)s")
|
conditions.append("item.brand=%(brand)s")
|
||||||
|
conditions.append("is_stock_item = 1")
|
||||||
|
|
||||||
return frappe.db.sql("""select name, item_name, description, brand, item_group,
|
return frappe.db.sql("""select name, item_name, description, brand, item_group,
|
||||||
safety_stock, lead_time_days from `tabItem` item where {}"""
|
safety_stock, lead_time_days from `tabItem` item where {}"""
|
||||||
|
@ -24,12 +24,10 @@
|
|||||||
"service_level_section",
|
"service_level_section",
|
||||||
"service_level_agreement",
|
"service_level_agreement",
|
||||||
"response_by",
|
"response_by",
|
||||||
"response_by_variance",
|
|
||||||
"reset_service_level_agreement",
|
"reset_service_level_agreement",
|
||||||
"cb",
|
"cb",
|
||||||
"agreement_status",
|
"agreement_status",
|
||||||
"resolution_by",
|
"resolution_by",
|
||||||
"resolution_by_variance",
|
|
||||||
"service_level_agreement_creation",
|
"service_level_agreement_creation",
|
||||||
"on_hold_since",
|
"on_hold_since",
|
||||||
"total_hold_time",
|
"total_hold_time",
|
||||||
@ -123,7 +121,6 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Medium",
|
|
||||||
"fieldname": "priority",
|
"fieldname": "priority",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -318,22 +315,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Via Customer Portal"
|
"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",
|
"fieldname": "service_level_agreement_creation",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
@ -391,12 +372,12 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Ongoing",
|
"default": "First Response Due",
|
||||||
"depends_on": "eval: doc.service_level_agreement",
|
"depends_on": "eval: doc.service_level_agreement",
|
||||||
"fieldname": "agreement_status",
|
"fieldname": "agreement_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Service Level Agreement Status",
|
"label": "Service Level Agreement Status",
|
||||||
"options": "Ongoing\nFulfilled\nFailed",
|
"options": "First Response Due\nResolution Due\nFulfilled\nFailed",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -410,10 +391,11 @@
|
|||||||
"icon": "fa fa-ticket",
|
"icon": "fa fa-ticket",
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-10 03:22:27.098898",
|
"modified": "2021-11-24 13:13:10.276630",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Issue",
|
"name": "Issue",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -87,11 +87,9 @@ class Issue(Document):
|
|||||||
if replicated_issue.service_level_agreement:
|
if replicated_issue.service_level_agreement:
|
||||||
replicated_issue.service_level_agreement_creation = now_datetime()
|
replicated_issue.service_level_agreement_creation = now_datetime()
|
||||||
replicated_issue.service_level_agreement = None
|
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 = None
|
||||||
replicated_issue.response_by_variance = None
|
|
||||||
replicated_issue.resolution_by = None
|
replicated_issue.resolution_by = None
|
||||||
replicated_issue.resolution_by_variance = None
|
|
||||||
replicated_issue.reset_issue_metrics()
|
replicated_issue.reset_issue_metrics()
|
||||||
|
|
||||||
frappe.get_doc(replicated_issue).insert()
|
frappe.get_doc(replicated_issue).insert()
|
||||||
|
@ -18,7 +18,6 @@ frappe.listview_settings['Issue'] = {
|
|||||||
},
|
},
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
if (doc.status === 'Open') {
|
if (doc.status === 'Open') {
|
||||||
if (!doc.priority) doc.priority = 'Medium';
|
|
||||||
const color = {
|
const color = {
|
||||||
'Low': 'yellow',
|
'Low': 'yellow',
|
||||||
'Medium': 'orange',
|
'Medium': 'orange',
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import datetime
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
from frappe.utils import flt, get_datetime
|
from frappe.utils import flt, get_datetime
|
||||||
|
|
||||||
@ -83,30 +83,6 @@ class TestIssue(TestSetUp):
|
|||||||
|
|
||||||
self.assertEqual(issue.agreement_status, 'Fulfilled')
|
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):
|
def test_hold_time_on_replied(self):
|
||||||
creation = get_datetime("2020-03-04 4:00")
|
creation = get_datetime("2020-03-04 4:00")
|
||||||
|
|
||||||
@ -142,6 +118,142 @@ class TestIssue(TestSetUp):
|
|||||||
issue.reload()
|
issue.reload()
|
||||||
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
|
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):
|
class TestFirstResponseTime(TestSetUp):
|
||||||
# working hours used in all cases: Mon-Fri, 10am to 6pm
|
# working hours used in all cases: Mon-Fri, 10am to 6pm
|
||||||
# all dates are in the mm-dd-yyyy format
|
# 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):
|
def create_issue_and_communication(issue_creation, first_responded_on):
|
||||||
issue = make_issue(issue_creation, index=1)
|
issue = make_issue(issue_creation, index=1)
|
||||||
sender = create_user("test@admin.com")
|
sender = create_user("test@admin.com")
|
||||||
|
frappe.flags.current_time = first_responded_on
|
||||||
create_communication(issue.name, sender.email, "Sent", first_responded_on)
|
create_communication(issue.name, sender.email, "Sent", first_responded_on)
|
||||||
issue.reload()
|
issue.reload()
|
||||||
|
|
||||||
return issue
|
return issue
|
||||||
|
|
||||||
def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
|
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({
|
issue = frappe.get_doc({
|
||||||
"doctype": "Issue",
|
"doctype": "Issue",
|
||||||
"subject": "Service Level Agreement Issue {0}".format(index),
|
"subject": "Service Level Agreement Issue {0}".format(index),
|
||||||
|
@ -22,10 +22,41 @@ frappe.ui.form.on('Service Level Agreement', {
|
|||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.trigger('fetch_status_fields');
|
frm.trigger('fetch_status_fields');
|
||||||
frm.trigger('toggle_resolution_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) {
|
document_type: function(frm) {
|
||||||
frm.trigger('fetch_status_fields');
|
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) {
|
fetch_status_fields: function(frm) {
|
||||||
|
@ -6,22 +6,17 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"enabled",
|
|
||||||
"section_break_2",
|
|
||||||
"document_type",
|
"document_type",
|
||||||
"default_service_level_agreement",
|
|
||||||
"default_priority",
|
"default_priority",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"service_level",
|
"service_level",
|
||||||
"holiday_list",
|
"enabled",
|
||||||
"entity_section",
|
|
||||||
"entity_type",
|
|
||||||
"column_break_10",
|
|
||||||
"entity",
|
|
||||||
"filters_section",
|
"filters_section",
|
||||||
"condition",
|
"default_service_level_agreement",
|
||||||
|
"entity_type",
|
||||||
|
"entity",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"condition_description",
|
"condition",
|
||||||
"agreement_details_section",
|
"agreement_details_section",
|
||||||
"start_date",
|
"start_date",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
@ -31,8 +26,10 @@
|
|||||||
"priorities",
|
"priorities",
|
||||||
"status_details",
|
"status_details",
|
||||||
"sla_fulfilled_on",
|
"sla_fulfilled_on",
|
||||||
|
"column_break_22",
|
||||||
"pause_sla_on",
|
"pause_sla_on",
|
||||||
"support_and_resolution_section_break",
|
"support_and_resolution_section_break",
|
||||||
|
"holiday_list",
|
||||||
"support_and_resolution"
|
"support_and_resolution"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -42,7 +39,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Service Level Name",
|
"label": "Service Level Name",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"set_only_once": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "holiday_list",
|
"fieldname": "holiday_list",
|
||||||
@ -56,10 +54,10 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: !doc.default_service_level_agreement",
|
"depends_on": "eval: doc.document_type",
|
||||||
"fieldname": "agreement_details_section",
|
"fieldname": "agreement_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Agreement Details"
|
"label": "Valid From"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "start_date",
|
"fieldname": "start_date",
|
||||||
@ -72,7 +70,6 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: !doc.default_service_level_agreement",
|
|
||||||
"fieldname": "end_date",
|
"fieldname": "end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "End Date"
|
"label": "End Date"
|
||||||
@ -80,7 +77,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "response_and_resolution_time_section",
|
"fieldname": "response_and_resolution_time_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Response and Resolution Time"
|
"label": "Response and Resolution"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "support_and_resolution_section_break",
|
"fieldname": "support_and_resolution_section_break",
|
||||||
@ -90,6 +87,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "support_and_resolution",
|
"fieldname": "support_and_resolution",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
|
"label": "Working Hours",
|
||||||
"options": "Service Day",
|
"options": "Service Day",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
@ -101,10 +99,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_10",
|
"depends_on": "eval: !doc.default_service_level_agreement",
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "entity",
|
"fieldname": "entity",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Dynamic Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -114,22 +109,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: !doc.default_service_level_agreement",
|
"depends_on": "eval: !doc.default_service_level_agreement",
|
||||||
"fieldname": "entity_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Entity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "entity_type",
|
"fieldname": "entity_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Entity Type",
|
"label": "Entity Type",
|
||||||
"options": "\nCustomer\nCustomer Group\nTerritory"
|
"options": "\nCustomer\nCustomer Group\nTerritory"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_2",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_border": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "default_service_level_agreement",
|
"fieldname": "default_service_level_agreement",
|
||||||
@ -152,7 +137,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "document_type",
|
"fieldname": "document_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Document Type",
|
"label": "Apply On",
|
||||||
"options": "DocType",
|
"options": "DocType",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"set_only_once": 1
|
"set_only_once": 1
|
||||||
@ -164,6 +149,7 @@
|
|||||||
"label": "Enabled"
|
"label": "Enabled"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "document_type",
|
||||||
"fieldname": "status_details",
|
"fieldname": "status_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Status Details"
|
"label": "Status Details"
|
||||||
@ -182,28 +168,31 @@
|
|||||||
"label": "Apply SLA for Resolution Time"
|
"label": "Apply SLA for Resolution Time"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "document_type",
|
||||||
"fieldname": "filters_section",
|
"fieldname": "filters_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Assignment Condition"
|
"label": "Assignment Conditions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_15",
|
"fieldname": "column_break_15",
|
||||||
"fieldtype": "Column Break"
|
"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",
|
"fieldname": "condition",
|
||||||
"fieldtype": "Code",
|
"fieldtype": "Code",
|
||||||
"label": "Condition",
|
"label": "Condition",
|
||||||
"options": "Python"
|
"max_height": "7rem",
|
||||||
|
"options": "PythonExpression"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "condition_description",
|
"fieldname": "column_break_22",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "Column Break"
|
||||||
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-02 11:32:55.556024",
|
"modified": "2021-11-26 15:45:33.289911",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Service Level Agreement",
|
"name": "Service Level Agreement",
|
||||||
|
@ -10,7 +10,6 @@ from frappe.core.utils import get_parent_doc
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_to_date,
|
add_to_date,
|
||||||
cint,
|
|
||||||
get_datetime,
|
get_datetime,
|
||||||
get_datetime_str,
|
get_datetime_str,
|
||||||
get_link_to_form,
|
get_link_to_form,
|
||||||
@ -22,6 +21,7 @@ from frappe.utils import (
|
|||||||
time_diff_in_seconds,
|
time_diff_in_seconds,
|
||||||
to_timedelta,
|
to_timedelta,
|
||||||
)
|
)
|
||||||
|
from frappe.utils.nestedset import get_ancestors_of
|
||||||
from frappe.utils.safe_exec import get_safe_globals
|
from frappe.utils.safe_exec import get_safe_globals
|
||||||
|
|
||||||
from erpnext.support.doctype.issue.issue import get_holidays
|
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')
|
customer = doc.get('customer')
|
||||||
or_filters.append(
|
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]]
|
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"))}
|
return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
|
||||||
|
|
||||||
def get_customer_group(customer):
|
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):
|
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()
|
@frappe.whitelist()
|
||||||
@ -299,7 +311,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None):
|
|||||||
if customer:
|
if customer:
|
||||||
# Include SLA with No Entity and Entity Type
|
# Include SLA with No Entity and Entity Type
|
||||||
or_filters.append(
|
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 {
|
return {
|
||||||
@ -337,84 +349,135 @@ def set_documents_with_active_service_level_agreement():
|
|||||||
|
|
||||||
def apply(doc, method=None):
|
def apply(doc, method=None):
|
||||||
# Applies SLA to document on validate
|
# 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 \
|
if (
|
||||||
doc.doctype not in get_documents_with_active_service_level_agreement():
|
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
|
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
|
return
|
||||||
|
|
||||||
set_sla_properties(doc, service_level_agreement)
|
process_sla(doc, sla)
|
||||||
|
|
||||||
|
|
||||||
def set_sla_properties(doc, service_level_agreement):
|
def process_sla(doc, sla):
|
||||||
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)
|
|
||||||
|
|
||||||
if not doc.creation:
|
if not doc.creation:
|
||||||
doc.creation = now_datetime(doc.get("owner"))
|
doc.creation = now_datetime(doc.get("owner"))
|
||||||
|
if doc.meta.has_field("service_level_agreement_creation"):
|
||||||
if meta.has_field("service_level_agreement_creation"):
|
|
||||||
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
|
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)
|
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
|
||||||
|
set_response_by(doc, start_date_time, priority)
|
||||||
set_response_by_and_variance(doc, meta, start_date_time, priority)
|
if apply_sla_for_resolution:
|
||||||
if service_level_agreement.apply_sla_for_resolution:
|
set_resolution_by(doc, start_date_time, priority)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def get_expected_time_for(parameter, service_level, start_date_time):
|
def get_expected_time_for(parameter, service_level, start_date_time):
|
||||||
@ -485,37 +548,13 @@ def get_support_days(service_level):
|
|||||||
return support_days
|
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
|
# 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
|
return
|
||||||
|
|
||||||
communications = frappe.get_all("Communication", filters={
|
communications = frappe.get_all("Communication", filters={
|
||||||
@ -531,7 +570,7 @@ def set_user_resolution_time(doc, meta):
|
|||||||
pending_time.append(wait_time)
|
pending_time.append(wait_time)
|
||||||
|
|
||||||
total_pending_time = sum(pending_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
|
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))
|
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
|
||||||
|
|
||||||
|
|
||||||
def get_priority(doc):
|
def get_response_and_resolution_duration(doc):
|
||||||
service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
|
sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
|
||||||
priority = service_level_agreement.get_service_level_agreement_priority(doc.priority)
|
priority = sla.get_service_level_agreement_priority(doc.priority)
|
||||||
priority.update({
|
priority.update({
|
||||||
"support_and_resolution": service_level_agreement.support_and_resolution,
|
"support_and_resolution": sla.support_and_resolution,
|
||||||
"holiday_list": service_level_agreement.holiday_list
|
"holiday_list": sla.holiday_list
|
||||||
})
|
})
|
||||||
return priority
|
return priority
|
||||||
|
|
||||||
@ -572,120 +611,102 @@ def reset_service_level_agreement(doc, reason, user):
|
|||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
|
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()
|
doc.save()
|
||||||
|
|
||||||
|
|
||||||
def reset_metrics(doc, meta):
|
def reset_resolution_metrics(doc):
|
||||||
if meta.has_field("resolution_date"):
|
if doc.meta.has_field("resolution_date"):
|
||||||
doc.resolution_date = None
|
doc.resolution_date = None
|
||||||
|
|
||||||
if not meta.has_field("resolution_time"):
|
if doc.meta.has_field("resolution_time"):
|
||||||
doc.resolution_time = None
|
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
|
doc.user_resolution_time = None
|
||||||
|
|
||||||
if meta.has_field("agreement_status"):
|
if doc.meta.has_field("agreement_status"):
|
||||||
doc.agreement_status = "Ongoing"
|
doc.agreement_status = "First Response Due"
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# called via hooks on communication update
|
# 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)
|
parent = get_parent_doc(doc)
|
||||||
if not parent:
|
if not parent:
|
||||||
return
|
return
|
||||||
|
|
||||||
if doc.communication_type == "Comment":
|
if not parent.meta.has_field('service_level_agreement'):
|
||||||
return
|
return
|
||||||
|
|
||||||
status_field = parent.meta.get_field("status")
|
if (
|
||||||
if status_field:
|
doc.sent_or_received == "Received" # a reply is received
|
||||||
options = (status_field.options or "").splitlines()
|
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
|
elif (
|
||||||
if ("Replied" in options) and doc.sent_or_received == "Received":
|
doc.sent_or_received == "Sent" # a reply is sent
|
||||||
meta = frappe.get_meta(parent.doctype)
|
and parent.get('first_responded_on') # first_responded_on is set from communication.py
|
||||||
handle_hold_time(parent, meta, 'Replied')
|
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):
|
def reset_expected_response_and_resolution(doc):
|
||||||
if meta.has_field("service_level_agreement") and doc.service_level_agreement:
|
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
|
||||||
# set response and resolution variance as None as the issue is on Hold for status as Replied
|
doc.response_by = None
|
||||||
hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
|
if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'):
|
||||||
"parent": doc.service_level_agreement
|
doc.resolution_by = None
|
||||||
}, 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 apply_hold_status(doc, meta):
|
def set_response_by(doc, start_date_time, priority):
|
||||||
update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))}
|
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 meta.has_field("first_responded_on") and not doc.first_responded_on:
|
if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'):
|
||||||
update_values['response_by'] = None
|
doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time')))
|
||||||
update_values['response_by_variance'] = 0
|
|
||||||
|
|
||||||
update_values['resolution_by'] = None
|
|
||||||
update_values['resolution_by_variance'] = 0
|
|
||||||
|
|
||||||
doc.db_set(update_values)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_hold_status_and_update_hold_time(doc, meta):
|
def set_resolution_by(doc, start_date_time, priority):
|
||||||
hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0
|
if doc.meta.has_field("resolution_by"):
|
||||||
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
|
doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
|
||||||
last_hold_time = 0
|
if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'):
|
||||||
update_values = {}
|
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
|
def record_assigned_users_on_failure(doc):
|
||||||
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
|
assigned_users = doc.get_assigned_users()
|
||||||
priority = get_priority(doc)
|
if assigned_users:
|
||||||
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
|
from frappe.utils import get_fullname
|
||||||
|
assigned_users = ', '.join((get_fullname(user) for user in assigned_users))
|
||||||
# add hold time to response by variance
|
message = _('First Response SLA Failed by {}').format(assigned_users)
|
||||||
if meta.has_field("first_responded_on") and not doc.first_responded_on:
|
doc.add_comment(
|
||||||
response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
|
comment_type='Assigned',
|
||||||
response_by = add_to_date(response_by, seconds=round(last_hold_time))
|
text=message
|
||||||
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 get_service_level_agreement_fields():
|
def get_service_level_agreement_fields():
|
||||||
@ -714,17 +735,11 @@ def get_service_level_agreement_fields():
|
|||||||
"label": "Response By",
|
"label": "Response By",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "response_by_variance",
|
|
||||||
"fieldtype": "Duration",
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Response By Variance",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "first_responded_on",
|
"fieldname": "first_responded_on",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "First Responded On",
|
"label": "First Responded On",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -746,11 +761,11 @@ def get_service_level_agreement_fields():
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Ongoing",
|
"default": "First Response Due",
|
||||||
"fieldname": "agreement_status",
|
"fieldname": "agreement_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Service Level Agreement Status",
|
"label": "Service Level Agreement Status",
|
||||||
"options": "Ongoing\nFulfilled\nFailed",
|
"options": "First Response Due\nResolution Due\nFulfilled\nFailed",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -759,13 +774,6 @@ def get_service_level_agreement_fields():
|
|||||||
"label": "Resolution By",
|
"label": "Resolution By",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "resolution_by_variance",
|
|
||||||
"fieldtype": "Duration",
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Resolution By Variance",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "service_level_agreement_creation",
|
"fieldname": "service_level_agreement_creation",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
@ -786,43 +794,28 @@ def get_service_level_agreement_fields():
|
|||||||
|
|
||||||
def update_agreement_status_on_custom_status(doc):
|
def update_agreement_status_on_custom_status(doc):
|
||||||
# Update Agreement Fulfilled status using Custom Scripts for Custom Status
|
# Update Agreement Fulfilled status using Custom Scripts for Custom Status
|
||||||
|
update_agreement_status(doc)
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
def update_agreement_status(doc, meta):
|
def update_agreement_status(doc, apply_sla_for_resolution):
|
||||||
if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \
|
if (doc.meta.has_field("agreement_status")):
|
||||||
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")
|
|
||||||
|
|
||||||
# if SLA is applied for resolution check for response and resolution, else only response
|
# if SLA is applied for resolution check for response and resolution, else only response
|
||||||
if apply_sla_for_resolution:
|
if apply_sla_for_resolution:
|
||||||
if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"):
|
if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
|
||||||
if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \
|
doc.agreement_status = "First Response Due"
|
||||||
cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0:
|
elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'):
|
||||||
|
doc.agreement_status = "Resolution Due"
|
||||||
doc.agreement_status = "Failed"
|
elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')):
|
||||||
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:
|
|
||||||
doc.agreement_status = "Fulfilled"
|
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):
|
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)
|
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):
|
def now_datetime(user):
|
||||||
dt = convert_utc_to_user_timezone(datetime.utcnow(), user)
|
dt = convert_utc_to_user_timezone(datetime.utcnow(), user)
|
||||||
return dt.replace(tzinfo=None)
|
return dt.replace(tzinfo=None)
|
||||||
|
@ -220,42 +220,6 @@ class TestServiceLevelAgreement(unittest.TestCase):
|
|||||||
lead.reload()
|
lead.reload()
|
||||||
self.assertEqual(lead.agreement_status, 'Fulfilled')
|
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):
|
def test_service_level_agreement_filters(self):
|
||||||
doctype = "Lead"
|
doctype = "Lead"
|
||||||
lead_sla = create_service_level_agreement(
|
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
|
return service_level_agreement
|
||||||
|
|
||||||
def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type,
|
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_holiday_list()
|
||||||
make_priorities()
|
make_priorities()
|
||||||
@ -312,7 +277,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
|
|||||||
"doctype": "Service Level Agreement",
|
"doctype": "Service Level Agreement",
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
"document_type": doctype,
|
"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,
|
"default_service_level_agreement": default_service_level_agreement,
|
||||||
"condition": condition,
|
"condition": condition,
|
||||||
"default_priority": "Medium",
|
"default_priority": "Medium",
|
||||||
@ -345,28 +310,28 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
|
|||||||
"support_and_resolution": [
|
"support_and_resolution": [
|
||||||
{
|
{
|
||||||
"workday": "Monday",
|
"workday": "Monday",
|
||||||
"start_time": "10:00:00",
|
"start_time": start_time,
|
||||||
"end_time": "18:00:00",
|
"end_time": end_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"workday": "Tuesday",
|
"workday": "Tuesday",
|
||||||
"start_time": "10:00:00",
|
"start_time": start_time,
|
||||||
"end_time": "18:00:00",
|
"end_time": end_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"workday": "Wednesday",
|
"workday": "Wednesday",
|
||||||
"start_time": "10:00:00",
|
"start_time": start_time,
|
||||||
"end_time": "18:00:00",
|
"end_time": end_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"workday": "Thursday",
|
"workday": "Thursday",
|
||||||
"start_time": "10:00:00",
|
"start_time": start_time,
|
||||||
"end_time": "18:00:00",
|
"end_time": end_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"workday": "Friday",
|
"workday": "Friday",
|
||||||
"start_time": "10:00:00",
|
"start_time": start_time,
|
||||||
"end_time": "18:00:00",
|
"end_time": end_time,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@ -386,7 +351,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
|
|||||||
if sla:
|
if sla:
|
||||||
frappe.delete_doc("Service Level Agreement", sla, force=1)
|
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():
|
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",
|
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)
|
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():
|
def make_holiday_list():
|
||||||
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
|
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
|
||||||
if not holiday_list:
|
if not holiday_list:
|
||||||
|
@ -82,7 +82,8 @@ class IssueSummary(object):
|
|||||||
self.sla_status_map = {
|
self.sla_status_map = {
|
||||||
'SLA Failed': 'failed',
|
'SLA Failed': 'failed',
|
||||||
'SLA Fulfilled': 'fulfilled',
|
'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():
|
for label, fieldname in self.sla_status_map.items():
|
||||||
|
Loading…
Reference in New Issue
Block a user