Merge branch 'develop' into ledger-merger

This commit is contained in:
Dany Robert 2021-12-18 11:32:20 +05:30 committed by GitHub
commit e75295a151
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 5224 additions and 1185 deletions

View File

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

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

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

View File

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

View File

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

56
.github/stale.yml vendored
View File

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

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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
}
]
})

View File

@ -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",

View File

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

View File

@ -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()

View File

@ -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')

View File

@ -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",

View File

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

View File

@ -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
});
});
}
};

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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
}

View File

@ -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",

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"

View File

@ -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() });
}
},

View File

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

View File

@ -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))

View File

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

View File

@ -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": []
}

View File

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

View File

@ -75,6 +75,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) {

View File

@ -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",

View File

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

View File

@ -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": [
{

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

@ -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'):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}

View 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"
}

View File

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

View File

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

View File

@ -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"
}

View 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"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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"
}

View 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
}

View File

@ -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