Merge branch 'develop' into ledger-merger
This commit is contained in:
commit
e75295a151
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
|
||||
about: Suggest an idea to improve ERPNext
|
||||
title: ''
|
||||
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
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 15
|
||||
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 3
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- hotfix
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: inactive
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This pull request has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed within a week if no further activity occurs, but it
|
||||
only takes a comment to keep a contribution alive :) Also, even if it is closed,
|
||||
you can always reopen the PR when you're ready. Thank you for contributing.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
limitPerRun: 10
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
pulls:
|
||||
daysUntilStale: 15
|
||||
daysUntilClose: 3
|
||||
exemptLabels:
|
||||
- hotfix
|
||||
markComment: >
|
||||
This pull request has been automatically marked as inactive because it has
|
||||
not had recent activity. It will be closed within 3 days if no further
|
||||
activity occurs, but it only takes a comment to keep a contribution alive
|
||||
:) Also, even if it is closed, you can always reopen the PR when you're
|
||||
ready. Thank you for contributing.
|
||||
|
||||
issues:
|
||||
daysUntilStale: 60
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- valid
|
||||
- to-validate
|
||||
markComment: >
|
||||
This issue has been automatically marked as inactive because it has not had
|
||||
recent activity and it wasn't validated by maintainer team. It will be
|
||||
closed within a week if no further activity occurs.
|
||||
|
10
codecov.yml
10
codecov.yml
@ -8,6 +8,16 @@ coverage:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
|
||||
patch:
|
||||
default:
|
||||
target: 85%
|
||||
threshold: 0%
|
||||
base: auto
|
||||
branches:
|
||||
- develop
|
||||
if_ci_failed: ignore
|
||||
only_pulls: true
|
||||
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
require_changes: true
|
||||
|
@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
def on_update(self):
|
||||
@ -25,6 +27,7 @@ class AccountsSettings(Document):
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.toggle_discount_accounting_fields()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@ -56,3 +59,8 @@ class AccountsSettings(Document):
|
||||
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
|
||||
|
||||
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
|
||||
frappe.scrub(row.party_type): row.party,
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0
|
||||
"update_stock": 0,
|
||||
"invoice_number": row.invoice_number
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
@ -200,10 +201,13 @@ def start_import(invoices):
|
||||
names = []
|
||||
for idx, d in enumerate(invoices):
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
invoice_number = d.invoice_number
|
||||
publish(idx, len(invoices), d.doctype)
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
frappe.db.commit()
|
||||
names.append(doc.name)
|
||||
|
@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
||||
party_1=party_1, party_2=party_2)
|
||||
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
|
||||
doc.update(args)
|
||||
return doc.make_invoices()
|
||||
|
||||
@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
# teardown
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
|
||||
|
||||
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
|
||||
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
|
||||
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||
|
||||
#teardown
|
||||
for inv in [sales_inv1, sales_inv2]:
|
||||
doc = frappe.get_doc('Sales Invoice', inv)
|
||||
doc.cancel()
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company)
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": args.get("invoice_number")
|
||||
},
|
||||
{
|
||||
"qty": 2.0,
|
||||
@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company)
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": None
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2017-08-29 04:26:36.159247",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invoice_number",
|
||||
"party_type",
|
||||
"party",
|
||||
"temporary_opening_account",
|
||||
@ -103,10 +105,18 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Reference number of the invoice from the previous system",
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Number"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-07-25 15:00:00.460695",
|
||||
"links": [],
|
||||
"modified": "2021-12-17 19:25:06.053187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
|
@ -297,8 +297,15 @@ class PurchaseInvoice(BuyingController):
|
||||
item.expense_account = stock_not_billed_account
|
||||
|
||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
||||
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
|
||||
asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
|
||||
company = self.company)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form('Asset Category', asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account")
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif not item.expense_account and for_validate:
|
||||
|
@ -23,6 +23,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
|
||||
class Subscription(Document):
|
||||
@ -355,6 +356,9 @@ class Subscription(Document):
|
||||
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
### Add party currency to invoice
|
||||
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
|
||||
|
||||
## Add dimensions in invoice for subscription:
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
|
@ -60,15 +60,38 @@ def create_plan():
|
||||
plan.billing_interval_count = 3
|
||||
plan.insert()
|
||||
|
||||
if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
|
||||
plan = frappe.new_doc('Subscription Plan')
|
||||
plan.plan_name = '_Test Plan Multicurrency'
|
||||
plan.item = '_Test Non Stock Item'
|
||||
plan.price_determination = "Fixed Rate"
|
||||
plan.cost = 50
|
||||
plan.currency = 'USD'
|
||||
plan.billing_interval = 'Month'
|
||||
plan.billing_interval_count = 1
|
||||
plan.insert()
|
||||
|
||||
def create_parties():
|
||||
if not frappe.db.exists('Supplier', '_Test Supplier'):
|
||||
supplier = frappe.new_doc('Supplier')
|
||||
supplier.supplier_name = '_Test Supplier'
|
||||
supplier.supplier_group = 'All Supplier Groups'
|
||||
supplier.insert()
|
||||
|
||||
if not frappe.db.exists('Customer', '_Test Subscription Customer'):
|
||||
customer = frappe.new_doc('Customer')
|
||||
customer.customer_name = '_Test Subscription Customer'
|
||||
customer.billing_currency = 'USD'
|
||||
customer.append('accounts', {
|
||||
'company': '_Test Company',
|
||||
'account': '_Test Receivable USD - _TC'
|
||||
})
|
||||
customer.insert()
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
create_parties()
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multicurrency_subscription(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.party_type = 'Customer'
|
||||
subscription.party = '_Test Subscription Customer'
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.company = '_Test Company'
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
|
||||
subscription.save()
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
|
||||
self.assertEqual(currency, 'USD')
|
@ -75,7 +75,8 @@
|
||||
"fieldname": "cost",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost"
|
||||
"label": "Cost",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
|
||||
@ -147,7 +148,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-08-13 10:53:44.205774",
|
||||
"modified": "2021-12-10 15:24:15.794477",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan",
|
||||
|
@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def set_ageing(self, row):
|
||||
if self.filters.ageing_based_on == "Due Date":
|
||||
entry_date = row.due_date
|
||||
# use posting date as a fallback for advances posted via journal and payment entry
|
||||
# when ageing viewed by due date
|
||||
entry_date = row.due_date or row.posting_date
|
||||
elif self.filters.ageing_based_on == "Supplier Invoice Date":
|
||||
entry_date = row.bill_date
|
||||
else:
|
||||
|
@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Filter Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"reqd": 1,
|
||||
on_change: function() {
|
||||
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "periodicity",
|
||||
"label": __("Periodicity"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Monthly", "label": __("Monthly") },
|
||||
{ "value": "Quarterly", "label": __("Quarterly") },
|
||||
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
|
||||
{ "value": "Yearly", "label": __("Yearly") }
|
||||
],
|
||||
"default": "Monthly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"label": __("Invoice Type"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Revenue", "label": __("Revenue") },
|
||||
{ "value": "Expense", "label": __("Expense") }
|
||||
],
|
||||
"default": "Revenue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname" : "with_upcoming_postings",
|
||||
"label": __("Show with upcoming revenue/expense"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
}
|
||||
]
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Deferred Revenue and Expense"] = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter){
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
onload: function(report){
|
||||
let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-12-10 19:27:14.654220",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-12-10 19:27:14.654220",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Deferred Revenue and Expense",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Deferred Revenue and Expense",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,440 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Column, functions
|
||||
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
|
||||
|
||||
class Deferred_Item(object):
|
||||
"""
|
||||
Helper class for processing items with deferred revenue/expense
|
||||
"""
|
||||
|
||||
def __init__(self, item, inv, gle_entries):
|
||||
self.name = item
|
||||
self.parent = inv.name
|
||||
self.item_name = gle_entries[0].item_name
|
||||
self.service_start_date = gle_entries[0].service_start_date
|
||||
self.service_end_date = gle_entries[0].service_end_date
|
||||
self.base_net_amount = gle_entries[0].base_net_amount
|
||||
self.filters = inv.filters
|
||||
self.period_list = inv.period_list
|
||||
|
||||
if gle_entries[0].deferred_revenue_account:
|
||||
self.type = "Deferred Sale Item"
|
||||
self.deferred_account = gle_entries[0].deferred_revenue_account
|
||||
elif gle_entries[0].deferred_expense_account:
|
||||
self.type = "Deferred Purchase Item"
|
||||
self.deferred_account = gle_entries[0].deferred_expense_account
|
||||
|
||||
self.gle_entries = []
|
||||
# holds period wise total for item
|
||||
self.period_total = []
|
||||
self.last_entry_date = self.service_start_date
|
||||
|
||||
if gle_entries:
|
||||
self.gle_entries = gle_entries
|
||||
for x in self.gle_entries:
|
||||
if self.get_amount(x):
|
||||
self.last_entry_date = x.gle_posting_date
|
||||
|
||||
def report_data(self):
|
||||
"""
|
||||
Generate report data for output
|
||||
"""
|
||||
ret_data = frappe._dict({"name": self.item_name})
|
||||
for period in self.period_total:
|
||||
ret_data[period.key] = period.total
|
||||
ret_data.indent = 1
|
||||
return ret_data
|
||||
|
||||
def get_amount(self, entry):
|
||||
"""
|
||||
For a given GL/Journal posting, get balance based on item type
|
||||
"""
|
||||
if self.type == "Deferred Sale Item":
|
||||
return entry.debit - entry.credit
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
return -(entry.credit - entry.debit)
|
||||
return 0
|
||||
|
||||
def get_item_total(self):
|
||||
"""
|
||||
Helper method - calculate booked amount. Includes simulated postings as well
|
||||
"""
|
||||
total = 0
|
||||
for gle_posting in self.gle_entries:
|
||||
total += self.get_amount(gle_posting)
|
||||
|
||||
return total
|
||||
|
||||
def calculate_amount(self, start_date, end_date):
|
||||
"""
|
||||
start_date, end_date - datetime.datetime.date
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
+ 1
|
||||
)
|
||||
|
||||
prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
|
||||
get_last_day(self.service_end_date), get_first_day(self.service_start_date)
|
||||
)
|
||||
|
||||
actual_months = rounded(total_months * prorate, 1)
|
||||
|
||||
already_booked_amount = self.get_item_total()
|
||||
base_amount = self.base_net_amount / actual_months
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
|
||||
partial_month = flt(date_diff(end_date, start_date)) / flt(
|
||||
date_diff(get_last_day(end_date), get_first_day(start_date))
|
||||
)
|
||||
base_amount *= rounded(partial_month, 1)
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
"""
|
||||
entry = frappe._dict(
|
||||
{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
|
||||
)
|
||||
if self.type == "Deferred Sale Item":
|
||||
entry.debit = amount
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
entry.credit = amount
|
||||
return entry
|
||||
|
||||
def simulate_future_posting(self):
|
||||
"""
|
||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||
"""
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
calculate item postings for each period and update period_total list
|
||||
"""
|
||||
for period in self.period_list:
|
||||
period_sum = 0
|
||||
actual = 0
|
||||
for posting in self.gle_entries:
|
||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||
period_sum += self.get_amount(posting)
|
||||
if posting.posted == "posted":
|
||||
actual += self.get_amount(posting)
|
||||
|
||||
self.period_total.append(
|
||||
frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
|
||||
)
|
||||
return self.period_total
|
||||
|
||||
|
||||
class Deferred_Invoice(object):
|
||||
def __init__(self, invoice, items, filters, period_list):
|
||||
"""
|
||||
Helper class for processing invoices with deferred revenue/expense items
|
||||
invoice - string : invoice name
|
||||
items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
|
||||
"""
|
||||
self.name = invoice
|
||||
self.posting_date = items[0].posting_date
|
||||
self.filters = filters
|
||||
self.period_list = period_list
|
||||
# holds period wise total for invoice
|
||||
self.period_total = []
|
||||
|
||||
if items[0].deferred_revenue_account:
|
||||
self.type = "Sales"
|
||||
elif items[0].deferred_expense_account:
|
||||
self.type = "Purchase"
|
||||
|
||||
self.items = []
|
||||
# for each uniq items
|
||||
self.uniq_items = set([x.item for x in items])
|
||||
for item in self.uniq_items:
|
||||
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||
|
||||
def calculate_invoice_revenue_expense_for_period(self):
|
||||
"""
|
||||
calculate deferred revenue/expense for all items in invoice
|
||||
"""
|
||||
# initialize period_total list for invoice
|
||||
for period in self.period_list:
|
||||
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||
|
||||
for item in self.items:
|
||||
item_total = item.calculate_item_revenue_expense_for_period()
|
||||
# update invoice total
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
self.period_total[idx].total += item_total[idx].total
|
||||
self.period_total[idx].actual += item_total[idx].actual
|
||||
return self.period_total
|
||||
|
||||
def estimate_future(self):
|
||||
"""
|
||||
create dummy GL entries for upcoming months for all items in invoice
|
||||
"""
|
||||
[item.simulate_future_posting() for item in self.items]
|
||||
|
||||
def report_data(self):
|
||||
"""
|
||||
generate report data for invoice, includes invoice total
|
||||
"""
|
||||
ret_data = []
|
||||
inv_total = frappe._dict({"name": self.name})
|
||||
for x in self.period_total:
|
||||
inv_total[x.key] = x.total
|
||||
inv_total.indent = 0
|
||||
ret_data.append(inv_total)
|
||||
list(map(lambda item: ret_data.append(item.report_data()), self.items))
|
||||
return ret_data
|
||||
|
||||
|
||||
class Deferred_Revenue_and_Expense_Report(object):
|
||||
def __init__(self, filters=None):
|
||||
"""
|
||||
Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
|
||||
"""
|
||||
|
||||
# If no filters are provided, get user defaults
|
||||
if not filters:
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Fiscal Year",
|
||||
"period_start_date": fiscal_year.year_start_date,
|
||||
"period_end_date": fiscal_year.year_end_date,
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Revenue",
|
||||
"with_upcoming_postings": True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
self.period_total = []
|
||||
|
||||
def get_period_list(self):
|
||||
"""
|
||||
Figure out selected period based on filters
|
||||
"""
|
||||
self.period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
self.filters.period_start_date,
|
||||
self.filters.period_end_date,
|
||||
self.filters.filter_based_on,
|
||||
self.filters.periodicity,
|
||||
company=self.filters.company,
|
||||
)
|
||||
|
||||
def get_invoices(self):
|
||||
"""
|
||||
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||
"""
|
||||
gle = qb.DocType("GL Entry")
|
||||
# column doesn't have an alias option
|
||||
posted = Column("posted")
|
||||
|
||||
if self.filters.type == "Revenue":
|
||||
inv = qb.DocType("Sales Invoice")
|
||||
inv_item = qb.DocType("Sales Invoice Item")
|
||||
deferred_flag_field = inv_item["enable_deferred_revenue"]
|
||||
deferred_account_field = inv_item["deferred_revenue_account"]
|
||||
|
||||
elif self.filters.type == "Expense":
|
||||
inv = qb.DocType("Purchase Invoice")
|
||||
inv_item = qb.DocType("Purchase Invoice Item")
|
||||
deferred_flag_field = inv_item["enable_deferred_expense"]
|
||||
deferred_account_field = inv_item["deferred_expense_account"]
|
||||
|
||||
query = (
|
||||
qb.from_(inv_item)
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
inv_item.name.as_("item"),
|
||||
inv_item.item_name,
|
||||
inv_item.service_start_date,
|
||||
inv_item.service_end_date,
|
||||
inv_item.base_net_amount,
|
||||
deferred_account_field,
|
||||
gle.posting_date.as_("gle_posting_date"),
|
||||
functions.Sum(gle.debit).as_("debit"),
|
||||
functions.Sum(gle.credit).as_("credit"),
|
||||
posted,
|
||||
)
|
||||
.where(
|
||||
(inv.docstatus == 1)
|
||||
& (deferred_flag_field == 1)
|
||||
& (
|
||||
(
|
||||
(self.period_list[0].from_date >= inv_item.service_start_date)
|
||||
& (inv_item.service_end_date >= self.period_list[0].from_date)
|
||||
)
|
||||
| (
|
||||
(inv_item.service_start_date >= self.period_list[0].from_date)
|
||||
& (inv_item.service_start_date <= self.period_list[-1].to_date)
|
||||
)
|
||||
)
|
||||
)
|
||||
.groupby(inv.name, inv_item.name, gle.posting_date)
|
||||
.orderby(gle.posting_date)
|
||||
)
|
||||
self.invoices = query.run(as_dict=True)
|
||||
|
||||
uniq_invoice = set([x.doc for x in self.invoices])
|
||||
for inv in uniq_invoice:
|
||||
self.deferred_invoices.append(
|
||||
Deferred_Invoice(
|
||||
inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
|
||||
)
|
||||
)
|
||||
|
||||
def estimate_future(self):
|
||||
"""
|
||||
For all Invoices estimate upcoming postings
|
||||
"""
|
||||
for x in self.deferred_invoices:
|
||||
x.estimate_future()
|
||||
|
||||
def calculate_revenue_and_expense(self):
|
||||
"""
|
||||
calculate the deferred revenue/expense for all invoices
|
||||
"""
|
||||
# initialize period_total list for report
|
||||
for period in self.period_list:
|
||||
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||
|
||||
for inv in self.deferred_invoices:
|
||||
inv_total = inv.calculate_invoice_revenue_expense_for_period()
|
||||
# calculate total for whole report
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
self.period_total[idx].total += inv_total[idx].total
|
||||
self.period_total[idx].actual += inv_total[idx].actual
|
||||
|
||||
def get_columns(self):
|
||||
columns = []
|
||||
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||
for period in self.period_list:
|
||||
columns.append(
|
||||
{
|
||||
"label": _(period.label),
|
||||
"fieldname": period.key,
|
||||
"fieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
})
|
||||
return columns
|
||||
|
||||
def generate_report_data(self):
|
||||
"""
|
||||
Generate report data for all invoices. Adds total rows for revenue and expense
|
||||
"""
|
||||
ret = []
|
||||
|
||||
for inv in self.deferred_invoices:
|
||||
ret += inv.report_data()
|
||||
|
||||
# empty row for padding
|
||||
ret += [{}]
|
||||
|
||||
# add total row
|
||||
if ret is not []:
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
return ret
|
||||
|
||||
def prepare_chart(self):
|
||||
chart = {
|
||||
"data": {
|
||||
"labels": [period.label for period in self.period_list],
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Actual Posting",
|
||||
"chartType": "bar",
|
||||
"values": [x.actual for x in self.period_total],
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "axis-mixed",
|
||||
"height": 500,
|
||||
"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
|
||||
"barOptions": {"stacked": False, "spaceRatio": 0.5},
|
||||
}
|
||||
|
||||
if self.filters.with_upcoming_postings:
|
||||
chart["data"]["datasets"].append({
|
||||
"name": "Expected",
|
||||
"chartType": "line",
|
||||
"values": [x.total for x in self.period_total]
|
||||
})
|
||||
|
||||
return chart
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""
|
||||
Run report and generate data
|
||||
"""
|
||||
self.deferred_invoices.clear()
|
||||
self.get_period_list()
|
||||
self.get_invoices()
|
||||
|
||||
if self.filters.with_upcoming_postings:
|
||||
self.estimate_future()
|
||||
self.calculate_revenue_and_expense()
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=filters)
|
||||
report.run()
|
||||
|
||||
columns = report.get_columns()
|
||||
data = report.generate_report_data()
|
||||
message = []
|
||||
chart = report.prepare_chart()
|
||||
|
||||
return columns, data, message, chart
|
@ -0,0 +1,253 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
|
||||
Deferred_Revenue_and_Expense_Report,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
clear_old_entries()
|
||||
create_company()
|
||||
|
||||
def test_deferred_revenue(self):
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Customer DR"
|
||||
customer.type = "Individual"
|
||||
customer.insert()
|
||||
|
||||
item = create_item(
|
||||
"_Test Internet Subscription",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_revenue_account
|
||||
item.no_of_months = 3
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
customer="_Test Customer DR",
|
||||
debit_to="Debtors - _CD",
|
||||
posting_date="2021-05-01",
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
do_not_submit=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2021-05-01"
|
||||
si.items[0].service_end_date = "2021-08-01"
|
||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Income",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Revenue",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
expected = [
|
||||
{"key": "may_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "jun_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "jul_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
def test_deferred_expense(self):
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_expense_account = create_account(
|
||||
account_name="Deferred Expense",
|
||||
parent_account="Current Assets - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
|
||||
)
|
||||
supplier.save()
|
||||
|
||||
item = create_item(
|
||||
"_Test Office Desk",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = deferred_expense_account
|
||||
item.no_of_months_exp = 3
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
supplier="_Test Furniture Supplier",
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
do_not_save=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
warehouse="All Warehouses - _CD",
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-05-01"
|
||||
pi.items[0].service_end_date = "2021-08-01"
|
||||
pi.items[0].deferred_expense_account = deferred_expense_account
|
||||
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Expense",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
expected = [
|
||||
{"key": "may_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "jun_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "jul_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.db.exists("Company", "_Test Company DR")
|
||||
if not company:
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = "_Test Company DR"
|
||||
company.default_currency = "INR"
|
||||
company.chart_of_accounts = "Standard"
|
||||
company.insert()
|
||||
|
||||
|
||||
def clear_old_entries():
|
||||
item = qb.DocType("Item")
|
||||
account = qb.DocType("Account")
|
||||
customer = qb.DocType("Customer")
|
||||
supplier = qb.DocType("Supplier")
|
||||
sinv = qb.DocType("Sales Invoice")
|
||||
sinv_item = qb.DocType("Sales Invoice Item")
|
||||
pinv = qb.DocType("Purchase Invoice")
|
||||
pinv_item = qb.DocType("Purchase Invoice Item")
|
||||
|
||||
qb.from_(account).delete().where(
|
||||
(account.account_name == "Deferred Revenue")
|
||||
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
|
||||
).run()
|
||||
qb.from_(item).delete().where(
|
||||
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
|
||||
).run()
|
||||
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
|
||||
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
|
||||
|
||||
# delete existing invoices with deferred items
|
||||
deferred_invoices = (
|
||||
qb.from_(sinv)
|
||||
.join(sinv_item)
|
||||
.on(sinv.name == sinv_item.parent)
|
||||
.select(sinv.name)
|
||||
.where(sinv_item.enable_deferred_revenue == 1)
|
||||
.run()
|
||||
)
|
||||
if deferred_invoices:
|
||||
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
|
||||
|
||||
deferred_invoices = (
|
||||
qb.from_(pinv)
|
||||
.join(pinv_item)
|
||||
.on(pinv.name == pinv_item.parent)
|
||||
.select(pinv.name)
|
||||
.where(pinv_item.enable_deferred_expense == 1)
|
||||
.run()
|
||||
)
|
||||
if deferred_invoices:
|
||||
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
if entry.account in tds_accounts:
|
||||
tds_deducted += (entry.credit - entry.debit)
|
||||
|
||||
total_amount_credited += (entry.credit - entry.debit)
|
||||
|
||||
if rate and tds_deducted:
|
||||
if tds_deducted:
|
||||
row = {
|
||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
||||
@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
|
||||
def get_supplier_pan_map():
|
||||
supplier_map = frappe._dict()
|
||||
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
|
||||
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
|
||||
|
||||
for d in suppliers:
|
||||
supplier_map[d.name] = d
|
||||
|
@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
if (frm.doc.status != 'Fully Depreciated') {
|
||||
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
||||
frm.trigger("create_asset_adjustment");
|
||||
frm.trigger("create_asset_value_adjustment");
|
||||
}, __("Manage"));
|
||||
}
|
||||
|
||||
@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
|
||||
});
|
||||
},
|
||||
|
||||
create_asset_adjustment: function(frm) {
|
||||
create_asset_value_adjustment: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"asset": frm.doc.name,
|
||||
"asset_category": frm.doc.asset_category,
|
||||
"company": frm.doc.company
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
|
||||
freeze: 1,
|
||||
callback: function(r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
|
@ -185,83 +185,84 @@ class Asset(AccountsController):
|
||||
if not self.available_for_use_date:
|
||||
return
|
||||
|
||||
for d in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(d)
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
start = self.clear_depreciation_schedule()
|
||||
for finance_book in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = flt(d.value_after_depreciation)
|
||||
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
|
||||
d.value_after_depreciation = value_after_depreciation
|
||||
finance_book.value_after_depreciation = value_after_depreciation
|
||||
|
||||
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
|
||||
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
|
||||
cint(self.number_of_depreciations_booked)
|
||||
|
||||
has_pro_rata = self.check_is_pro_rata(d)
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
|
||||
if has_pro_rata:
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
for n in range(start, number_of_pending_depreciations):
|
||||
|
||||
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row: continue
|
||||
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
||||
|
||||
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||
schedule_date = add_months(d.depreciation_start_date,
|
||||
n * cint(d.frequency_of_depreciation))
|
||||
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||
n * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
|
||||
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
from_date = self.get_from_date(d.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
self.available_for_use_date, d.depreciation_start_date)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
self.available_for_use_date, finance_book.depreciation_start_date)
|
||||
|
||||
# For first depr schedule date will be the start date
|
||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
|
||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
if not self.flags.increase_in_asset_life:
|
||||
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
||||
self.to_date = add_months(self.available_for_use_date,
|
||||
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
|
||||
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d,
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
|
||||
depreciation_amount, schedule_date, self.to_date)
|
||||
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
||||
depreciation_amount, d.finance_book)
|
||||
depreciation_amount, finance_book.finance_book)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
@ -272,10 +273,10 @@ class Asset(AccountsController):
|
||||
self.precision("gross_purchase_amount"))
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != d.expected_value_after_useful_life)
|
||||
or value_after_depreciation < d.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
|
||||
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@ -285,7 +286,7 @@ class Asset(AccountsController):
|
||||
# In pro rata case, for first and last depreciation, month range would be different
|
||||
month_range = months \
|
||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
||||
else d.frequency_of_depreciation
|
||||
else finance_book.frequency_of_depreciation
|
||||
|
||||
for r in range(month_range):
|
||||
if (has_pro_rata and n == 0):
|
||||
@ -311,27 +312,52 @@ class Asset(AccountsController):
|
||||
self.append("schedules", {
|
||||
"schedule_date": date,
|
||||
"depreciation_amount": amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
else:
|
||||
self.append("schedules", {
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
# used when depreciation schedule needs to be modified due to increase in asset life
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
def clear_depreciation_schedule(self):
|
||||
start = 0
|
||||
for n in range(len(self.schedules)):
|
||||
if not self.schedules[n].journal_entry:
|
||||
del self.schedules[n:]
|
||||
start = n
|
||||
break
|
||||
start = []
|
||||
num_of_depreciations_completed = 0
|
||||
depr_schedule = []
|
||||
|
||||
for schedule in self.get('schedules'):
|
||||
|
||||
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 2):
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to ensure that start will only be updated once for each FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 1):
|
||||
if schedule.journal_entry:
|
||||
num_of_depreciations_completed += 1
|
||||
depr_schedule.append(schedule)
|
||||
else:
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
|
||||
if len(start) == (len(self.finance_books) - 1):
|
||||
start.append(num_of_depreciations_completed)
|
||||
|
||||
# when the Depreciation Schedule is being created for the first time
|
||||
if start == []:
|
||||
start = [0] * len(self.finance_books)
|
||||
else:
|
||||
self.schedules = depr_schedule
|
||||
|
||||
return start
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
@ -469,7 +495,6 @@ class Asset(AccountsController):
|
||||
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation) -
|
||||
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
||||
|
||||
if (row.expected_value_after_useful_life and
|
||||
@ -731,14 +756,14 @@ def create_asset_repair(asset, asset_name):
|
||||
return asset_repair
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_adjustment(asset, asset_category, company):
|
||||
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
|
||||
asset_maintenance.update({
|
||||
def create_asset_value_adjustment(asset, asset_category, company):
|
||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||
asset_value_adjustment.update({
|
||||
"asset": asset,
|
||||
"company": company,
|
||||
"asset_category": asset_category
|
||||
})
|
||||
return asset_maintenance
|
||||
return asset_value_adjustment
|
||||
|
||||
@frappe.whitelist()
|
||||
def transfer_asset(args):
|
||||
|
@ -955,6 +955,82 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertEqual(len(asset.schedules), 1)
|
||||
|
||||
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.submit()
|
||||
|
||||
post_depreciation_entries(date="2020-04-01")
|
||||
asset.load_from_db()
|
||||
|
||||
asset.clear_depreciation_schedule()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 6)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, "1")
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, "2")
|
||||
|
||||
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.save()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 9)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, 1)
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, 2)
|
||||
|
||||
def test_depreciation_entry_cancellation(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
|
@ -13,7 +13,7 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2021-08-24 17:50:41.573281",
|
||||
"modified": "2021-12-02 11:24:37.963746",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Assets",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-08-24 12:49:37.665239",
|
||||
"modified": "2021-11-23 10:02:03.242127",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Asset Category",
|
||||
"owner": "Administrator",
|
||||
|
@ -1,21 +1,22 @@
|
||||
{
|
||||
"action": "Show Form Tour",
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let's create a new Asset item",
|
||||
"creation": "2021-08-13 14:27:07.277167",
|
||||
"description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"form_tour": "Item",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-08-16 13:59:18.362233",
|
||||
"modified": "2021-12-02 11:23:48.158504",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Asset Item",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Item",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Create an Asset Item",
|
||||
"validate_action": 1
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-08-24 17:26:57.180637",
|
||||
"modified": "2021-11-23 10:02:03.235498",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Asset Purchase",
|
||||
"owner": "Administrator",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-08-24 17:46:37.646174",
|
||||
"modified": "2021-11-23 10:02:03.229566",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Fixed Asset Accounts",
|
||||
"owner": "Administrator",
|
||||
|
@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
dialog.show()
|
||||
},
|
||||
|
||||
schedule_date(frm) {
|
||||
if(frm.doc.schedule_date){
|
||||
frm.doc.items.forEach((item) => {
|
||||
item.schedule_date = frm.doc.schedule_date;
|
||||
})
|
||||
}
|
||||
refresh_field("items");
|
||||
},
|
||||
preview: (frm) => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Preview Email'),
|
||||
@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
dialog.show();
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Request for Quotation Item", {
|
||||
items_add(frm, cdt, cdn) {
|
||||
if (frm.doc.schedule_date) {
|
||||
frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
|
||||
}
|
||||
}
|
||||
});
|
||||
frappe.ui.form.on("Request for Quotation Supplier",{
|
||||
supplier: function(frm, cdt, cdn) {
|
||||
var d = locals[cdt][cdn]
|
||||
|
@ -12,6 +12,7 @@
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"status",
|
||||
"amended_from",
|
||||
"suppliers_section",
|
||||
@ -246,16 +247,22 @@
|
||||
"fieldname": "sec_break_email_2",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Required Date"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 22:04:29.017134",
|
||||
"modified": "2021-11-24 17:47:49.909000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -17,6 +17,7 @@
|
||||
"company",
|
||||
"transaction_date",
|
||||
"valid_till",
|
||||
"quotation_number",
|
||||
"amended_from",
|
||||
"address_section",
|
||||
"supplier_address",
|
||||
@ -797,6 +798,11 @@
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Valid Till"
|
||||
},
|
||||
{
|
||||
"fieldname": "quotation_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Quotation Number"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
@ -804,10 +810,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 00:58:20.995491",
|
||||
"modified": "2021-12-11 06:43:20.924080",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -91,8 +91,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
|
||||
"modified": "2021-11-03 09:00:36.883496",
|
||||
"modified": "2021-11-03 10:00:36.883496",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
|
@ -510,8 +510,7 @@
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
|
||||
"modified": "2021-10-21 11:04:30.151379",
|
||||
"modified": "2021-10-21 12:04:30.151379",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
@ -115,8 +115,7 @@
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
|
||||
"modified": "2021-11-30 11:17:24.647979",
|
||||
"modified": "2021-11-30 12:17:24.647979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "TaxJar Settings",
|
||||
|
@ -234,7 +234,7 @@ doc_events = {
|
||||
},
|
||||
"Communication": {
|
||||
"on_update": [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
|
||||
"erpnext.support.doctype.issue.issue.set_first_response_time"
|
||||
]
|
||||
},
|
||||
@ -343,8 +343,7 @@ scheduler_events = {
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||
|
@ -21,7 +21,11 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
|
||||
},
|
||||
{
|
||||
'label': _('Exit'),
|
||||
'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
|
||||
},
|
||||
{
|
||||
'label': _('Shift'),
|
||||
|
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",
|
||||
"column_break_4",
|
||||
"send_interview_feedback_reminder",
|
||||
"feedback_reminder_notification_template"
|
||||
"feedback_reminder_notification_template",
|
||||
"employee_exit_section",
|
||||
"exit_questionnaire_web_form",
|
||||
"column_break_34",
|
||||
"exit_questionnaire_notification_template"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -226,13 +230,34 @@
|
||||
"fieldname": "check_vacancies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Vacancies On Job Offer Creation"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_exit_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Employee Exit Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "exit_questionnaire_web_form",
|
||||
"fieldtype": "Link",
|
||||
"label": "Exit Questionnaire Web Form",
|
||||
"options": "Web Form"
|
||||
},
|
||||
{
|
||||
"fieldname": "exit_questionnaire_notification_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Exit Questionnaire Notification Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_34",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-01 23:46:11.098236",
|
||||
"modified": "2021-12-05 14:48:10.884253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR Settings",
|
||||
|
@ -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"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
@ -15,14 +15,6 @@
|
||||
"idx": 0,
|
||||
"label": "HR",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@ -111,14 +103,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Lifecycle",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Job Applicant",
|
||||
"hidden": 0,
|
||||
@ -227,14 +211,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Shift Management",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@ -268,14 +244,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leaves",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@ -386,14 +354,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Attendance",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
@ -449,14 +409,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Expense Claims",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
@ -489,14 +441,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@ -530,14 +474,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Fleet Management",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@ -581,14 +517,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Recruitment",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@ -808,14 +736,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Key Reports",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Attendance",
|
||||
"hidden": 0,
|
||||
@ -933,9 +853,796 @@
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Lifecycle",
|
||||
"link_count": 7,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Job Applicant",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Onboarding",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Onboarding",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Skill Map",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Skill Map",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Promotion",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Promotion",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Transfer",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Transfer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Grievance Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Grievance Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Grievance",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Grievance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Onboarding Template",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Onboarding Template",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Exit",
|
||||
"link_count": 4,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Separation Template",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Separation Template",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Separation",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Separation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Full and Final Statement",
|
||||
"link_count": 0,
|
||||
"link_to": "Full and Final Statement",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Exit Interview",
|
||||
"link_count": 0,
|
||||
"link_to": "Exit Interview",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee",
|
||||
"link_count": 8,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employment Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Employment Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Branch",
|
||||
"link_count": 0,
|
||||
"link_to": "Branch",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Department",
|
||||
"link_count": 0,
|
||||
"link_to": "Department",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Designation",
|
||||
"link_count": 0,
|
||||
"link_to": "Designation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Grade",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Grade",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Health Insurance",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Health Insurance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Key Reports",
|
||||
"link_count": 7,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Attendance",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Monthly Attendance Sheet",
|
||||
"link_count": 0,
|
||||
"link_to": "Monthly Attendance Sheet",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Staffing Plan",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Recruitment Analytics",
|
||||
"link_count": 0,
|
||||
"link_to": "Recruitment Analytics",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Employee Analytics",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Analytics",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Employee Leave Balance",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Leave Balance",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Employee Leave Balance Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Leave Balance Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee Advance",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Employee Advance Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Advance Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Exits",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Exits",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Recruitment",
|
||||
"link_count": 11,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Job Opening",
|
||||
"link_count": 0,
|
||||
"link_to": "Job Opening",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Referral",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Referral",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Job Applicant",
|
||||
"link_count": 0,
|
||||
"link_to": "Job Applicant",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Job Offer",
|
||||
"link_count": 0,
|
||||
"link_to": "Job Offer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Staffing Plan",
|
||||
"link_count": 0,
|
||||
"link_to": "Staffing Plan",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Appointment Letter",
|
||||
"link_count": 0,
|
||||
"link_to": "Appointment Letter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Appointment Letter Template",
|
||||
"link_count": 0,
|
||||
"link_to": "Appointment Letter Template",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Interview Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Interview Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Interview Round",
|
||||
"link_count": 0,
|
||||
"link_to": "Interview Round",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Interview",
|
||||
"link_count": 0,
|
||||
"link_to": "Interview",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Interview Feedback",
|
||||
"link_count": 0,
|
||||
"link_to": "Interview Feedback",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Fleet Management",
|
||||
"link_count": 4,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Driver",
|
||||
"link_count": 0,
|
||||
"link_to": "Driver",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Vehicle",
|
||||
"link_count": 0,
|
||||
"link_to": "Vehicle",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Vehicle Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Vehicle Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Vehicle",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Vehicle Expenses",
|
||||
"link_count": 0,
|
||||
"link_to": "Vehicle Expenses",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 3,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "HR Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "HR Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Daily Work Summary Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Daily Work Summary Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Team Updates",
|
||||
"link_count": 0,
|
||||
"link_to": "team-updates",
|
||||
"link_type": "Page",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Expense Claims",
|
||||
"link_count": 3,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Expense Claim",
|
||||
"link_count": 0,
|
||||
"link_to": "Expense Claim",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Advance",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Advance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Travel Request",
|
||||
"link_count": 0,
|
||||
"link_to": "Travel Request",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Attendance",
|
||||
"link_count": 5,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Attendance Tool",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Attendance Tool",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Attendance",
|
||||
"link_count": 0,
|
||||
"link_to": "Attendance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Attendance Request",
|
||||
"link_count": 0,
|
||||
"link_to": "Attendance Request",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Upload Attendance",
|
||||
"link_count": 0,
|
||||
"link_to": "Upload Attendance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Checkin",
|
||||
"link_count": 0,
|
||||
"link_to": "Employee Checkin",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leaves",
|
||||
"link_count": 10,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Holiday List",
|
||||
"link_count": 0,
|
||||
"link_to": "Holiday List",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Period",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Period",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Leave Type",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Policy",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Policy",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Leave Policy",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Policy Assignment",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Policy Assignment",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Application",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Application",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Allocation",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Allocation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Encashment",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Encashment",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Leave Block List",
|
||||
"link_count": 0,
|
||||
"link_to": "Leave Block List",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Compensatory Leave Request",
|
||||
"link_count": 0,
|
||||
"link_to": "Compensatory Leave Request",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Shift Management",
|
||||
"link_count": 3,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Shift Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Shift Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Shift Request",
|
||||
"link_count": 0,
|
||||
"link_to": "Shift Request",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Shift Assignment",
|
||||
"link_count": 0,
|
||||
"link_to": "Shift Assignment",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-31 12:18:59.842919",
|
||||
"modified": "2021-12-05 22:05:13.004462",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR",
|
||||
|
@ -56,9 +56,14 @@ class TestMaintenanceSchedule(unittest.TestCase):
|
||||
|
||||
ms.submit()
|
||||
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
|
||||
test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
|
||||
|
||||
# Check if item is mapped in visit.
|
||||
test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
|
||||
self.assertEqual(len(test_map_visit.purposes), 1)
|
||||
self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
|
||||
|
||||
visit = frappe.new_doc('Maintenance Visit')
|
||||
visit = test
|
||||
visit = test_map_visit
|
||||
visit.maintenance_schedule = ms.name
|
||||
visit.maintenance_schedule_detail = s_id
|
||||
visit.completion_status = "Partially Completed"
|
||||
|
@ -47,7 +47,7 @@ frappe.ui.form.on('Maintenance Visit', {
|
||||
frm.set_value({ status: 'Draft' });
|
||||
}
|
||||
if (frm.doc.__islocal) {
|
||||
frm.clear_table("purposes");
|
||||
frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
|
||||
frm.set_value({ mntc_date: frappe.datetime.get_today() });
|
||||
}
|
||||
},
|
||||
|
@ -1,17 +1,15 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
from .blanket_order import make_order
|
||||
|
||||
|
||||
class TestBlanketOrder(unittest.TestCase):
|
||||
class TestBlanketOrder(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
frappe.flags.args = frappe._dict()
|
||||
|
||||
|
@ -922,7 +922,7 @@ def validate_bom_no(item, bom_no):
|
||||
rm_item_exists = True
|
||||
if bom.item.lower() == item.lower() or \
|
||||
bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
|
||||
rm_item_exists = True
|
||||
rm_item_exists = True
|
||||
if not rm_item_exists:
|
||||
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
|
||||
@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
|
||||
class TestBOM(unittest.TestCase):
|
||||
class TestBOM(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.get_value('Item', '_Test Item'):
|
||||
make_test_records('Item')
|
||||
|
@ -11,6 +11,7 @@
|
||||
"col_break1",
|
||||
"workstation",
|
||||
"time_in_mins",
|
||||
"fixed_time",
|
||||
"costing_section",
|
||||
"hour_rate",
|
||||
"base_hour_rate",
|
||||
@ -79,6 +80,14 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Operation time does not depend on quantity to produce",
|
||||
"fieldname": "fixed_time",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Fixed Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "operating_cost",
|
||||
"fieldtype": "Currency",
|
||||
@ -177,12 +186,13 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-13 16:45:01.092868",
|
||||
"modified": "2021-12-15 03:00:00.473173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
|
||||
class TestBOMUpdateTool(unittest.TestCase):
|
||||
class TestBOMUpdateTool(ERPNextTestCase):
|
||||
def test_replace_bom(self):
|
||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||
|
||||
|
@ -75,6 +75,32 @@ frappe.ui.form.on('Job Card', {
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
|
||||
frm.trigger("setup_quality_inspection");
|
||||
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_quality_inspection: function(frm) {
|
||||
let quality_inspection_field = frm.get_docfield("quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(frm) {
|
||||
return {
|
||||
"inspection_type": "In Process",
|
||||
"reference_type": "Job Card",
|
||||
"reference_name": frm.doc.name,
|
||||
"item_code": frm.doc.production_item,
|
||||
"item_name": frm.doc.item_name,
|
||||
"item_serial_no": frm.doc.serial_no,
|
||||
"batch_no": frm.doc.batch_no,
|
||||
"quality_inspection_template": frm.doc.quality_inspection_template,
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
setup_corrective_job_card: function(frm) {
|
||||
|
@ -19,6 +19,7 @@
|
||||
"serial_no",
|
||||
"column_break_12",
|
||||
"wip_warehouse",
|
||||
"quality_inspection_template",
|
||||
"quality_inspection",
|
||||
"project",
|
||||
"batch_no",
|
||||
@ -408,11 +409,18 @@
|
||||
"no_copy": 1,
|
||||
"options": "Job Card Scrap Item",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "operation.quality_inspection_template",
|
||||
"fieldname": "quality_inspection_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection Template",
|
||||
"options": "Quality Inspection Template"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-12 10:15:03.572401",
|
||||
"modified": "2021-11-24 19:17:40.879235",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
@ -1,6 +1,5 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import random_string
|
||||
@ -12,9 +11,10 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestJobCard(unittest.TestCase):
|
||||
class TestJobCard(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
make_bom_for_jc_tests()
|
||||
|
||||
@ -329,4 +329,4 @@ def make_bom_for_jc_tests():
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
bom.items[0].uom = "_Test UOM 1"
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert()
|
||||
bom.insert()
|
||||
|
@ -13,6 +13,7 @@
|
||||
"is_corrective_operation",
|
||||
"job_card_section",
|
||||
"create_job_card_based_on_batch_size",
|
||||
"quality_inspection_template",
|
||||
"column_break_6",
|
||||
"batch_size",
|
||||
"sub_operations_section",
|
||||
@ -92,15 +93,22 @@
|
||||
"fieldname": "is_corrective_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Corrective Operation"
|
||||
},
|
||||
{
|
||||
"fieldname": "quality_inspection_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection Template",
|
||||
"options": "Quality Inspection Template"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-12 15:09:23.593338",
|
||||
"modified": "2021-11-24 19:15:24.357187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Operation",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_to_date, flt, now_datetime, nowdate
|
||||
|
||||
@ -17,9 +14,10 @@ from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestProductionPlan(unittest.TestCase):
|
||||
class TestProductionPlan(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
||||
'Raw Material Item 1', 'Raw Material Item 2']:
|
||||
|
@ -1,17 +1,15 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestRouting(unittest.TestCase):
|
||||
class TestRouting(ERPNextTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.item_code = "Test Routing Item - A"
|
||||
|
@ -1,6 +1,5 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, cint, flt, now, today
|
||||
@ -29,6 +28,9 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
self.warehouse = '_Test Warehouse 2 - _TC'
|
||||
self.item = '_Test Item'
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def check_planned_qty(self):
|
||||
|
||||
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
|
||||
@ -92,7 +94,7 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
|
||||
def test_reserved_qty_for_partial_completion(self):
|
||||
item = "_Test Item"
|
||||
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
bin1_at_start = get_bin(item, warehouse)
|
||||
|
||||
@ -844,6 +846,45 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
close_work_order(wo_order, "Closed")
|
||||
self.assertEqual(wo_order.get('status'), "Closed")
|
||||
|
||||
def test_fix_time_operations(self):
|
||||
bom = frappe.get_doc({
|
||||
"doctype": "BOM",
|
||||
"item": "_Test FG Item 2",
|
||||
"is_active": 1,
|
||||
"is_default": 1,
|
||||
"quantity": 1.0,
|
||||
"with_operations": 1,
|
||||
"operations": [
|
||||
{
|
||||
"operation": "_Test Operation 1",
|
||||
"description": "_Test",
|
||||
"workstation": "_Test Workstation 1",
|
||||
"time_in_mins": 60,
|
||||
"operating_cost": 140,
|
||||
"fixed_time": 1
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"amount": 5000.0,
|
||||
"doctype": "BOM Item",
|
||||
"item_code": "_Test Item",
|
||||
"parentfield": "items",
|
||||
"qty": 1.0,
|
||||
"rate": 5000.0,
|
||||
},
|
||||
],
|
||||
})
|
||||
bom.save()
|
||||
bom.submit()
|
||||
|
||||
|
||||
wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1)
|
||||
wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1)
|
||||
|
||||
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
|
||||
|
||||
|
||||
def update_job_card(job_card):
|
||||
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||
job_card_doc.set('scrap_items', [
|
||||
|
@ -505,16 +505,19 @@ class WorkOrder(Document):
|
||||
"""Fetch operations from BOM and set in 'Work Order'"""
|
||||
|
||||
def _get_operations(bom_no, qty=1):
|
||||
return frappe.db.sql(
|
||||
f"""select
|
||||
operation, description, workstation, idx,
|
||||
base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
|
||||
"Pending" as status, parent as bom, batch_size, sequence_id
|
||||
from
|
||||
`tabBOM Operation`
|
||||
where
|
||||
parent = %s order by idx
|
||||
""", bom_no, as_dict=1)
|
||||
data = frappe.get_all("BOM Operation",
|
||||
filters={"parent": bom_no},
|
||||
fields=["operation", "description", "workstation", "idx",
|
||||
"base_hour_rate as hour_rate", "time_in_mins", "parent as bom",
|
||||
"batch_size", "sequence_id", "fixed_time"],
|
||||
order_by="idx")
|
||||
|
||||
for d in data:
|
||||
if not d.fixed_time:
|
||||
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
|
||||
d.status = "Pending"
|
||||
|
||||
return data
|
||||
|
||||
|
||||
self.set('operations', [])
|
||||
@ -542,7 +545,8 @@ class WorkOrder(Document):
|
||||
|
||||
def calculate_time(self):
|
||||
for d in self.get("operations"):
|
||||
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
|
||||
if not d.fixed_time:
|
||||
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
|
||||
|
||||
self.calculate_operating_cost()
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
|
||||
WorkstationHolidayError,
|
||||
check_if_within_operating_hours,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Warehouse"]
|
||||
test_records = frappe.get_test_records('Workstation')
|
||||
make_test_records('Workstation')
|
||||
|
||||
class TestWorkstation(unittest.TestCase):
|
||||
class TestWorkstation(ERPNextTestCase):
|
||||
def test_validate_timings(self):
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
||||
|
@ -165,6 +165,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
|
||||
erpnext.patches.v12_0.set_default_payroll_based_on
|
||||
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
||||
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
|
||||
erpnext.patches.v13_0.validate_options_for_data_field
|
||||
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
|
||||
erpnext.patches.v12_0.fix_quotation_expired_status
|
||||
erpnext.patches.v12_0.rename_pos_closing_doctype
|
||||
@ -287,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
|
||||
erpnext.patches.v14_0.delete_einvoicing_doctypes
|
||||
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
|
||||
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
||||
erpnext.patches.v13_0.validate_options_for_data_field
|
||||
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
|
||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||
erpnext.patches.v13_0.fix_invoice_statuses
|
||||
@ -313,6 +313,8 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||
erpnext.patches.v14_0.delete_hub_doctypes
|
||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||
erpnext.patches.v14_0.migrate_crm_settings
|
||||
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others
|
||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
@ -3,10 +3,13 @@
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.regional.saudi_arabia.setup import add_print_formats
|
||||
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
|
||||
if company:
|
||||
add_print_formats()
|
||||
return
|
||||
|
||||
if frappe.db.exists('DocType', 'Print Format'):
|
||||
|
@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
@ -12,5 +13,20 @@ def execute():
|
||||
|
||||
if frappe.db.exists('DocType', 'Sales Invoice'):
|
||||
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
|
||||
|
||||
# rename_field method assumes that the field already exists or the doc is synced
|
||||
if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
|
||||
create_custom_fields({
|
||||
'Sales Invoice': [
|
||||
dict(
|
||||
fieldname='ksa_einv_qr',
|
||||
label='KSA E-Invoicing QR',
|
||||
fieldtype='Attach Image',
|
||||
read_only=1, no_copy=1, hidden=1
|
||||
)
|
||||
]
|
||||
})
|
||||
|
||||
if frappe.db.has_column('Sales Invoice', 'qr_code'):
|
||||
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
|
||||
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")
|
||||
|
@ -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):
|
||||
amount, additional_amount = row.amount, row.additional_amount
|
||||
timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
|
||||
|
||||
if (self.salary_structure and
|
||||
cint(row.depends_on_payment_days) and cint(self.total_working_days)
|
||||
and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
|
||||
and (not self.salary_slip_based_on_timesheet or
|
||||
and (row.salary_component != timesheet_component or
|
||||
getdate(self.start_date) < joining_date or
|
||||
(relieving_date and getdate(self.end_date) > relieving_date)
|
||||
)):
|
||||
@ -952,7 +954,7 @@ class SalarySlip(TransactionBase):
|
||||
amount = flt((flt(row.default_amount) * flt(self.payment_days)
|
||||
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
|
||||
|
||||
elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
|
||||
elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
|
||||
amount, additional_amount = 0, 0
|
||||
elif not row.amount:
|
||||
amount = flt(row.default_amount) + flt(row.additional_amount)
|
||||
|
@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
def test_payment_days_in_salary_slip_based_on_timesheet(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.projects.doctype.timesheet.test_timesheet import (
|
||||
make_salary_structure_for_timesheet,
|
||||
make_timesheet,
|
||||
)
|
||||
from erpnext.projects.doctype.timesheet.timesheet import (
|
||||
make_salary_slip as make_salary_slip_for_timesheet,
|
||||
)
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
||||
|
||||
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
|
||||
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
# mark attendance
|
||||
month_start_date = get_first_day(nowdate())
|
||||
month_end_date = get_last_day(nowdate())
|
||||
|
||||
first_sunday = frappe.db.sql("""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = 'Salary Slip Test Holiday List'
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""", (month_start_date, month_end_date))[0][0]
|
||||
|
||||
mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
|
||||
|
||||
# salary structure based on timesheet
|
||||
make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
|
||||
salary_slip.start_date = month_start_date
|
||||
salary_slip.end_date = month_end_date
|
||||
salary_slip.save()
|
||||
salary_slip.submit()
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
|
||||
|
||||
# gross pay calculation based on attendance (payment days)
|
||||
gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days))
|
||||
|
||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
|
@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
|
||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
|
||||
|
||||
|
||||
def test_timesheet_billing_amount(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
@ -160,6 +156,9 @@ def make_salary_structure_for_timesheet(employee, company=None):
|
||||
salary_structure_name = "Timesheet Salary Structure Test"
|
||||
frequency = "Monthly"
|
||||
|
||||
if not frappe.db.exists("Salary Component", "Timesheet Component"):
|
||||
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
|
||||
|
||||
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
|
||||
salary_structure.salary_component = "Timesheet Component"
|
||||
salary_structure.salary_slip_based_on_timesheet = 1
|
||||
|
@ -84,6 +84,10 @@ $.extend(erpnext, {
|
||||
});
|
||||
},
|
||||
|
||||
route_to_pending_reposts: (args) => {
|
||||
frappe.set_route('List', 'Repost Item Valuation', args);
|
||||
},
|
||||
|
||||
proceed_save_with_reminders_frequency_change: () => {
|
||||
frappe.ui.hide_open_dialog();
|
||||
|
||||
@ -426,12 +430,9 @@ erpnext.utils.select_alternate_items = function(opts) {
|
||||
qty = row.qty;
|
||||
}
|
||||
row[item_field] = d.alternate_item;
|
||||
frm.script_manager.trigger(item_field, row.doctype, row.name)
|
||||
.then(() => {
|
||||
frappe.model.set_value(row.doctype, row.name, 'qty', qty);
|
||||
frappe.model.set_value(row.doctype, row.name,
|
||||
opts.original_item_field, d.item_code);
|
||||
});
|
||||
frappe.model.set_value(row.doctype, row.name, 'qty', qty);
|
||||
frappe.model.set_value(row.doctype, row.name, opts.original_item_field, d.item_code);
|
||||
frm.trigger(item_field, row.doctype, row.name);
|
||||
});
|
||||
|
||||
refresh_field(opts.child_docname);
|
||||
@ -831,7 +832,7 @@ $(document).on('app_ready', function() {
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
|
||||
&& frm.doc.agreement_status === 'Ongoing') {
|
||||
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
|
||||
frappe.call({
|
||||
'method': 'frappe.client.get',
|
||||
args: {
|
||||
@ -884,9 +885,11 @@ $(document).on('app_ready', function() {
|
||||
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||
frm.dashboard.clear_headline();
|
||||
|
||||
let time_to_respond = get_status(frm.doc.response_by_variance);
|
||||
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
|
||||
let time_to_respond;
|
||||
if (!frm.doc.first_responded_on) {
|
||||
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
|
||||
} else {
|
||||
time_to_respond = get_status(frm.doc.response_by, frm.doc.first_responded_on);
|
||||
}
|
||||
|
||||
let alert = `
|
||||
@ -899,9 +902,11 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||
|
||||
|
||||
if (apply_sla_for_resolution) {
|
||||
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
|
||||
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
|
||||
let time_to_resolve;
|
||||
if (!frm.doc.resolution_date) {
|
||||
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
||||
} else {
|
||||
time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date);
|
||||
}
|
||||
|
||||
alert += `
|
||||
@ -924,8 +929,9 @@ function get_time_left(timestamp, agreement_status) {
|
||||
return {'diff_display': diff_display, 'indicator': indicator};
|
||||
}
|
||||
|
||||
function get_status(variance) {
|
||||
if (variance > 0) {
|
||||
function get_status(expected, actual) {
|
||||
const time_left = moment(expected).diff(moment(actual));
|
||||
if (time_left >= 0) {
|
||||
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
|
||||
} else {
|
||||
return {'diff_display': 'Failed', 'indicator': 'red'};
|
||||
|
@ -114,9 +114,11 @@ def get_items(filters):
|
||||
|
||||
items = frappe.db.sql("""
|
||||
select
|
||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
|
||||
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
|
||||
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.stock_uom,
|
||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
||||
@ -124,6 +126,8 @@ def get_items(filters):
|
||||
and `tabSales Invoice`.docstatus = 1
|
||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||
group by
|
||||
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
|
||||
|
||||
""" % (conditions, match_conditions), filters, as_dict=1)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.utils import flt
|
||||
@ -11,7 +9,7 @@ from frappe.utils import flt
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
||||
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
|
||||
|
||||
test_ignore = ["Price List"]
|
||||
test_dependencies = ['Payment Term', 'Payment Terms Template']
|
||||
@ -19,7 +17,7 @@ test_records = frappe.get_test_records('Customer')
|
||||
|
||||
|
||||
|
||||
class TestCustomer(unittest.TestCase):
|
||||
class TestCustomer(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.get_value('Item', '_Test Item'):
|
||||
make_test_records('Item')
|
||||
|
@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.queries import item_query
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ['Item', 'Customer', 'Supplier']
|
||||
|
||||
@ -17,7 +18,7 @@ def create_party_specific_item(**args):
|
||||
psi.based_on_value = args.get('based_on_value')
|
||||
psi.insert()
|
||||
|
||||
class TestPartySpecificItem(unittest.TestCase):
|
||||
class TestPartySpecificItem(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
self.customer = frappe.get_last_doc("Customer")
|
||||
self.supplier = frappe.get_last_doc("Supplier")
|
||||
|
@ -1,15 +1,15 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Product Bundle"]
|
||||
|
||||
|
||||
class TestQuotation(unittest.TestCase):
|
||||
class TestQuotation(ERPNextTestCase):
|
||||
def test_make_quotation_without_terms(self):
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
self.assertFalse(quotation.get('payment_schedule'))
|
||||
|
@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
@ -28,12 +27,14 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestSalesOrder(unittest.TestCase):
|
||||
class TestSalesOrder(ERPNextTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
||||
"unlink_advance_payment_on_cancelation_of_order"))
|
||||
|
||||
@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase):
|
||||
# reset config to previous state
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
|
||||
super().tearDownClass()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
29
erpnext/selling/form_tour/customer/customer.json
Normal file
29
erpnext/selling/form_tour/customer/customer.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"creation": "2021-11-23 10:44:13.185982",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-11-23 10:54:09.602358",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Customer",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Enter the Full Name of the Customer",
|
||||
"field": "",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Full Name",
|
||||
"parent_field": "",
|
||||
"position": "Left",
|
||||
"title": "Full Name"
|
||||
}
|
||||
],
|
||||
"title": "Customer"
|
||||
}
|
67
erpnext/selling/form_tour/quotation/quotation.json
Normal file
67
erpnext/selling/form_tour/quotation/quotation.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"creation": "2021-11-23 12:00:36.138824",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-11-23 12:02:48.010298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Quotation",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.",
|
||||
"field": "",
|
||||
"fieldname": "quotation_to",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Quotation To",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Quotation To"
|
||||
},
|
||||
{
|
||||
"description": "Select a specific Customer to whom this quotation will be sent.",
|
||||
"field": "",
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Party",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Party"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Quotation Item",
|
||||
"description": "Select an item for which you will be quoting a price.",
|
||||
"field": "",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Items",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Bottom",
|
||||
"title": "Items"
|
||||
},
|
||||
{
|
||||
"description": "You can select pre-populated Sales Taxes and Charges from here.",
|
||||
"field": "",
|
||||
"fieldname": "taxes",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Sales Taxes and Charges",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Sales Taxes and Charges"
|
||||
}
|
||||
],
|
||||
"title": "Quotation"
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
||||
@ -11,9 +9,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
|
||||
class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
|
||||
def test_result_for_partial_material_request(self):
|
||||
so = make_sales_order()
|
||||
mr=make_material_request(so.name)
|
||||
|
@ -2,15 +2,14 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.sales_analytics.sales_analytics import execute
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestAnalytics(unittest.TestCase):
|
||||
class TestAnalytics(ERPNextTestCase):
|
||||
def test_sales_analytics(self):
|
||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||
|
||||
|
67
erpnext/setup/form_tour/company/company.json
Normal file
67
erpnext/setup/form_tour/company/company.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"creation": "2021-11-24 10:17:18.534917",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 1,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-11-24 15:38:21.026582",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Company",
|
||||
"save_on_complete": 0,
|
||||
"steps": [
|
||||
{
|
||||
"description": "This is the default currency for this company.",
|
||||
"field": "",
|
||||
"fieldname": "default_currency",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Default Currency",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Default Currency"
|
||||
},
|
||||
{
|
||||
"description": "Here, you can add multiple addresses of the company",
|
||||
"field": "",
|
||||
"fieldname": "company_info",
|
||||
"fieldtype": "Section Break",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Address & Contact",
|
||||
"parent_field": "",
|
||||
"position": "Top",
|
||||
"title": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"description": "Here, you can set default Accounts, which will ease the creation of accounting entries.",
|
||||
"field": "",
|
||||
"fieldname": "default_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Accounts Settings",
|
||||
"parent_field": "",
|
||||
"position": "Top",
|
||||
"title": "Accounts Settings"
|
||||
},
|
||||
{
|
||||
"description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.",
|
||||
"field": "",
|
||||
"fieldname": "enable_perpetual_inventory",
|
||||
"fieldtype": "Check",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Enable Perpetual Inventory",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Enable Perpetual Inventory"
|
||||
}
|
||||
],
|
||||
"title": "Company"
|
||||
}
|
62
erpnext/setup/module_onboarding/home/home.json
Normal file
62
erpnext/setup/module_onboarding/home/home.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"allow_roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager"
|
||||
},
|
||||
{
|
||||
"role": "Item Manager"
|
||||
}
|
||||
],
|
||||
"creation": "2021-11-22 12:19:15.888642",
|
||||
"docstatus": 0,
|
||||
"doctype": "Module Onboarding",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2021-12-15 14:23:52.460913",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Home",
|
||||
"owner": "Administrator",
|
||||
"steps": [
|
||||
{
|
||||
"step": "Company Set Up"
|
||||
},
|
||||
{
|
||||
"step": "Navigation Help"
|
||||
},
|
||||
{
|
||||
"step": "Data import"
|
||||
},
|
||||
{
|
||||
"step": "Create an Item"
|
||||
},
|
||||
{
|
||||
"step": "Create a Customer"
|
||||
},
|
||||
{
|
||||
"step": "Create a Supplier"
|
||||
},
|
||||
{
|
||||
"step": "Create a Quotation"
|
||||
},
|
||||
{
|
||||
"step": "Letterhead"
|
||||
}
|
||||
],
|
||||
"subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation",
|
||||
"success_message": "Masters are all set up!",
|
||||
"title": "Let's Set Up Some Masters"
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let's review your Company",
|
||||
"creation": "2021-11-22 11:55:48.931427",
|
||||
"description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:22:18.317423",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Company Set Up",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Company",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Set Up a Company",
|
||||
"validate_action": 1
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let\u2019s create your first Customer",
|
||||
"creation": "2020-05-14 17:46:41.831517",
|
||||
"description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:20:31.197564",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Customer",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Customer",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Manage Customers",
|
||||
"validate_action": 1
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let\u2019s create your first Quotation",
|
||||
"creation": "2020-06-01 13:34:58.958641",
|
||||
"description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:21:31.675330",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Quotation",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Quotation",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Create your first Quotation",
|
||||
"validate_action": 1
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let\u2019s create your first Supplier",
|
||||
"creation": "2020-05-14 22:09:10.043554",
|
||||
"description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:21:23.518301",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Supplier",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Supplier",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Manage Suppliers",
|
||||
"validate_action": 1
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Create a new Item",
|
||||
"creation": "2021-05-17 13:47:18.515052",
|
||||
"description": "# Create an Item\n\nItem is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"form_tour": "Item General",
|
||||
"idx": 0,
|
||||
"intro_video_url": "",
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:19:56.297772",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create an Item",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Item",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Manage Items",
|
||||
"validate_action": 1
|
||||
}
|
21
erpnext/setup/onboarding_step/data_import/data_import.json
Normal file
21
erpnext/setup/onboarding_step/data_import/data_import.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Watch Video",
|
||||
"action_label": "Learn more about data migration",
|
||||
"creation": "2021-05-19 05:29:16.809610",
|
||||
"description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 13:10:57.346422",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Data import",
|
||||
"owner": "Administrator",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Import Data from Spreadsheet",
|
||||
"validate_action": 1,
|
||||
"video_url": "https://youtu.be/DQyqeurPI64"
|
||||
}
|
21
erpnext/setup/onboarding_step/letterhead/letterhead.json
Normal file
21
erpnext/setup/onboarding_step/letterhead/letterhead.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action_label": "Let\u2019s setup your first Letter Head",
|
||||
"creation": "2021-11-22 12:36:34.583783",
|
||||
"description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:21:39.037742",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Letterhead",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Letter Head",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Setup Your Letterhead",
|
||||
"validate_action": 1
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "Watch Video",
|
||||
"action_label": "Learn about Navigation options",
|
||||
"creation": "2021-11-22 12:09:52.233872",
|
||||
"description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-12-15 14:20:55.441678",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Navigation Help",
|
||||
"owner": "Administrator",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "How to Navigate in ERPNext",
|
||||
"validate_action": 1,
|
||||
"video_url": "https://youtu.be/j60xyNFqX_A"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user