Merge pull request #27739 from nextchamp-saqib/sla-migrate-fix
fix: sync_jobs fails
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
index 39b00d3..79e08b3 100644
--- a/cypress/integration/test_organizational_chart_desktop.js
+++ b/cypress/integration/test_organizational_chart_desktop.js
@@ -6,7 +6,7 @@
it('navigates to org chart', () => {
cy.visit('/app');
- cy.awesomebar('Organizational Chart');
+ cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {
diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js
index 6e75151..161fae0 100644
--- a/cypress/integration/test_organizational_chart_mobile.js
+++ b/cypress/integration/test_organizational_chart_mobile.js
@@ -7,7 +7,7 @@
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
- cy.awesomebar('Organizational Chart');
+ cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index bcd0771..71957e6 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -374,12 +374,15 @@
try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
frappe.db.commit()
- except Exception:
- frappe.db.rollback()
- traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
+ except Exception as e:
+ if frappe.flags.in_test:
+ raise e
+ else:
+ frappe.db.rollback()
+ traceback = frappe.get_traceback()
+ frappe.log_error(message=traceback)
- frappe.flags.deferred_accounting_error = True
+ frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
index f67c59c..66a269e 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
@@ -12,11 +12,6 @@
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
frm.set_df_property('chart_preview', 'hidden',
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
-
- // Show import button when file is successfully attached
- if (frm.page && frm.page.show_import_button) {
- create_import_button(frm);
- }
},
download_template: function(frm) {
@@ -78,7 +73,12 @@
frm.page.set_indicator("");
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
} else {
- generate_tree_preview(frm);
+ frappe.run_serially([
+ () => validate_coa(frm),
+ () => generate_tree_preview(frm),
+ () => create_import_button(frm),
+ () => frm.set_df_property('chart_preview', 'hidden', 0),
+ ]);
}
},
@@ -104,24 +104,26 @@
});
var create_import_button = function(frm) {
- frm.page.set_primary_action(__("Import"), function () {
- frappe.call({
- method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
- args: {
- file_name: frm.doc.import_file,
- company: frm.doc.company
- },
- freeze: true,
- freeze_message: __("Creating Accounts..."),
- callback: function(r) {
- if(!r.exc) {
- clearInterval(frm.page["interval"]);
- frm.page.set_indicator(__('Import Successful'), 'blue');
- create_reset_button(frm);
+ if (frm.page.show_import_button) {
+ frm.page.set_primary_action(__("Import"), function () {
+ return frappe.call({
+ method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
+ args: {
+ file_name: frm.doc.import_file,
+ company: frm.doc.company
+ },
+ freeze: true,
+ freeze_message: __("Creating Accounts..."),
+ callback: function(r) {
+ if (!r.exc) {
+ clearInterval(frm.page["interval"]);
+ frm.page.set_indicator(__('Import Successful'), 'blue');
+ create_reset_button(frm);
+ }
}
- }
- });
- }).addClass('btn btn-primary');
+ });
+ }).addClass('btn btn-primary');
+ }
};
var create_reset_button = function(frm) {
@@ -132,13 +134,35 @@
}).addClass('btn btn-primary');
};
+var validate_coa = function(frm) {
+ if (frm.doc.import_file) {
+ let parent = __('All Accounts');
+
+ return frappe.call({
+ 'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
+ 'args': {
+ file_name: frm.doc.import_file,
+ parent: parent,
+ doctype: 'Chart of Accounts Importer',
+ file_type: frm.doc.file_type,
+ for_validate: 1
+ },
+ callback: function(r) {
+ if (r.message['show_import_button']) {
+ frm.page['show_import_button'] = Boolean(r.message['show_import_button']);
+ }
+ }
+ });
+ }
+};
+
var generate_tree_preview = function(frm) {
if (frm.doc.import_file) {
let parent = __('All Accounts');
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
// generate tree structure based on the csv data
- new frappe.ui.Tree({
+ return new frappe.ui.Tree({
parent: $(frm.fields_dict['chart_tree'].wrapper),
label: parent,
expandable: true,
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 9a0234a..bd2a6f1 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -64,6 +64,7 @@
else:
data = generate_data_from_excel(file_doc, extension)
+ frappe.local.flags.ignore_root_company_validation = True
forest = build_forest(data)
create_charts(company, custom_chart=forest)
@@ -128,7 +129,7 @@
return data
@frappe.whitelist()
-def get_coa(doctype, parent, is_root=False, file_name=None):
+def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
''' called by tree view (to fetch node's children) '''
file_doc, extension = get_file(file_name)
@@ -140,14 +141,20 @@
data = generate_data_from_excel(file_doc, extension)
validate_columns(data)
- validate_accounts(data)
- forest = build_forest(data)
- accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
+ validate_accounts(file_doc, extension)
- # filter out to show data for the selected node only
- accounts = [d for d in accounts if d['parent_account']==parent]
+ if not for_validate:
+ forest = build_forest(data)
+ accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
- return accounts
+ # filter out to show data for the selected node only
+ accounts = [d for d in accounts if d['parent_account']==parent]
+
+ return accounts
+ else:
+ return {
+ 'show_import_button': 1
+ }
def build_forest(data):
'''
@@ -304,10 +311,7 @@
@frappe.whitelist()
-def validate_accounts(file_name):
-
- file_doc, extension = get_file(file_name)
-
+def validate_accounts(file_doc, extension):
if extension == 'csv':
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:
@@ -326,8 +330,6 @@
validate_root(accounts_dict)
- validate_account_types(accounts_dict)
-
return [True, len(accounts)]
def validate_root(accounts):
@@ -340,9 +342,19 @@
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
+ validate_missing_roots(roots)
+
if error_messages:
frappe.throw("<br>".join(error_messages))
+def validate_missing_roots(roots):
+ root_types_added = set(d.get('root_type') for d in roots)
+
+ missing = list(set(get_root_types()) - root_types_added)
+
+ if missing:
+ frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
+
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -368,15 +380,6 @@
{'account_type': 'Stock', 'root_type': 'Asset'}
]
-
-def validate_account_types(accounts):
- account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
- account_types = [accounts[d]["account_type"] for d in accounts if not cint(accounts[d]['is_group']) == 1]
-
- missing = list(set(account_types_for_ledger) - set(account_types))
- if missing:
- frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
-
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
diff --git a/erpnext/accounts/doctype/fiscal_year_company/fiscal_year_company.json b/erpnext/accounts/doctype/fiscal_year_company/fiscal_year_company.json
index 3eb0d74..67acb26 100644
--- a/erpnext/accounts/doctype/fiscal_year_company/fiscal_year_company.json
+++ b/erpnext/accounts/doctype/fiscal_year_company/fiscal_year_company.json
@@ -1,63 +1,33 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2014-10-02 13:35:44.155278",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2014-10-02 13:35:44.155278",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2016-07-11 03:28:00.505946",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Fiscal Year Company",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-28 18:01:53.495929",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Fiscal Year Company",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index b7bbb74..20678d7 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -13,10 +13,12 @@
"voucher_type",
"naming_series",
"finance_book",
+ "tax_withholding_category",
"column_break1",
"from_template",
"company",
"posting_date",
+ "apply_tds",
"2_add_edit_gl_entries",
"accounts",
"section_break99",
@@ -498,16 +500,32 @@
"options": "Journal Entry Template",
"print_hide": 1,
"report_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.apply_tds",
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "mandatory_depends_on": "eval:doc.apply_tds",
+ "options": "Tax Withholding Category"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
+ "fieldname": "apply_tds",
+ "fieldtype": "Check",
+ "label": "Apply Tax Withholding Amount "
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:56:01.121995",
+ "modified": "2021-09-09 15:31:14.484029",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 24368f0..e568a82 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -15,6 +15,9 @@
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
get_party_account_based_on_invoice_discounting,
)
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
+ get_party_tax_withholding_details,
+)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
check_if_stock_and_account_balance_synced,
@@ -57,7 +60,8 @@
self.validate_against_jv()
self.validate_reference_doc()
- self.set_against_account()
+ if self.docstatus == 0:
+ self.set_against_account()
self.create_remarks()
self.set_print_format_fields()
self.validate_expense_claim()
@@ -66,6 +70,10 @@
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_stock_accounts()
+
+ if self.docstatus == 0:
+ self.apply_tax_withholding()
+
if not self.title:
self.title = self.get_title()
@@ -139,6 +147,72 @@
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction)
+ def apply_tax_withholding(self):
+ from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
+
+ if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
+ return
+
+ parties = [d.party for d in self.get('accounts') if d.party]
+ parties = list(set(parties))
+
+ if len(parties) > 1:
+ frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
+
+ account_type_map = get_account_type_map(self.company)
+ party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
+ doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
+ debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
+ rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
+
+ party_account = get_party_account(party_type.title(), parties[0], self.company)
+
+ net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
+ not in ('Tax', 'Chargeable'))
+
+ party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
+
+ inv = frappe._dict({
+ party_type: parties[0],
+ 'doctype': doctype,
+ 'company': self.company,
+ 'posting_date': self.posting_date,
+ 'net_total': net_total
+ })
+
+ tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
+
+ if not tax_withholding_details:
+ return
+
+ accounts = []
+ for d in self.get('accounts'):
+ if d.get('account') == tax_withholding_details.get("account_head"):
+ d.update({
+ 'account': tax_withholding_details.get("account_head"),
+ debit_or_credit: tax_withholding_details.get('tax_amount')
+ })
+
+ accounts.append(d.get('account'))
+
+ if d.get('account') == party_account:
+ d.update({
+ rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
+ })
+
+ if not accounts or tax_withholding_details.get("account_head") not in accounts:
+ self.append("accounts", {
+ 'account': tax_withholding_details.get("account_head"),
+ rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
+ 'against_account': parties[0]
+ })
+
+ to_remove = [d for d in self.get('accounts')
+ if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
+
+ for d in to_remove:
+ self.remove(d)
+
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 43eb0b6..8961167 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -93,6 +93,7 @@
"options": "Payment Term"
},
{
+ "depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@@ -103,7 +104,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-21 13:30:11.605388",
+ "modified": "2021-09-26 17:06:55.597389",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index dde0328..55e288e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1303,6 +1303,7 @@
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
+ "ignore_user_permissions": 1,
"label": "Is Internal Supplier",
"read_only": 1
},
@@ -1400,7 +1401,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-21 09:27:39.967811",
+ "modified": "2021-09-28 13:10:28.351810",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index dd4a005..1c9943f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1146,6 +1146,12 @@
if not self.apply_tds:
return
+ if self.apply_tds and not self.get('tax_withholding_category'):
+ self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category')
+
+ if not self.tax_withholding_category:
+ return
+
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
if not tax_withholding_details:
diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
index 63dfff8..9fcbf5c 100644
--- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
+++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
@@ -97,6 +97,7 @@
"width": "100px"
},
{
+ "depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@@ -104,6 +105,7 @@
"read_only": 1
},
{
+ "depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
@@ -115,7 +117,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-20 16:26:53.820530",
+ "modified": "2021-09-26 15:47:28.167371",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index f383b52..2d6c04e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1953,6 +1953,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -2022,7 +2023,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-09-21 09:27:50.191854",
+ "modified": "2021-09-28 13:09:34.391799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 619dc7b..8a2e945 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1800,6 +1800,47 @@
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
+ def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
+ frappe.set_user("Administrator")
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
+
+ deferred_account = create_account(account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC", company="_Test Company")
+
+ item = create_item("_Test Item for Deferred Accounting")
+ item.enable_deferred_revenue = 1
+ item.deferred_revenue_account = deferred_account
+ item.no_of_months = 12
+ item.save()
+
+ si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
+ si.items[0].enable_deferred_revenue = 1
+ si.items[0].service_start_date = "2019-01-10"
+ si.items[0].service_end_date = "2019-03-15"
+ si.items[0].deferred_revenue_account = deferred_account
+ si.save()
+ si.submit()
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
+
+ pda1 = frappe.get_doc(dict(
+ doctype='Process Deferred Accounting',
+ posting_date=nowdate(),
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Income",
+ company="_Test Company"
+ ))
+
+ pda1.insert()
+ self.assertRaises(frappe.ValidationError, pda1.submit)
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
+
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@@ -2352,6 +2393,7 @@
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
+ "address_line2": "_Test Address Line 2",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
@@ -2373,11 +2415,12 @@
address.save()
- if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
+ if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
+ "address_line2": "_Test Address Line 2",
"address_title": "_Test Customer-Address for Eway bill",
- "address_type": "Shipping",
+ "address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
@@ -2397,9 +2440,34 @@
address.save()
+ if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_line2": "_Test Address Line 2",
+ "address_title": "_Test Customer-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "410098"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Customer",
+ "link_name": "_Test Customer"
+ })
+
+ address.save()
+
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Dispatch Address Line 1",
+ "address_line2": "_Test Dispatch Address Line 2",
"address_title": "_Test Dispatch-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
@@ -2414,11 +2482,6 @@
"pincode": "1100101"
}).insert()
- address.append("links", {
- "link_doctype": "Company",
- "link_name": "_Test Company"
- })
-
address.save()
def make_test_transporter_for_ewaybill():
@@ -2458,7 +2521,8 @@
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
- si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
+ si.customer_address = "_Test Customer-Address for Eway bill-Billing"
+ si.shipping_address_name = "_Test Customer-Address for Eway bill-Shipping"
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
index 29422d6..f92b57a 100644
--- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
+++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
@@ -98,6 +98,7 @@
"width": "120px"
},
{
+ "depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@@ -105,6 +106,7 @@
"read_only": 1
},
{
+ "depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
@@ -116,7 +118,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-04 20:25:49.832052",
+ "modified": "2021-09-26 15:47:46.911595",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index fa4ea21..16ef5fc 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -100,6 +100,7 @@
for account_detail in tax_withholding.accounts:
if company == account_detail.company:
return frappe._dict({
+ "tax_withholding_category": tax_withholding_category,
"account_head": account_detail.account,
"rate": tax_rate_detail.tax_withholding_rate,
"from_date": tax_rate_detail.from_date,
@@ -206,18 +207,39 @@
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
+ doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice'
filters = {
- dr_or_cr: ['>', 0],
'company': company,
- 'party_type': party_type,
- 'party': ['in', parties],
+ frappe.scrub(party_type): ['in', parties],
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'is_opening': 'No',
- 'is_cancelled': 0
+ 'docstatus': 1
}
- return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
+ if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice":
+ filters.update({
+ 'apply_tds': 1,
+ 'tax_withholding_category': tax_details.get('tax_withholding_category')
+ })
+
+ invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
+
+ journal_entries = frappe.db.sql("""
+ SELECT j.name
+ FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
+ WHERE
+ j.docstatus = 1
+ AND j.is_opening = 'No'
+ AND j.posting_date between %s and %s
+ AND ja.{dr_or_cr} > 0
+ AND ja.party in %s
+ """.format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1)
+
+ if journal_entries:
+ journal_entries = journal_entries[0]
+
+ return invoices + journal_entries
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
# for advance vouchers, debit and credit is reversed
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 8a88d79..84b364b 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -176,6 +176,29 @@
for d in invoices:
d.cancel()
+ def test_multi_category_single_supplier(self):
+ frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category")
+ invoices = []
+
+ pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True)
+ pi.tax_withholding_category = "Test Service Category"
+ pi.save()
+ pi.submit()
+ invoices.append(pi)
+
+ # Second Invoice will apply TDS checked
+ pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True)
+ pi1.tax_withholding_category = "Test Goods Category"
+ pi1.save()
+ pi1.submit()
+ invoices.append(pi1)
+
+ self.assertEqual(pi1.taxes[0].tax_amount, 250)
+
+ #delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@@ -251,7 +274,8 @@
def create_records():
# create a new suppliers
- for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
+ for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3',
+ 'Test TDS Supplier4', 'Test TDS Supplier5']:
if frappe.db.exists('Supplier', name):
continue
@@ -390,3 +414,39 @@
'account': 'TDS - _TC'
}]
}).insert()
+
+ if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
+ frappe.get_doc({
+ "doctype": "Tax Withholding Category",
+ "name": "Test Service Category",
+ "category_name": "Test Service Category",
+ "rates": [{
+ 'from_date': fiscal_year[1],
+ 'to_date': fiscal_year[2],
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 2000,
+ 'cumulative_threshold': 2000
+ }],
+ "accounts": [{
+ 'company': '_Test Company',
+ 'account': 'TDS - _TC'
+ }]
+ }).insert()
+
+ if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
+ frappe.get_doc({
+ "doctype": "Tax Withholding Category",
+ "name": "Test Goods Category",
+ "category_name": "Test Goods Category",
+ "rates": [{
+ 'from_date': fiscal_year[1],
+ 'to_date': fiscal_year[2],
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 2000,
+ 'cumulative_threshold': 2000
+ }],
+ "accounts": [{
+ 'company': '_Test Company',
+ 'account': 'TDS - _TC'
+ }]
+ }).insert()
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 4bf2b82..0cee6f5 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -284,13 +284,16 @@
"""
Nobody can do GL Entries where posting date is before freezing date
except authorized person
+
+ Administrator has all the roles so this check will be bypassed if any role is allowed to post
+ Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
if getdate(posting_date) <= getdate(acc_frozen_upto) \
- and not frozen_accounts_modifier in frappe.get_roles():
+ and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
def set_as_cancel(voucher_type, voucher_no):
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.json b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.json
index dfc4b18..91f0798 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.json
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.json
@@ -1,12 +1,15 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
+ "columns": [],
"creation": "2018-08-21 11:25:00.551823",
+ "disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2018-09-21 11:25:00.551823",
+ "modified": "2021-09-20 17:43:39.518851",
"modified_by": "Administrator",
"module": "Accounts",
"name": "TDS Computation Summary",
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c4a8c7a..536df1f 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -2,11 +2,10 @@
import frappe
from frappe import _
-from frappe.utils import flt
-from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
- get_advance_vouchers,
- get_debit_note_amount,
+from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
+ get_result,
+ get_tds_docs,
)
from erpnext.accounts.utils import get_fiscal_year
@@ -17,9 +16,12 @@
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
columns = get_columns(filters)
- res = get_result(filters)
+ tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
- return columns, res
+ res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
+ final_result = group_by_supplier_and_category(res)
+
+ return columns, final_result
def validate_filters(filters):
''' Validate if dates are properly set and lie in the same fiscal year'''
@@ -33,81 +35,39 @@
filters["fiscal_year"] = from_year
-def get_result(filters):
- # if no supplier selected, fetch data for all tds applicable supplier
- # else fetch relevant data for selected supplier
- pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
- fields = ["name", pan+" as pan", "tax_withholding_category", "supplier_type", "supplier_name"]
+def group_by_supplier_and_category(data):
+ supplier_category_wise_map = {}
- if filters.supplier:
- filters.supplier = frappe.db.get_list('Supplier',
- {"name": filters.supplier}, fields)
- else:
- filters.supplier = frappe.db.get_list('Supplier',
- {"tax_withholding_category": ["!=", ""]}, fields)
+ for row in data:
+ supplier_category_wise_map.setdefault((row.get('supplier'), row.get('section_code')), {
+ 'pan': row.get('pan'),
+ 'supplier': row.get('supplier'),
+ 'supplier_name': row.get('supplier_name'),
+ 'section_code': row.get('section_code'),
+ 'entity_type': row.get('entity_type'),
+ 'tds_rate': row.get('tds_rate'),
+ 'total_amount_credited': 0.0,
+ 'tds_deducted': 0.0
+ })
+ supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['total_amount_credited'] += \
+ row.get('total_amount_credited', 0.0)
+
+ supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['tds_deducted'] += \
+ row.get('tds_deducted', 0.0)
+
+ final_result = get_final_result(supplier_category_wise_map)
+
+ return final_result
+
+
+def get_final_result(supplier_category_wise_map):
out = []
- for supplier in filters.supplier:
- tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category)
- rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year]
-
- if rate:
- rate = rate[0]
-
- try:
- account = [d.account for d in tds.accounts if d.company == filters.company][0]
-
- except IndexError:
- account = []
- total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
- filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
-
- if total_invoiced_amount or tds_deducted:
- row = [supplier.pan, supplier.name]
-
- if filters.naming_series == 'Naming Series':
- row.append(supplier.supplier_name)
-
- row.extend([tds.name, supplier.supplier_type, rate, total_invoiced_amount, tds_deducted])
- out.append(row)
+ for key, value in supplier_category_wise_map.items():
+ out.append(value)
return out
-def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
- ''' calculate total invoice amount and total tds deducted for given supplier '''
-
- entries = frappe.db.sql("""
- select voucher_no, credit
- from `tabGL Entry`
- where party in (%s) and credit > 0
- and company=%s and is_cancelled = 0
- and posting_date between %s and %s
- """, (supplier, company, from_date, to_date), as_dict=1)
-
- supplier_credit_amount = flt(sum(d.credit for d in entries))
-
- vouchers = [d.voucher_no for d in entries]
- vouchers += get_advance_vouchers([supplier], company=company,
- from_date=from_date, to_date=to_date)
-
- tds_deducted = 0
- if vouchers:
- tds_deducted = flt(frappe.db.sql("""
- select sum(credit)
- from `tabGL Entry`
- where account=%s and posting_date between %s and %s
- and company=%s and credit > 0 and voucher_no in ({0})
- """.format(', '.join("'%s'" % d for d in vouchers)),
- (account, from_date, to_date, company))[0][0])
-
- date_range_filter = [fiscal_year, from_date, to_date]
-
- debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
-
- total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
-
- return total_invoiced_amount, tds_deducted
-
def get_columns(filters):
columns = [
{
@@ -149,7 +109,7 @@
{
"label": _("TDS Rate %"),
"fieldname": "tds_rate",
- "fieldtype": "Float",
+ "fieldtype": "Percent",
"width": 90
},
{
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
index 72de318..ff2aa30 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
@@ -16,69 +16,6 @@
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
- "get_query": function() {
- return {
- "filters": {
- "tax_withholding_category": ["!=", ""],
- }
- }
- },
- on_change: function() {
- frappe.query_report.set_filter_value("purchase_invoice", "");
- frappe.query_report.refresh();
- }
- },
- {
- "fieldname":"purchase_invoice",
- "label": __("Purchase Invoice"),
- "fieldtype": "Link",
- "options": "Purchase Invoice",
- "get_query": function() {
- return {
- "filters": {
- "name": ["in", frappe.query_report.invoices]
- }
- }
- },
- on_change: function() {
- let supplier = frappe.query_report.get_filter_value('supplier');
- if(!supplier) return; // return if no supplier selected
-
- // filter invoices based on selected supplier
- let invoices = [];
- frappe.query_report.invoice_data.map(d => {
- if(d.supplier==supplier)
- invoices.push(d.name)
- });
- frappe.query_report.invoices = invoices;
- frappe.query_report.refresh();
- }
- },
- {
- "fieldname":"purchase_order",
- "label": __("Purchase Order"),
- "fieldtype": "Link",
- "options": "Purchase Order",
- "get_query": function() {
- return {
- "filters": {
- "name": ["in", frappe.query_report.invoices]
- }
- }
- },
- on_change: function() {
- let supplier = frappe.query_report.get_filter_value('supplier');
- if(!supplier) return; // return if no supplier selected
-
- // filter invoices based on selected supplier
- let invoices = [];
- frappe.query_report.invoice_data.map(d => {
- if(d.supplier==supplier)
- invoices.push(d.name)
- });
- frappe.query_report.invoices = invoices;
- frappe.query_report.refresh();
- }
},
{
"fieldname":"from_date",
@@ -96,23 +33,5 @@
"reqd": 1,
"width": "60px"
}
- ],
-
- onload: function(report) {
- // fetch all tds applied invoices
- frappe.call({
- "method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
- callback: function(r) {
- let invoices = [];
-
- r.message.map(d => {
- invoices.push(d.name);
- });
-
- report["invoice_data"] = r.message.invoices;
- report["invoices"] = invoices;
-
- }
- });
- }
+ ]
}
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json
index 557a62d..4d555bd 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json
@@ -1,13 +1,15 @@
{
"add_total_row": 1,
+ "columns": [],
"creation": "2018-08-21 11:32:30.874923",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2019-09-24 13:46:16.473711",
+ "modified": "2021-09-20 12:05:50.387572",
"modified_by": "Administrator",
"module": "Accounts",
"name": "TDS Payable Monthly",
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 9e1382b..621b697 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -8,19 +8,12 @@
def execute(filters=None):
- filters["invoices"] = frappe.cache().hget("invoices", frappe.session.user)
validate_filters(filters)
- set_filters(filters)
-
- # TDS payment entries
- payment_entries = get_payment_entires(filters)
+ tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
columns = get_columns(filters)
- if not filters.get("invoices"):
- return columns, []
- res = get_result(filters, payment_entries)
-
+ res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
return columns, res
def validate_filters(filters):
@@ -28,109 +21,59 @@
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
-def set_filters(filters):
- invoices = []
-
- if not filters.get("invoices"):
- filters["invoices"] = get_tds_invoices_and_orders()
-
- if filters.supplier and filters.purchase_invoice:
- for d in filters["invoices"]:
- if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
- invoices.append(d)
- elif filters.supplier and not filters.purchase_invoice:
- for d in filters["invoices"]:
- if d.supplier == filters.supplier:
- invoices.append(d)
- elif filters.purchase_invoice and not filters.supplier:
- for d in filters["invoices"]:
- if d.name == filters.purchase_invoice:
- invoices.append(d)
- elif filters.supplier and filters.purchase_order:
- for d in filters.get("invoices"):
- if d.name == filters.purchase_order and d.supplier == filters.supplier:
- invoices.append(d)
- elif filters.supplier and not filters.purchase_order:
- for d in filters.get("invoices"):
- if d.supplier == filters.supplier:
- invoices.append(d)
- elif filters.purchase_order and not filters.supplier:
- for d in filters.get("invoices"):
- if d.name == filters.purchase_order:
- invoices.append(d)
-
- filters["invoices"] = invoices if invoices else filters["invoices"]
- filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
-
- #print(filters.get('invoices'))
-
-def get_result(filters, payment_entries):
- supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
- documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
-
- gle_map = get_gle_map(filters, documents)
+def get_result(filters, tds_docs, tds_accounts, tax_category_map):
+ supplier_map = get_supplier_pan_map()
+ tax_rate_map = get_tax_rate_map(filters)
+ gle_map = get_gle_map(filters, tds_docs)
out = []
- for d in gle_map:
+ for name, details in gle_map.items():
tds_deducted, total_amount_credited = 0, 0
- supplier = supplier_map[d]
+ tax_withholding_category = tax_category_map.get(name)
+ rate = tax_rate_map.get(tax_withholding_category)
- tds_doc = tds_docs[supplier.tax_withholding_category]
- account_list = [i.account for i in tds_doc.accounts if i.company == filters.company]
+ for entry in details:
+ supplier = entry.party or entry.against
+ posting_date = entry.posting_date
+ voucher_type = entry.voucher_type
- if account_list:
- account = account_list[0]
+ if entry.account in tds_accounts:
+ tds_deducted += (entry.credit - entry.debit)
- for k in gle_map[d]:
- if k.party == supplier_map[d] and k.credit > 0:
- total_amount_credited += (k.credit - k.debit)
- elif account_list and k.account == account and (k.credit - k.debit) > 0:
- tds_deducted = (k.credit - k.debit)
- total_amount_credited += (k.credit - k.debit)
- voucher_type = k.voucher_type
+ total_amount_credited += (entry.credit - entry.debit)
- rate = [i.tax_withholding_rate for i in tds_doc.rates
- if i.fiscal_year == gle_map[d][0].fiscal_year]
-
- if rate and len(rate) > 0 and tds_deducted:
- rate = rate[0]
-
- row = [supplier.pan, supplier.name]
+ if rate and tds_deducted:
+ row = {
+ 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
+ 'supplier': supplier_map.get(supplier).name
+ }
if filters.naming_series == 'Naming Series':
- row.append(supplier.supplier_name)
+ row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
- row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
- tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
+ row.update({
+ 'section_code': tax_withholding_category,
+ 'entity_type': supplier_map.get(supplier).supplier_type,
+ 'tds_rate': rate,
+ 'total_amount_credited': total_amount_credited,
+ 'tds_deducted': tds_deducted,
+ 'transaction_date': posting_date,
+ 'transaction_type': voucher_type,
+ 'ref_no': name
+ })
+
out.append(row)
return out
-def get_supplier_map(filters, payment_entries):
- # create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
- # pre-fetch all distinct applicable tds docs
- supplier_map, tds_docs = {}, {}
- pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
- supplier_list = [d.supplier for d in filters["invoices"]]
+def get_supplier_pan_map():
+ supplier_map = frappe._dict()
+ suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
- supplier_detail = frappe.db.get_all('Supplier',
- {"name": ["in", supplier_list]},
- ["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
+ for d in suppliers:
+ supplier_map[d.name] = d
- for d in filters["invoices"]:
- supplier_map[d.get("name")] = [k for k in supplier_detail
- if k.name == d.get("supplier")][0]
-
- for d in payment_entries:
- supplier_map[d.get("name")] = [k for k in supplier_detail
- if k.name == d.get("supplier")][0]
-
- for d in supplier_detail:
- if d.get("tax_withholding_category") not in tds_docs:
- tds_docs[d.get("tax_withholding_category")] = \
- frappe.get_doc("Tax Withholding Category", d.get("tax_withholding_category"))
-
- return supplier_map, tds_docs
+ return supplier_map
def get_gle_map(filters, documents):
# create gle_map of the form
@@ -140,10 +83,9 @@
gle = frappe.db.get_all('GL Entry',
{
"voucher_no": ["in", documents],
- 'is_cancelled': 0,
- 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
+ "credit": (">", 0)
},
- ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
+ ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
)
for d in gle:
@@ -233,39 +175,57 @@
return columns
-def get_payment_entires(filters):
- filter_dict = {
- 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
- 'party_type': 'Supplier',
- 'apply_tax_withholding_amount': 1
+def get_tds_docs(filters):
+ tds_documents = []
+ purchase_invoices = []
+ payment_entries = []
+ journal_entries = []
+ tax_category_map = {}
+
+ tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
+ pluck="account")
+
+ query_filters = {
+ "credit": ('>', 0),
+ "account": ("in", tds_accounts),
+ "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
+ "is_cancelled": 0
}
- if filters.get('purchase_invoice') or filters.get('purchase_order'):
- parent = frappe.db.get_all('Payment Entry Reference',
- {'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
+ if filters.get('supplier'):
+ query_filters.update({'against': filters.get('supplier')})
- filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
+ tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
- payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
- filters=filter_dict)
+ for d in tds_docs:
+ if d.voucher_type == "Purchase Invoice":
+ purchase_invoices.append(d.voucher_no)
+ elif d.voucher_type == "Payment Entry":
+ payment_entries.append(d.voucher_no)
+ elif d.voucher_type == "Journal Entry":
+ journal_entries.append(d.voucher_no)
- return payment_entries
+ tds_documents.append(d.voucher_no)
-@frappe.whitelist()
-def get_tds_invoices_and_orders():
- # fetch tds applicable supplier and fetch invoices for these suppliers
- suppliers = [d.name for d in frappe.db.get_list("Supplier",
- {"tax_withholding_category": ["!=", ""]}, ["name"])]
+ if purchase_invoices:
+ get_tax_category_map(purchase_invoices, 'Purchase Invoice', tax_category_map)
- invoices = frappe.db.get_list("Purchase Invoice",
- {"supplier": ["in", suppliers]}, ["name", "supplier"])
+ if payment_entries:
+ get_tax_category_map(payment_entries, 'Payment Entry', tax_category_map)
- orders = frappe.db.get_list("Purchase Order",
- {"supplier": ["in", suppliers]}, ["name", "supplier"])
+ if journal_entries:
+ get_tax_category_map(journal_entries, 'Journal Entry', tax_category_map)
- invoices = invoices + orders
- invoices = [d for d in invoices if d.supplier]
+ return tds_documents, tds_accounts, tax_category_map
- frappe.cache().hset("invoices", frappe.session.user, invoices)
+def get_tax_category_map(vouchers, doctype, tax_category_map):
+ tax_category_map.update(frappe._dict(frappe.get_all(doctype,
+ filters = {'name': ('in', vouchers)}, fields=['name', 'tax_withholding_category'], as_list=1)))
- return invoices
+def get_tax_rate_map(filters):
+ rate_map = frappe.get_all('Tax Withholding Rate', filters={
+ 'from_date': ('<=', filters.get('from_date')),
+ 'to_date': ('>=', filters.get('to_date'))
+ }, fields=['parent', 'tax_withholding_rate'], as_list=1)
+
+ return frappe._dict(rate_map)
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 8ff4f97..39f102e 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -394,10 +394,6 @@
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
- if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(nowdate()):
- frappe.msgprint(_("Depreciation Row {0}: Depreciation Start Date is entered as past date")
- .format(row.idx), title=_('Warning'), indicator='red')
-
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
.format(row.idx))
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 4cc9be5..7183ee7 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -645,12 +645,18 @@
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
+ finance_book = frappe.new_doc('Finance Book')
+ finance_book.finance_book_name = 'Income Tax'
+ finance_book.for_income_tax = 1
+ finance_book.insert(ignore_if_duplicate=1)
+
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-07-12'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
+ "finance_book": finance_book.name,
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3,
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index ef54538..896208f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1121,6 +1121,7 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1143,7 +1144,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-08-30 20:03:14.008804",
+ "modified": "2021-09-28 13:10:47.955401",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
index 701da43..ca3be03 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
@@ -30,7 +30,14 @@
"default": frappe.datetime.get_today()
},
{
- "fieldname": "purchase_order",
+ "fieldname":"project",
+ "label": __("Project"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Project"
+ },
+ {
+ "fieldname": "name",
"label": __("Purchase Order"),
"fieldtype": "Link",
"width": "80",
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
index 5d59456..1b25dd4 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
@@ -41,14 +41,12 @@
if filters.get("from_date") and filters.get("to_date"):
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
- if filters.get("company"):
- conditions += " and po.company = %(company)s"
+ for field in ['company', 'name', 'status']:
+ if filters.get(field):
+ conditions += f" and po.{field} = %({field})s"
- if filters.get("purchase_order"):
- conditions += " and po.name = %(purchase_order)s"
-
- if filters.get("status"):
- conditions += " and po.status in %(status)s"
+ if filters.get('project'):
+ conditions += " and poi.project = %(project)s"
return conditions
@@ -57,6 +55,7 @@
SELECT
po.transaction_date as date,
poi.schedule_date as required_date,
+ poi.project,
po.name as purchase_order,
po.status, po.supplier, poi.item_code,
poi.qty, poi.received_qty,
@@ -175,6 +174,12 @@
"fieldtype": "Link",
"options": "Supplier",
"width": 130
+ },{
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 130
}]
if not filters.get("group_by_po"):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 87d5648..e9b531e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -685,13 +685,17 @@
.format(d.reference_name, d.against_order))
def set_advance_gain_or_loss(self):
- if not self.get("advances"):
+ if self.get('conversion_rate') == 1 or not self.get("advances"):
+ return
+
+ is_purchase_invoice = self.doctype == 'Purchase Invoice'
+ party_account = self.credit_to if is_purchase_invoice else self.debit_to
+ if get_account_currency(party_account) != self.currency:
return
for d in self.get("advances"):
advance_exchange_rate = d.ref_exchange_rate
- if (d.allocated_amount and self.conversion_rate != 1
- and self.conversion_rate != advance_exchange_rate):
+ if (d.allocated_amount and self.conversion_rate != advance_exchange_rate):
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
@@ -710,7 +714,7 @@
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
- frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
+ frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency:
@@ -729,7 +733,7 @@
"against": party,
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
dr_or_cr: abs(d.exchange_gain_loss),
- "cost_center": self.cost_center,
+ "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
"project": self.project
}, item=d)
)
diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
index 099146c..9dd97a6 100644
--- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
+++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
@@ -21,6 +21,7 @@
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ frappe.db.sql('delete from `tabPatient Appointment`')
make_pos_profile()
def test_medical_record(self):
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index 4f96f6a..021ba9b 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -6,7 +6,7 @@
import unittest
import frappe
-from frappe.utils import flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
create_appointment,
@@ -33,10 +33,12 @@
self.assertEqual(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = getdate()
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = add_days(getdate(), 1)
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
@@ -44,6 +46,7 @@
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session.start_date = add_days(getdate(), 2)
session = frappe.get_doc(session)
session.submit()
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index 6d63f39..b31a952 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -6,7 +6,7 @@
import frappe
from frappe.model.document import Document
-from frappe.utils import flt, today
+from frappe.utils import flt
class TherapyPlan(Document):
@@ -63,8 +63,6 @@
therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
- if frappe.flags.in_test:
- therapy_session.start_date = today()
return therapy_session.as_dict()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a8f1617..b89b10b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -344,6 +344,7 @@
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
+ "erpnext.hr.doctype.interview.interview.send_interview_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
],
"hourly": [
@@ -388,6 +389,7 @@
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
+ "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 9a3bac0..6b3c29a 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -9,83 +9,86 @@
return [__(doc.status), "orange", "status,=," + doc.status];
}
},
+
onload: function(list_view) {
let me = this;
- const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function() {
+ const months = moment.months();
+ list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
- fields: [
- {
- fieldname: 'employee',
- label: __('For Employee'),
- fieldtype: 'Link',
- options: 'Employee',
- get_query: () => {
- return {query: "erpnext.controllers.queries.employee_query"}
- },
- reqd: 1,
- onchange: function() {
- dialog.set_df_property("unmarked_days", "hidden", 1);
- dialog.set_df_property("status", "hidden", 1);
- dialog.set_df_property("month", "value", '');
+ fields: [{
+ fieldname: 'employee',
+ label: __('For Employee'),
+ fieldtype: 'Link',
+ options: 'Employee',
+ get_query: () => {
+ return {query: "erpnext.controllers.queries.employee_query"};
+ },
+ reqd: 1,
+ onchange: function() {
+ dialog.set_df_property("unmarked_days", "hidden", 1);
+ dialog.set_df_property("status", "hidden", 1);
+ dialog.set_df_property("month", "value", '');
+ dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
+ }
+ },
+ {
+ label: __("For Month"),
+ fieldtype: "Select",
+ fieldname: "month",
+ options: months,
+ reqd: 1,
+ onchange: function() {
+ if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
+ dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
+ me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
+ });
}
- },
- {
- label: __("For Month"),
- fieldtype: "Select",
- fieldname: "month",
- options: months,
- reqd: 1,
- onchange: function() {
- if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
- dialog.set_df_property("status", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- if (options.length > 0) {
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
- } else {
- dialog.no_unmarked_days_left = true;
- }
- });
- }
- }
- },
- {
- label: __("Status"),
- fieldtype: "Select",
- fieldname: "status",
- options: ["Present", "Absent", "Half Day", "Work From Home"],
- hidden:1,
- reqd: 1,
+ }
+ },
+ {
+ label: __("Status"),
+ fieldtype: "Select",
+ fieldname: "status",
+ options: ["Present", "Absent", "Half Day", "Work From Home"],
+ hidden: 1,
+ reqd: 1,
- },
- {
- label: __("Unmarked Attendance for days"),
- fieldname: "unmarked_days",
- fieldtype: "MultiCheck",
- options: [],
- columns: 2,
- hidden: 1
- },
- ],
- primary_action(data) {
+ },
+ {
+ label: __("Unmarked Attendance for days"),
+ fieldname: "unmarked_days",
+ fieldtype: "MultiCheck",
+ options: [],
+ columns: 2,
+ hidden: 1
+ }],
+ primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
- frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
+ [dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
- frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
- callback: function(r) {
+ callback: function (r) {
if (r.message === 1) {
- frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ frappe.show_alert({
+ message: __("Attendance Marked"),
+ indicator: 'blue'
+ });
cur_dialog.hide();
}
}
@@ -101,21 +104,26 @@
dialog.show();
});
},
- get_multi_select_options: function(employee, month){
+
+ get_multi_select_options: function(employee, month) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
async: false,
- args:{
+ args: {
employee: employee,
month: month,
}
}).then(r => {
var options = [];
- for(var d in r.message){
+ for (var d in r.message) {
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY');
- options.push({ "label":date, "value": r.message[d] , "checked": 1});
+ options.push({
+ "label": date,
+ "value": r.message[d],
+ "checked": 1
+ });
}
resolve(options);
});
diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js
index 5639cc9..13b33e2 100755
--- a/erpnext/hr/doctype/employee/employee.js
+++ b/erpnext/hr/doctype/employee/employee.js
@@ -15,19 +15,20 @@
}
refresh() {
- var me = this;
erpnext.toggle_naming_series();
}
date_of_birth() {
return cur_frm.call({
method: "get_retirement_date",
- args: {date_of_birth: this.frm.doc.date_of_birth}
+ args: {
+ date_of_birth: this.frm.doc.date_of_birth
+ }
});
}
salutation() {
- if(this.frm.doc.salutation) {
+ if (this.frm.doc.salutation) {
this.frm.set_value("gender", {
"Mr": "Male",
"Ms": "Female"
@@ -36,8 +37,9 @@
}
};
-frappe.ui.form.on('Employee',{
- setup: function(frm) {
+
+frappe.ui.form.on('Employee', {
+ setup: function (frm) {
frm.set_query("leave_policy", function() {
return {
"filters": {
@@ -46,7 +48,7 @@
};
});
},
- onload:function(frm) {
+ onload: function (frm) {
frm.set_query("department", function() {
return {
"filters": {
@@ -55,23 +57,28 @@
};
});
},
- prefered_contact_email:function(frm){
- frm.events.update_contact(frm)
+ prefered_contact_email: function(frm) {
+ frm.events.update_contact(frm);
},
- personal_email:function(frm){
- frm.events.update_contact(frm)
+
+ personal_email: function(frm) {
+ frm.events.update_contact(frm);
},
- company_email:function(frm){
- frm.events.update_contact(frm)
+
+ company_email: function(frm) {
+ frm.events.update_contact(frm);
},
- user_id:function(frm){
- frm.events.update_contact(frm)
+
+ user_id: function(frm) {
+ frm.events.update_contact(frm);
},
- update_contact:function(frm){
+
+ update_contact: function(frm) {
var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id';
frm.set_value("prefered_email",
- frm.fields_dict[prefered_email_fieldname].value)
+ frm.fields_dict[prefered_email_fieldname].value);
},
+
status: function(frm) {
return frm.call({
method: "deactivate_sales_person",
@@ -81,19 +88,63 @@
}
});
},
+
create_user: function(frm) {
- if (!frm.doc.prefered_email)
- {
- frappe.throw(__("Please enter Preferred Contact Email"))
+ if (!frm.doc.prefered_email) {
+ frappe.throw(__("Please enter Preferred Contact Email"));
}
frappe.call({
method: "erpnext.hr.doctype.employee.employee.create_user",
- args: { employee: frm.doc.name, email: frm.doc.prefered_email },
- callback: function(r)
- {
- frm.set_value("user_id", r.message)
+ args: {
+ employee: frm.doc.name,
+ email: frm.doc.prefered_email
+ },
+ callback: function (r) {
+ frm.set_value("user_id", r.message);
}
});
}
});
-cur_frm.cscript = new erpnext.hr.EmployeeController({frm: cur_frm});
+
+cur_frm.cscript = new erpnext.hr.EmployeeController({
+ frm: cur_frm
+});
+
+
+frappe.tour['Employee'] = [
+ {
+ fieldname: "first_name",
+ title: "First Name",
+ description: __("Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.")
+ },
+ {
+ fieldname: "company",
+ title: "Company",
+ description: __("Select a Company this Employee belongs to. Other HR features like Payroll. Expense Claims and Leaves for this Employee will be created for a given company only.")
+ },
+ {
+ fieldname: "date_of_birth",
+ title: "Date of Birth",
+ description: __("Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.")
+ },
+ {
+ fieldname: "date_of_joining",
+ title: "Date of Joining",
+ description: __("Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.")
+ },
+ {
+ fieldname: "holiday_list",
+ title: "Holiday List",
+ description: __("Select a default Holiday List for this Employee. The days listed in Holiday List will not be counted in Leave Application.")
+ },
+ {
+ fieldname: "reports_to",
+ title: "Reports To",
+ description: __("Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.")
+ },
+ {
+ fieldname: "leave_approver",
+ title: "Leave Approver",
+ description: __("Select Leave Approver for an employee. The user one who will look after his/her Leave application")
+ },
+];
diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
index 7c751a4..1a1bcb2 100644
--- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
+++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
@@ -55,8 +55,7 @@
else:
leave_type = None
- if not company:
- company = frappe.db.get_value("Employee", employee['employee'], "Company")
+ company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True)
attendance=frappe.get_doc(dict(
doctype='Attendance',
@@ -68,4 +67,4 @@
company=company
))
attendance.insert()
- attendance.submit()
+ attendance.submit()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index eae600d..1e3b9cb 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -71,6 +71,7 @@
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
+ applicant.designation = 'Researcher'
applicant.status = 'Open'
applicant.cover_letter = 'I am a great Researcher.'
applicant.insert()
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py
index 5cb5bb5..db356bf 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral.py
@@ -38,8 +38,10 @@
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
+ job_applicant.source = "Employee Referral"
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
+ job_applicant.designation = emp_ref.for_designation
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no
diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
index d0ee2fc..1340f62 100644
--- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
@@ -17,6 +17,11 @@
class TestEmployeeReferral(unittest.TestCase):
+
+ def setUp(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
@@ -50,6 +55,10 @@
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")
diff --git a/erpnext/hr/doctype/expected_skill_set/__init__.py b/erpnext/hr/doctype/expected_skill_set/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/__init__.py
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
new file mode 100644
index 0000000..899f5bd
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 13:05:06.741330",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "skill.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 14:26:33.062549",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Expected Skill Set",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
new file mode 100644
index 0000000..27120c1
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ExpectedSkillSet(Document):
+ pass
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.js b/erpnext/hr/doctype/holiday_list/holiday_list.js
index 462bd8b..ea033c7 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.js
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.js
@@ -1,10 +1,10 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Holiday List', {
+frappe.ui.form.on("Holiday List", {
refresh: function(frm) {
if (frm.doc.holidays) {
- frm.set_value('total_holidays', frm.doc.holidays.length);
+ frm.set_value("total_holidays", frm.doc.holidays.length);
}
},
from_date: function(frm) {
@@ -14,3 +14,36 @@
}
}
});
+
+frappe.tour["Holiday List"] = [
+ {
+ fieldname: "holiday_list_name",
+ title: "Holiday List Name",
+ description: __("Enter a name for this Holiday List."),
+ },
+ {
+ fieldname: "from_date",
+ title: "From Date",
+ description: __("Based on your HR Policy, select your leave allocation period's start date"),
+ },
+ {
+ fieldname: "to_date",
+ title: "To Date",
+ description: __("Based on your HR Policy, select your leave allocation period's end date"),
+ },
+ {
+ fieldname: "weekly_off",
+ title: "Weekly Off",
+ description: __("Select your weekly off day"),
+ },
+ {
+ fieldname: "get_weekly_off_dates",
+ title: "Add Holidays",
+ description: __("Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"),
+ },
+ {
+ fieldname: "holidays",
+ title: "Holidays",
+ description: __("Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.")
+ },
+];
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.js b/erpnext/hr/doctype/hr_settings/hr_settings.js
index ec99472..6e26a1f 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.js
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.js
@@ -2,7 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('HR Settings', {
- restrict_backdated_leave_application: function(frm) {
- frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
- }
});
+
+frappe.tour['HR Settings'] = [
+ {
+ fieldname: 'emp_created_by',
+ title: 'Employee Naming By',
+ description: __('Employee can be named by Employee ID if you assign one, or via Naming Series. Select your preference here.'),
+ },
+ {
+ fieldname: 'standard_working_hours',
+ title: 'Standard Working Hours',
+ description: __('Enter the Standard Working Hours for a normal work day. These hours will be used in calculations of reports such as Employee Hours Utilization and Project Profitability analysis.'),
+ },
+ {
+ fieldname: 'leave_and_expense_claim_settings',
+ title: 'Leave and Expense Clain Settings',
+ description: __('Review various other settings related to Employee Leaves and Expense Claim')
+ }
+];
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 8aa3c0c..5148435 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -7,30 +7,36 @@
"engine": "InnoDB",
"field_order": [
"employee_settings",
- "retirement_age",
"emp_created_by",
- "column_break_4",
"standard_working_hours",
- "expense_approver_mandatory_in_expense_claim",
+ "column_break_9",
+ "retirement_age",
"reminders_section",
"send_birthday_reminders",
- "column_break_9",
- "send_work_anniversary_reminders",
"column_break_11",
+ "send_work_anniversary_reminders",
+ "column_break_18",
"send_holiday_reminders",
"frequency",
- "leave_settings",
+ "leave_and_expense_claim_settings",
"send_leave_notification",
"leave_approval_notification_template",
"leave_status_notification_template",
- "role_allowed_to_create_backdated_leave_application",
- "column_break_18",
"leave_approver_mandatory_in_leave_application",
+ "restrict_backdated_leave_application",
+ "role_allowed_to_create_backdated_leave_application",
+ "column_break_29",
+ "expense_approver_mandatory_in_expense_claim",
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
- "restrict_backdated_leave_application",
- "hiring_settings",
- "check_vacancies"
+ "hiring_settings_section",
+ "check_vacancies",
+ "send_interview_reminder",
+ "interview_reminder_template",
+ "remind_before",
+ "column_break_4",
+ "send_interview_feedback_reminder",
+ "feedback_reminder_notification_template"
],
"fields": [
{
@@ -39,17 +45,16 @@
"label": "Employee Settings"
},
{
- "description": "Enter retirement age in years",
"fieldname": "retirement_age",
"fieldtype": "Data",
- "label": "Retirement Age"
+ "label": "Retirement Age (In Years)"
},
{
"default": "Naming Series",
- "description": "Employee records are created using the selected field",
+ "description": "Employee records are created using the selected option",
"fieldname": "emp_created_by",
"fieldtype": "Select",
- "label": "Employee Records to be created by",
+ "label": "Employee Naming By",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@@ -63,28 +68,6 @@
"label": "Expense Approver Mandatory In Expense Claim"
},
{
- "collapsible": 1,
- "fieldname": "leave_settings",
- "fieldtype": "Section Break",
- "label": "Leave Settings"
- },
- {
- "depends_on": "eval: doc.send_leave_notification == 1",
- "fieldname": "leave_approval_notification_template",
- "fieldtype": "Link",
- "label": "Leave Approval Notification Template",
- "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
- "options": "Email Template"
- },
- {
- "depends_on": "eval: doc.send_leave_notification == 1",
- "fieldname": "leave_status_notification_template",
- "fieldtype": "Link",
- "label": "Leave Status Notification Template",
- "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
- "options": "Email Template"
- },
- {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
@@ -101,34 +84,17 @@
"label": "Show Leaves Of All Department Members In Calendar"
},
{
- "collapsible": 1,
- "fieldname": "hiring_settings",
- "fieldtype": "Section Break",
- "label": "Hiring Settings"
- },
- {
- "default": "0",
- "fieldname": "check_vacancies",
- "fieldtype": "Check",
- "label": "Check Vacancies On Job Offer Creation"
- },
- {
"default": "0",
"fieldname": "auto_leave_encashment",
"fieldtype": "Check",
"label": "Auto Leave Encashment"
},
{
- "default": "0",
- "fieldname": "restrict_backdated_leave_application",
- "fieldtype": "Check",
- "label": "Restrict Backdated Leave Application"
- },
- {
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"fieldname": "role_allowed_to_create_backdated_leave_application",
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
+ "mandatory_depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"options": "Role"
},
{
@@ -138,12 +104,41 @@
"label": "Send Leave Notification"
},
{
+ "depends_on": "eval: doc.send_leave_notification == 1",
+ "fieldname": "leave_approval_notification_template",
+ "fieldtype": "Link",
+ "label": "Leave Approval Notification Template",
+ "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
+ "options": "Email Template"
+ },
+ {
+ "depends_on": "eval: doc.send_leave_notification == 1",
+ "fieldname": "leave_status_notification_template",
+ "fieldtype": "Link",
+ "label": "Leave Status Notification Template",
+ "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
+ "options": "Email Template"
+ },
+ {
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"label": "Standard Working Hours"
},
{
"collapsible": 1,
+ "fieldname": "leave_and_expense_claim_settings",
+ "fieldtype": "Section Break",
+ "label": "Leave and Expense Claim Settings"
+ },
+ {
+ "default": "00:15:00",
+ "depends_on": "send_interview_reminder",
+ "fieldname": "remind_before",
+ "fieldtype": "Time",
+ "label": "Remind Before"
+ },
+ {
+ "collapsible": 1,
"fieldname": "reminders_section",
"fieldtype": "Section Break",
"label": "Reminders"
@@ -166,6 +161,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Set the frequency for holiday reminders",
+ "mandatory_depends_on": "send_holiday_reminders",
"options": "Weekly\nMonthly"
},
{
@@ -181,13 +177,62 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_interview_reminder",
+ "fieldtype": "Check",
+ "label": "Send Interview Reminder"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_interview_feedback_reminder",
+ "fieldtype": "Check",
+ "label": "Send Interview Feedback Reminder"
+ },
+ {
+ "fieldname": "column_break_29",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "send_interview_feedback_reminder",
+ "fieldname": "feedback_reminder_notification_template",
+ "fieldtype": "Link",
+ "label": "Feedback Reminder Notification Template",
+ "mandatory_depends_on": "send_interview_feedback_reminder",
+ "options": "Email Template"
+ },
+ {
+ "depends_on": "send_interview_reminder",
+ "fieldname": "interview_reminder_template",
+ "fieldtype": "Link",
+ "label": "Interview Reminder Notification Template",
+ "mandatory_depends_on": "send_interview_reminder",
+ "options": "Email Template"
+ },
+ {
+ "default": "0",
+ "fieldname": "restrict_backdated_leave_application",
+ "fieldtype": "Check",
+ "label": "Restrict Backdated Leave Application"
+ },
+ {
+ "fieldname": "hiring_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Hiring Settings"
+ },
+ {
+ "default": "0",
+ "fieldname": "check_vacancies",
+ "fieldtype": "Check",
+ "label": "Check Vacancies On Job Offer Creation"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-08-24 14:54:12.834162",
+ "modified": "2021-10-01 23:46:11.098236",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/interview/__init__.py b/erpnext/hr/doctype/interview/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview/__init__.py
diff --git a/erpnext/hr/doctype/interview/interview.js b/erpnext/hr/doctype/interview/interview.js
new file mode 100644
index 0000000..6341e3a
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.js
@@ -0,0 +1,237 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview', {
+ onload: function (frm) {
+ frm.events.set_job_applicant_query(frm);
+
+ frm.set_query('interviewer', 'interview_details', function () {
+ return {
+ query: 'erpnext.hr.doctype.interview.interview.get_interviewer_list'
+ };
+ });
+ },
+
+ refresh: function (frm) {
+ if (frm.doc.docstatus != 2 && !frm.doc.__islocal) {
+ if (frm.doc.status === 'Pending') {
+ frm.add_custom_button(__('Reschedule Interview'), function() {
+ frm.events.show_reschedule_dialog(frm);
+ frm.refresh();
+ });
+ }
+
+ let allowed_interviewers = [];
+ frm.doc.interview_details.forEach(values => {
+ allowed_interviewers.push(values.interviewer);
+ });
+
+ if ((allowed_interviewers.includes(frappe.session.user))) {
+ frappe.db.get_value('Interview Feedback', {'interviewer': frappe.session.user, 'interview': frm.doc.name, 'docstatus': 1}, 'name', (r) => {
+ if (Object.keys(r).length === 0) {
+ frm.add_custom_button(__('Submit Feedback'), function () {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (r) {
+ frm.events.show_feedback_dialog(frm, r.message);
+ frm.refresh();
+ }
+ });
+ }).addClass('btn-primary');
+ }
+ });
+ }
+ }
+ },
+
+ show_reschedule_dialog: function (frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Reschedule Interview',
+ fields: [
+ {
+ label: 'Schedule On',
+ fieldname: 'scheduled_on',
+ fieldtype: 'Date',
+ reqd: 1
+ },
+ {
+ label: 'From Time',
+ fieldname: 'from_time',
+ fieldtype: 'Time',
+ reqd: 1
+ },
+ {
+ label: 'To Time',
+ fieldname: 'to_time',
+ fieldtype: 'Time',
+ reqd: 1
+ }
+ ],
+ primary_action_label: 'Reschedule',
+ primary_action(values) {
+ frm.call({
+ method: 'reschedule_interview',
+ doc: frm.doc,
+ args: {
+ scheduled_on: values.scheduled_on,
+ from_time: values.from_time,
+ to_time: values.to_time
+ }
+ }).then(() => {
+ frm.refresh();
+ d.hide();
+ });
+ }
+ });
+ d.show();
+ },
+
+ show_feedback_dialog: function (frm, data) {
+ let fields = frm.events.get_fields_for_feedback();
+
+ let d = new frappe.ui.Dialog({
+ title: __('Submit Feedback'),
+ fields: [
+ {
+ fieldname: 'skill_set',
+ fieldtype: 'Table',
+ label: __('Skill Assessment'),
+ cannot_add_rows: false,
+ in_editable_grid: true,
+ reqd: 1,
+ fields: fields,
+ data: data
+ },
+ {
+ fieldname: 'result',
+ fieldtype: 'Select',
+ options: ['', 'Cleared', 'Rejected'],
+ label: __('Result')
+ },
+ {
+ fieldname: 'feedback',
+ fieldtype: 'Small Text',
+ label: __('Feedback')
+ }
+ ],
+ size: 'large',
+ minimizable: true,
+ primary_action: function(values) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.create_interview_feedback',
+ args: {
+ data: values,
+ interview_name: frm.doc.name,
+ interviewer: frappe.session.user,
+ job_applicant: frm.doc.job_applicant
+ }
+ }).then(() => {
+ frm.refresh();
+ });
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ get_fields_for_feedback: function () {
+ return [{
+ fieldtype: 'Link',
+ fieldname: 'skill',
+ options: 'Skill',
+ in_list_view: 1,
+ label: __('Skill')
+ }, {
+ fieldtype: 'Rating',
+ fieldname: 'rating',
+ label: __('Rating'),
+ in_list_view: 1,
+ reqd: 1,
+ }];
+ },
+
+ set_job_applicant_query: function (frm) {
+ frm.set_query('job_applicant', function () {
+ let job_applicant_filters = {
+ status: ['!=', 'Rejected']
+ };
+ if (frm.doc.designation) {
+ job_applicant_filters.designation = frm.doc.designation;
+ }
+ return {
+ filters: job_applicant_filters
+ };
+ });
+ },
+
+ interview_round: async function (frm) {
+ frm.events.reset_values(frm);
+ frm.set_value('job_applicant', '');
+
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ if (frm.doc.interview_round) {
+ frm.events.set_interview_details(frm);
+ } else {
+ frm.set_value('interview_details', []);
+ }
+ },
+
+ set_interview_details: function (frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_interviewers',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (data) {
+ let interview_details = data.message;
+ frm.set_value('interview_details', []);
+ if (data.message.length) {
+ frm.set_value('interview_details', interview_details);
+ }
+ }
+ });
+ },
+
+ job_applicant: function (frm) {
+ if (!frm.doc.interview_round) {
+ frm.doc.job_applicant = '';
+ frm.refresh();
+ frappe.throw(__('Select Interview Round First'));
+ }
+
+ if (frm.doc.job_applicant) {
+ frm.events.set_designation_and_job_opening(frm);
+ } else {
+ frm.events.reset_values(frm);
+ }
+ },
+
+ set_designation_and_job_opening: async function (frm) {
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ let job_applicant_data = (await frappe.db.get_value(
+ 'Job Applicant', frm.doc.job_applicant, ['designation', 'job_title', 'resume_link'],
+ )).message;
+
+ if (!round_data.designation) {
+ frm.set_value('designation', job_applicant_data.designation);
+ }
+
+ frm.set_value('job_opening', job_applicant_data.job_title);
+ frm.set_value('resume_link', job_applicant_data.resume_link);
+ },
+
+ reset_values: function (frm) {
+ frm.set_value('designation', '');
+ frm.set_value('job_opening', '');
+ frm.set_value('resume_link', '');
+ }
+});
diff --git a/erpnext/hr/doctype/interview/interview.json b/erpnext/hr/doctype/interview/interview.json
new file mode 100644
index 0000000..0d393e7
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.json
@@ -0,0 +1,254 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-.YYYY.-.####",
+ "creation": "2021-04-12 15:03:11.524090",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interview_details_section",
+ "interview_round",
+ "job_applicant",
+ "job_opening",
+ "designation",
+ "resume_link",
+ "column_break_4",
+ "status",
+ "scheduled_on",
+ "from_time",
+ "to_time",
+ "interview_feedback_section",
+ "interview_details",
+ "ratings_section",
+ "expected_average_rating",
+ "column_break_12",
+ "average_rating",
+ "section_break_13",
+ "interview_summary",
+ "reminded",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "reqd": 1
+ },
+ {
+ "fieldname": "job_opening",
+ "fieldtype": "Link",
+ "label": "Job Opening",
+ "options": "Job Opening",
+ "read_only": 1
+ },
+ {
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "reqd": 1
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Pending\nUnder Review\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ratings_section",
+ "fieldtype": "Section Break",
+ "label": "Ratings"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Obtained Average Rating",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_summary",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "label": "Resume link"
+ },
+ {
+ "fieldname": "interview_details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview_round.expected_average_rating",
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Interview Summary"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "interview_round.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Designation",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "scheduled_on",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Scheduled On",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "reminded",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Reminded"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_details",
+ "fieldtype": "Table",
+ "options": "Interview Detail"
+ },
+ {
+ "fieldname": "interview_feedback_section",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "From Time",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "To Time",
+ "reqd": 1,
+ "set_only_once": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Feedback",
+ "link_fieldname": "interview"
+ }
+ ],
+ "modified": "2021-09-30 13:30:05.421035",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "job_applicant",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
new file mode 100644
index 0000000..955acca
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import datetime
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cstr, get_datetime, get_link_to_form
+
+
+class DuplicateInterviewRoundError(frappe.ValidationError):
+ pass
+
+class Interview(Document):
+ def validate(self):
+ self.validate_duplicate_interview()
+ self.validate_designation()
+ self.validate_overlap()
+
+ def on_submit(self):
+ if self.status not in ['Cleared', 'Rejected']:
+ frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
+
+ def validate_duplicate_interview(self):
+ duplicate_interview = frappe.db.exists('Interview', {
+ 'job_applicant': self.job_applicant,
+ 'interview_round': self.interview_round,
+ 'docstatus': 1
+ }
+ )
+
+ if duplicate_interview:
+ frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
+ frappe.bold(get_link_to_form('Interview', duplicate_interview)),
+ frappe.bold(self.job_applicant)
+ ))
+
+ def validate_designation(self):
+ applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
+ if self.designation :
+ if self.designation != applicant_designation:
+ frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
+ self.interview_round, frappe.bold(self.designation), applicant_designation),
+ exc=DuplicateInterviewRoundError)
+ else:
+ self.designation = applicant_designation
+
+ def validate_overlap(self):
+ interviewers = [entry.interviewer for entry in self.interview_details] or ['']
+
+ overlaps = frappe.db.sql("""
+ SELECT interview.name
+ FROM `tabInterview` as interview
+ INNER JOIN `tabInterview Detail` as detail
+ WHERE
+ interview.scheduled_on = %s and interview.name != %s and interview.docstatus != 2
+ and (interview.job_applicant = %s or detail.interviewer IN %s) and
+ ((from_time < %s and to_time > %s) or
+ (from_time > %s and to_time < %s) or
+ (from_time = %s))
+ """, (self.scheduled_on, self.name, self.job_applicant, interviewers,
+ self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
+
+ if overlaps:
+ overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
+ frappe.throw(overlapping_details, title=_('Overlap'))
+
+
+ @frappe.whitelist()
+ def reschedule_interview(self, scheduled_on, from_time, to_time):
+ original_date = self.scheduled_on
+ from_time = self.from_time
+ to_time = self.to_time
+
+ self.db_set({
+ 'scheduled_on': scheduled_on,
+ 'from_time': from_time,
+ 'to_time': to_time
+ })
+ self.notify_update()
+
+ recipients = get_recipients(self.name)
+
+ try:
+ frappe.sendmail(
+ recipients= recipients,
+ subject=_('Interview: {0} Rescheduled').format(self.name),
+ message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
+ original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
+ reference_doctype=self.doctype,
+ reference_name=self.name
+ )
+ except Exception:
+ frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
+
+ frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
+
+
+def get_recipients(name, for_feedback=0):
+ interview = frappe.get_doc('Interview', name)
+
+ if for_feedback:
+ recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
+ else:
+ recipients = [d.interviewer for d in interview.interview_details]
+ recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
+
+ return recipients
+
+
+@frappe.whitelist()
+def get_interviewers(interview_round):
+ return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
+
+
+def send_interview_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_reminder:
+ return
+
+ remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
+ remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
+ reminder_date_time = datetime.datetime.now() + datetime.timedelta(
+ hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
+
+ interviews = frappe.get_all('Interview', filters={
+ 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
+ 'status': 'Pending',
+ 'reminded': 0,
+ 'docstatus': ['!=', 2]
+ })
+
+ interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
+
+ for d in interviews:
+ doc = frappe.get_doc('Interview', d.name)
+ context = doc.as_dict()
+ message = frappe.render_template(interview_template.response, context)
+ recipients = get_recipients(doc.name)
+
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_template.subject,
+ message=message,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
+
+ doc.db_set('reminded', 1)
+
+
+def send_daily_feedback_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_feedback_reminder:
+ return
+
+ interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
+ interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
+
+ for entry in interviews:
+ recipients = get_recipients(entry.name, for_feedback=1)
+
+ doc = frappe.get_doc('Interview', entry.name)
+ context = doc.as_dict()
+
+ message = frappe.render_template(interview_feedback_template.response, context)
+
+ if len(recipients):
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_feedback_template.subject,
+ message=message,
+ reference_doctype='Interview',
+ reference_name=entry.name
+ )
+
+
+@frappe.whitelist()
+def get_expected_skill_set(interview_round):
+ return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
+
+
+@frappe.whitelist()
+def create_interview_feedback(data, interview_name, interviewer, job_applicant):
+ import json
+
+ from six import string_types
+
+ if isinstance(data, string_types):
+ data = frappe._dict(json.loads(data))
+
+ if frappe.session.user != interviewer:
+ frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
+
+ interview_feedback = frappe.new_doc('Interview Feedback')
+ interview_feedback.interview = interview_name
+ interview_feedback.interviewer = interviewer
+ interview_feedback.job_applicant = job_applicant
+
+ for d in data.skill_set:
+ d = frappe._dict(d)
+ interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
+
+ interview_feedback.feedback = data.feedback
+ interview_feedback.result = data.result
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
+ get_link_to_form('Interview Feedback', interview_feedback.name)))
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
+ filters = [
+ ['Has Role', 'parent', 'like', '%{}%'.format(txt)],
+ ['Has Role', 'role', '=', 'interviewer'],
+ ['Has Role', 'parenttype', '=', 'User']
+ ]
+
+ if filters and isinstance(filters, list):
+ filters.extend(filters)
+
+ return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
+ filters=filters, fields = ['parent'], as_list=1)
+
+
+@frappe.whitelist()
+def get_events(start, end, filters=None):
+ """Returns events for Gantt / Calendar view rendering.
+
+ :param start: Start date-time.
+ :param end: End date-time.
+ :param filters: Filters (JSON).
+ """
+ from frappe.desk.calendar import get_event_conditions
+
+ events = []
+
+ event_color = {
+ "Pending": "#fff4f0",
+ "Under Review": "#d3e8fc",
+ "Cleared": "#eaf5ed",
+ "Rejected": "#fce7e7"
+ }
+
+ conditions = get_event_conditions('Interview', filters)
+
+ interviews = frappe.db.sql("""
+ SELECT DISTINCT
+ `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
+ `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
+ `tabInterview`.to_time as to_time
+ from
+ `tabInterview`
+ where
+ (`tabInterview`.scheduled_on between %(start)s and %(end)s)
+ and docstatus != 2
+ {conditions}
+ """.format(conditions=conditions), {
+ "start": start,
+ "end": end
+ }, as_dict=True, update={"allDay": 0})
+
+ for d in interviews:
+ subject_data = []
+ for field in ["name", "job_applicant", "interview_round"]:
+ if not d.get(field):
+ continue
+ subject_data.append(d.get(field))
+
+ color = event_color.get(d.status)
+ interview_data = {
+ 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
+ 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
+ 'name': d.name,
+ 'subject': '\n'.join(subject_data),
+ 'color': color if color else "#89bcde"
+ }
+
+ events.append(interview_data)
+
+ return events
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview_calendar.js b/erpnext/hr/doctype/interview/interview_calendar.js
new file mode 100644
index 0000000..b46b72e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_calendar.js
@@ -0,0 +1,14 @@
+
+frappe.views.calendar['Interview'] = {
+ field_map: {
+ 'start': 'from',
+ 'end': 'to',
+ 'id': 'name',
+ 'title': 'subject',
+ 'allDay': 'allDay',
+ 'color': 'color'
+ },
+ order_by: 'scheduled_on',
+ gantt: true,
+ get_events_method: 'erpnext.hr.doctype.interview.interview.get_events'
+};
diff --git a/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
new file mode 100644
index 0000000..8d39fb5
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Feedback Reminder</h1>
+
+<p>
+ Interview Feedback for Interview {{ name }} is not submitted yet. Please submit your feedback. Thank you, good day!
+</p>
diff --git a/erpnext/hr/doctype/interview/interview_list.js b/erpnext/hr/doctype/interview/interview_list.js
new file mode 100644
index 0000000..b1f072f
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings['Interview'] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ let status_color = {
+ 'Pending': 'orange',
+ 'Under Review': 'blue',
+ 'Cleared': 'green',
+ 'Rejected': 'red',
+ };
+ return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+ }
+};
diff --git a/erpnext/hr/doctype/interview/interview_reminder_notification_template.html b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
new file mode 100644
index 0000000..76de46e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Reminder</h1>
+
+<p>
+ Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
+</p>
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
new file mode 100644
index 0000000..4612e17
--- /dev/null
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import datetime
+import os
+import unittest
+
+import frappe
+from frappe import _
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.utils import add_days, getdate, nowtime
+
+from erpnext.hr.doctype.designation.test_designation import create_designation
+from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterview(unittest.TestCase):
+ def test_validations_for_designation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
+ self.assertRaises(DuplicateInterviewRoundError, interview.save)
+
+ def test_notification_on_rescheduling(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
+
+ previous_scheduled_date = interview.scheduled_on
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+
+ interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
+ from_time=nowtime(), to_time=nowtime())
+ interview.reload()
+
+ self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
+
+ notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
+ self.assertIsNotNone(notification)
+
+ def test_notification_for_scheduling(self):
+ from erpnext.hr.doctype.interview.interview import send_interview_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = datetime.datetime.now() + datetime.timedelta(minutes=10)
+
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_interview_reminder()
+
+ interview.reload()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Reminder" in email_queue[0].message)
+
+ def test_notification_for_feedback_submission(self):
+ from erpnext.hr.doctype.interview.interview import send_daily_feedback_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = add_days(getdate(), -4)
+ create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_daily_feedback_reminder()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
+ if designation:
+ designation=create_designation(designation_name = "_Test_Sales_manager").name
+
+ interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
+ interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
+
+ interview_round = create_interview_round(
+ "Technical Round", ["Python", "JS"],
+ designation=designation, save=True
+ )
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round.name
+ interview.job_applicant = job_applicant
+ interview.scheduled_on = scheduled_on or getdate()
+ interview.from_time = from_time or nowtime()
+ interview.to_time = to_time or nowtime()
+
+ interview.append("interview_details", {"interviewer": interviewer_1.name})
+ interview.append("interview_details", {"interviewer": interviewer_2.name})
+
+ if save:
+ interview.save()
+
+ return interview
+
+def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
+ create_skill_set(skill_set)
+ interview_round = frappe.new_doc("Interview Round")
+ interview_round.round_name = name
+ interview_round.interview_type = create_interview_type()
+ interview_round.expected_average_rating = 4
+ if designation:
+ interview_round.designation = designation
+
+ for skill in skill_set:
+ interview_round.append("expected_skill_set", {"skill": skill})
+
+ for interviewer in interviewers:
+ interview_round.append("interviewer", {
+ "user": interviewer
+ })
+
+ if save:
+ interview_round.save()
+
+ return interview_round
+
+def create_skill_set(skill_set):
+ for skill in skill_set:
+ if not frappe.db.exists("Skill", skill):
+ doc = frappe.new_doc("Skill")
+ doc.skill_name = skill
+ doc.save()
+
+def create_interview_type(name="test_interview_type"):
+ if frappe.db.exists("Interview Type", name):
+ return frappe.get_doc("Interview Type", name).name
+ else:
+ doc = frappe.new_doc("Interview Type")
+ doc.name = name
+ doc.description = "_Test_Description"
+ doc.save()
+
+ return doc.name
+
+def setup_reminder_settings():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/hr/doctype/interview_detail/__init__.py b/erpnext/hr/doctype/interview_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/__init__.py
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.js b/erpnext/hr/doctype/interview_detail/interview_detail.js
new file mode 100644
index 0000000..88518ca
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Detail', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.json b/erpnext/hr/doctype/interview_detail/interview_detail.json
new file mode 100644
index 0000000..b5b49c0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.json
@@ -0,0 +1,74 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 16:24:10.382863",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interviewer",
+ "interview_feedback",
+ "average_rating",
+ "result",
+ "column_break_4",
+ "comments"
+ ],
+ "fields": [
+ {
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interviewer",
+ "options": "User"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_feedback",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interview Feedback",
+ "options": "Interview Feedback",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fetch_from": "interview_feedback.feedback",
+ "fieldname": "comments",
+ "fieldtype": "Text",
+ "label": "Comments",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-29 13:13:25.865063",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.py b/erpnext/hr/doctype/interview_detail/interview_detail.py
new file mode 100644
index 0000000..8be3d34
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewDetail(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_detail/test_interview_detail.py b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
new file mode 100644
index 0000000..a29dfff
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewDetail(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interview_feedback/__init__.py b/erpnext/hr/doctype/interview_feedback/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/__init__.py
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.js b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
new file mode 100644
index 0000000..dec559f
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
@@ -0,0 +1,54 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Feedback', {
+ onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['Interview'];
+
+ frm.set_query('interview', function() {
+ return {
+ filters: {
+ docstatus: ['!=', 2]
+ }
+ };
+ });
+ },
+
+ interview_round: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function(r) {
+ frm.set_value('skill_assessment', r.message);
+ }
+ });
+ },
+
+ interview: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview_feedback.interview_feedback.get_applicable_interviewers',
+ args: {
+ interview: frm.doc.interview || ''
+ },
+ callback: function(r) {
+ frm.set_query('interviewer', function() {
+ return {
+ filters: {
+ name: ['in', r.message]
+ }
+ };
+ });
+ }
+ });
+
+ },
+
+ interviewer: function(frm) {
+ if (!frm.doc.interview) {
+ frappe.throw(__('Select Interview first'));
+ frm.set_value('interviewer', '');
+ }
+ }
+});
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.json b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
new file mode 100644
index 0000000..6a2f7e8
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
@@ -0,0 +1,171 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-FEED-.####",
+ "creation": "2021-04-12 17:03:13.833285",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "details_section",
+ "interview",
+ "interview_round",
+ "job_applicant",
+ "column_break_3",
+ "interviewer",
+ "result",
+ "section_break_4",
+ "skill_assessment",
+ "average_rating",
+ "section_break_7",
+ "feedback",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interview",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview",
+ "options": "Interview",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fetch_from": "interview.interview_round",
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interviewer",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Skill Assessment"
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "skill_assessment",
+ "fieldtype": "Table",
+ "options": "Skill Assessment",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview Feedback",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "feedback",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview.job_applicant",
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-09-30 13:30:49.955352",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "interviewer",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
new file mode 100644
index 0000000..1c5a494
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt, get_link_to_form, getdate
+
+
+class InterviewFeedback(Document):
+ def validate(self):
+ self.validate_interviewer()
+ self.validate_interview_date()
+ self.validate_duplicate()
+ self.calculate_average_rating()
+
+ def on_submit(self):
+ self.update_interview_details()
+
+ def on_cancel(self):
+ self.update_interview_details()
+
+ def validate_interviewer(self):
+ applicable_interviewers = get_applicable_interviewers(self.interview)
+ if self.interviewer not in applicable_interviewers:
+ frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
+ frappe.bold(self.interviewer), frappe.bold(self.interview)))
+
+ def validate_interview_date(self):
+ scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
+
+ if getdate() < getdate(scheduled_date) and self.docstatus == 1:
+ frappe.throw(_('{0} submission before {1} is not allowed').format(
+ frappe.bold('Interview Feedback'),
+ frappe.bold('Interview Scheduled Date')
+ ))
+
+ def validate_duplicate(self):
+ duplicate_feedback = frappe.db.exists('Interview Feedback', {
+ 'interviewer': self.interviewer,
+ 'interview': self.interview,
+ 'docstatus': 1
+ })
+
+ if duplicate_feedback:
+ frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
+ self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
+
+ def calculate_average_rating(self):
+ total_rating = 0
+ for d in self.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
+
+ def update_interview_details(self):
+ doc = frappe.get_doc('Interview', self.interview)
+ total_rating = 0
+
+ if self.docstatus == 2:
+ for entry in doc.interview_details:
+ if entry.interview_feedback == self.name:
+ entry.average_rating = entry.interview_feedback = entry.comments = entry.result = None
+ break
+ else:
+ for entry in doc.interview_details:
+ if entry.interviewer == self.interviewer:
+ entry.average_rating = self.average_rating
+ entry.interview_feedback = self.name
+ entry.comments = self.feedback
+ entry.result = self.result
+
+ if entry.average_rating:
+ total_rating += entry.average_rating
+
+ doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
+ doc.save()
+ doc.notify_update()
+
+
+@frappe.whitelist()
+def get_applicable_interviewers(interview):
+ data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
+ return [d.interviewer for d in data]
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
new file mode 100644
index 0000000..c4b7981
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, flt, getdate
+
+from erpnext.hr.doctype.interview.test_interview import (
+ create_interview_and_dependencies,
+ create_skill_set,
+)
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterviewFeedback(unittest.TestCase):
+ def test_validation_for_skill_set(self):
+ frappe.set_user("Administrator")
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ interviewer = interview.interview_details[0].interviewer
+ create_skill_set(['Leadership'])
+
+ interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+ frappe.set_user(interviewer)
+
+ self.assertRaises(frappe.ValidationError, interview_feedback.save)
+
+ frappe.set_user("Administrator")
+
+ def test_average_ratings_on_feedback_submission_and_cancellation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ # For First Interviewer Feedback
+ interviewer = interview.interview_details[0].interviewer
+ frappe.set_user(interviewer)
+
+ # calculating Average
+ feedback_1 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+
+ total_rating = 0
+ for d in feedback_1.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
+
+ self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
+
+ avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
+ 'parent': feedback_1.interview,
+ 'interviewer': feedback_1.interviewer,
+ 'interview_feedback': feedback_1.name
+ }, 'average_rating')
+
+ # 1. average should be reflected in Interview Detail.
+ self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
+
+ '''For Second Interviewer Feedback'''
+ interviewer = interview.interview_details[1].interviewer
+ frappe.set_user(interviewer)
+
+ feedback_2 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview.reload()
+
+ feedback_2.cancel()
+ interview.reload()
+
+ frappe.set_user("Administrator")
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_feedback(interview, interviewer, skills_ratings):
+ interview_feedback = frappe.new_doc("Interview Feedback")
+ interview_feedback.interview = interview
+ interview_feedback.interviewer = interviewer
+ interview_feedback.result = "Cleared"
+
+ for rating in skills_ratings:
+ interview_feedback.append("skill_assessment", rating)
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ return interview_feedback
+
+
+def get_skills_rating(interview_round):
+ import random
+
+ skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
+ for d in skills:
+ d["rating"] = random.randint(1, 5)
+ return skills
diff --git a/erpnext/hr/doctype/interview_round/__init__.py b/erpnext/hr/doctype/interview_round/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/__init__.py
diff --git a/erpnext/hr/doctype/interview_round/interview_round.js b/erpnext/hr/doctype/interview_round/interview_round.js
new file mode 100644
index 0000000..6a608b0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Interview Round", {
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_interview(frm);
+ });
+ }
+ },
+ create_interview: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.interview_round.interview_round.create_interview",
+ args: {
+ doc: frm.doc
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/interview_round/interview_round.json b/erpnext/hr/doctype/interview_round/interview_round.json
new file mode 100644
index 0000000..9c95185
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.json
@@ -0,0 +1,118 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:round_name",
+ "creation": "2021-04-12 12:57:19.902866",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "round_name",
+ "interview_type",
+ "interviewers",
+ "column_break_3",
+ "designation",
+ "expected_average_rating",
+ "expected_skills_section",
+ "expected_skill_set"
+ ],
+ "fields": [
+ {
+ "fieldname": "round_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Round Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
+ },
+ {
+ "fieldname": "expected_skills_section",
+ "fieldtype": "Section Break",
+ "label": "Expected Skillset"
+ },
+ {
+ "fieldname": "expected_skill_set",
+ "fieldtype": "Table",
+ "options": "Expected Skill Set",
+ "reqd": 1
+ },
+ {
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "interview_type",
+ "fieldtype": "Link",
+ "label": "Interview Type",
+ "options": "Interview Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "interviewers",
+ "fieldtype": "Table MultiSelect",
+ "label": "Interviewers",
+ "options": "Interviewer"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-30 13:01:25.666660",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Round",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "select": 1,
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py
new file mode 100644
index 0000000..8230c78
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.model.document import Document
+
+
+class InterviewRound(Document):
+ pass
+
+@frappe.whitelist()
+def create_interview(doc):
+ if isinstance(doc, str):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = doc.name
+ interview.designation = doc.designation
+
+ if doc.interviewers:
+ interview.interview_details = []
+ for data in doc.interviewers:
+ interview.append("interview_details", {
+ "interviewer": data.user
+ })
+ return interview
+
+
+
diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py
new file mode 100644
index 0000000..932d3de
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/test_interview_round.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+# import frappe
+
+
+class TestInterviewRound(unittest.TestCase):
+ pass
+
diff --git a/erpnext/hr/doctype/interview_type/__init__.py b/erpnext/hr/doctype/interview_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/__init__.py
diff --git a/erpnext/hr/doctype/interview_type/interview_type.js b/erpnext/hr/doctype/interview_type/interview_type.js
new file mode 100644
index 0000000..af77b52
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Type', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_type/interview_type.json b/erpnext/hr/doctype/interview_type/interview_type.json
new file mode 100644
index 0000000..14636a1
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.json
@@ -0,0 +1,73 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-04-12 14:44:40.664034",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Round",
+ "link_fieldname": "interview_type"
+ }
+ ],
+ "modified": "2021-09-30 13:00:16.471518",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Type",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_type/interview_type.py b/erpnext/hr/doctype/interview_type/interview_type.py
new file mode 100644
index 0000000..ee5be54
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewType(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_type/test_interview_type.py b/erpnext/hr/doctype/interview_type/test_interview_type.py
new file mode 100644
index 0000000..a5d3cf9
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/test_interview_type.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewType(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interviewer/__init__.py b/erpnext/hr/doctype/interviewer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/__init__.py
diff --git a/erpnext/hr/doctype/interviewer/interviewer.json b/erpnext/hr/doctype/interviewer/interviewer.json
new file mode 100644
index 0000000..a37b8b0
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:38:19.354734",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-13 13:41:35.817568",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interviewer",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interviewer/interviewer.py b/erpnext/hr/doctype/interviewer/interviewer.py
new file mode 100644
index 0000000..1c8dbbe
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class Interviewer(Document):
+ pass
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js
index 7658bc9..d7b1c6c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.js
@@ -8,6 +8,24 @@
frappe.ui.form.on("Job Applicant", {
refresh: function(frm) {
+ frm.set_query("job_title", function() {
+ return {
+ filters: {
+ 'status': 'Open'
+ }
+ };
+ });
+ frm.events.create_custom_buttons(frm);
+ frm.events.make_dashboard(frm);
+ },
+
+ create_custom_buttons: function(frm) {
+ if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_dialog(frm);
+ });
+ }
+
if (!frm.doc.__islocal) {
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
$('[data-doctype="Employee Onboarding"]').find("button").show();
@@ -28,14 +46,57 @@
});
}
}
+ },
- frm.set_query("job_title", function() {
- return {
- filters: {
- 'status': 'Open'
- }
- };
+ make_dashboard: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.get_interview_details",
+ args: {
+ job_applicant: frm.doc.name
+ },
+ callback: function(r) {
+ $("div").remove(".form-dashboard-section.custom");
+ frm.dashboard.add_section(
+ frappe.render_template('job_applicant_dashboard', {
+ data: r.message
+ }),
+ __("Interview Summary")
+ );
+ }
});
+ },
+ create_dialog: function(frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Enter Interview Round',
+ fields: [
+ {
+ label: 'Interview Round',
+ fieldname: 'interview_round',
+ fieldtype: 'Link',
+ options: 'Interview Round'
+ },
+ ],
+ primary_action_label: 'Create Interview',
+ primary_action(values) {
+ frm.events.create_interview(frm, values);
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ create_interview: function (frm, values) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.create_interview",
+ args: {
+ doc: frm.doc,
+ interview_round: values.interview_round
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
}
});
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index bcea5f5..200f675 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -9,16 +9,20 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
+ "details_section",
"applicant_name",
"email_id",
"phone_number",
"country",
- "status",
"column_break_3",
"job_title",
+ "designation",
+ "status",
+ "source_and_rating_section",
"source",
"source_name",
"employee_referral",
+ "column_break_13",
"applicant_rating",
"section_break_6",
"notes",
@@ -84,7 +88,8 @@
},
{
"fieldname": "section_break_6",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Resume"
},
{
"fieldname": "cover_letter",
@@ -160,13 +165,34 @@
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fieldname": "source_and_rating_section",
+ "fieldtype": "Section Break",
+ "label": "Source and Rating"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "job_opening.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-24 15:51:11.117517",
+ "modified": "2021-09-29 23:06:10.904260",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 6971e5b..151f492 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -8,7 +8,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import comma_and, validate_email_address
+from frappe.utils import validate_email_address
+
+from erpnext.hr.doctype.interview.interview import get_interviewers
class DuplicationError(frappe.ValidationError): pass
@@ -26,7 +28,6 @@
self.name = " - ".join(keys)
def validate(self):
- self.check_email_id_is_unique()
if self.email_id:
validate_email_address(self.email_id, True)
@@ -44,11 +45,44 @@
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
+@frappe.whitelist()
+def create_interview(doc, interview_round):
+ import json
- def check_email_id_is_unique(self):
- if self.email_id:
- names = frappe.db.sql_list("""select name from `tabJob Applicant`
- where email_id=%s and name!=%s and job_title=%s""", (self.email_id, self.name, self.job_title))
+ from six import string_types
- if names:
- frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError)
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ round_designation = frappe.db.get_value("Interview Round", interview_round, "designation")
+
+ if round_designation and doc.designation and round_designation != doc.designation:
+ frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation))
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round
+ interview.job_applicant = doc.name
+ interview.designation = doc.designation
+ interview.resume_link = doc.resume_link
+ interview.job_opening = doc.job_title
+ interviewer_detail = get_interviewers(interview_round)
+
+ for d in interviewer_detail:
+ interview.append("interview_details", {
+ "interviewer": d.interviewer
+ })
+ return interview
+
+@frappe.whitelist()
+def get_interview_details(job_applicant):
+ interview_details = frappe.db.get_all("Interview",
+ filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]},
+ fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
+ )
+ interview_detail_map = {}
+
+ for detail in interview_details:
+ interview_detail_map[detail.name] = detail
+
+ return interview_detail_map
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
new file mode 100644
index 0000000..c286787
--- /dev/null
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
@@ -0,0 +1,44 @@
+
+{% if not jQuery.isEmptyObject(data) %}
+
+<table class="table table-bordered small">
+ <thead>
+ <tr>
+ <th style="width: 16%" class="text-left">{{ __("Interview") }}</th>
+ <th style="width: 16%" class="text-left">{{ __("Interview Round") }}</th>
+ <th style="width: 12%" class="text-left">{{ __("Status") }}</th>
+ <th style="width: 14%" class="text-left">{{ __("Expected Rating") }}</th>
+ <th style="width: 10%" class="text-left">{{ __("Rating") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for(const [key, value] of Object.entries(data)) { %}
+ <tr>
+ <td class="text-left"> {%= key %} </td>
+ <td class="text-left"> {%= value["interview_round"] %} </td>
+ <td class="text-left"> {%= value["status"] %} </td>
+ <td class="text-left">
+ {% for (i = 0; i < value["expected_average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ </td>
+ <td class="text-left">
+ {% if(value["average_rating"]){ %}
+ {% for (i = 0; i < value["average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ {% } %}
+ </td>
+ </tr>
+ {% } %}
+ </tbody>
+</table>
+{% else %}
+<p style="margin-top: 30px;"> No Interview has been scheduled.</p>
+{% endif %}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
index c005943..2f7795f 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
@@ -2,14 +2,17 @@
def get_data():
- return {
- 'fieldname': 'job_applicant',
- 'transactions': [
- {
- 'items': ['Employee', 'Employee Onboarding']
- },
- {
- 'items': ['Job Offer']
- },
- ],
- }
+ return {
+ 'fieldname': 'job_applicant',
+ 'transactions': [
+ {
+ 'items': ['Employee', 'Employee Onboarding']
+ },
+ {
+ 'items': ['Job Offer', 'Appointment Letter']
+ },
+ {
+ 'items': ['Interview']
+ }
+ ],
+ }
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index e583e25..8fc1290 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -7,7 +7,8 @@
import frappe
-# test_records = frappe.get_test_records('Job Applicant')
+from erpnext.hr.doctype.designation.test_designation import create_designation
+
class TestJobApplicant(unittest.TestCase):
pass
@@ -25,7 +26,8 @@
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
- "status": args.status or "Open"
+ "status": args.status or "Open",
+ "designation": create_designation().name
})
job_applicant.update(filters)
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py
index 3f3eca1..162b245 100644
--- a/erpnext/hr/doctype/job_offer/test_job_offer.py
+++ b/erpnext/hr/doctype/job_offer/test_job_offer.py
@@ -32,6 +32,7 @@
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
@@ -43,7 +44,11 @@
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
- self.assertEqual(job_applicant.status, "Rejected")
+ self.assertEquals(job_applicant.status, "Rejected")
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
+
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
def create_job_offer(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index d947641..9742387 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -1,14 +1,14 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.add_fetch('employee','employee_name','employee_name');
+cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
frappe.ui.form.on("Leave Allocation", {
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
- if(!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today());
+ if (!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today());
frm.set_query("employee", function() {
return {
@@ -25,9 +25,9 @@
},
refresh: function(frm) {
- if(frm.doc.docstatus === 1 && frm.doc.expired) {
+ if (frm.doc.docstatus === 1 && frm.doc.expired) {
var valid_expiry = moment(frappe.datetime.get_today()).isBetween(frm.doc.from_date, frm.doc.to_date);
- if(valid_expiry) {
+ if (valid_expiry) {
// expire current allocation
frm.add_custom_button(__('Expire Allocation'), function() {
frm.trigger("expire_allocation");
@@ -44,8 +44,8 @@
'expiry_date': frappe.datetime.get_today()
},
freeze: true,
- callback: function(r){
- if(!r.exc){
+ callback: function(r) {
+ if (!r.exc) {
frappe.msgprint(__("Allocation Expired!"));
}
frm.refresh();
@@ -77,8 +77,8 @@
},
leave_policy: function(frm) {
- if(frm.doc.leave_policy && frm.doc.leave_type) {
- frappe.db.get_value("Leave Policy Detail",{
+ if (frm.doc.leave_policy && frm.doc.leave_type) {
+ frappe.db.get_value("Leave Policy Detail", {
'parent': frm.doc.leave_policy,
'leave_type': frm.doc.leave_type
}, 'annual_allocation', (r) => {
@@ -91,13 +91,41 @@
return frappe.call({
method: "set_total_leaves_allocated",
doc: frm.doc,
- callback: function(r) {
+ callback: function() {
frm.refresh_fields();
}
- })
+ });
} else if (cint(frm.doc.carry_forward) == 0) {
frm.set_value("unused_leaves", 0);
frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated));
}
}
});
+
+frappe.tour["Leave Allocation"] = [
+ {
+ fieldname: "employee",
+ title: "Employee",
+ description: __("Select the Employee for which you want to allocate leaves.")
+ },
+ {
+ fieldname: "leave_type",
+ title: "Leave Type",
+ description: __("Select the Leave Type like Sick leave, Privilege Leave, Casual Leave, etc.")
+ },
+ {
+ fieldname: "from_date",
+ title: "From Date",
+ description: __("Select the date from which this Leave Allocation will be valid.")
+ },
+ {
+ fieldname: "to_date",
+ title: "To Date",
+ description: __("Select the date after which this Leave Allocation will expire.")
+ },
+ {
+ fieldname: "new_leaves_allocated",
+ title: "New Leaves Allocated",
+ description: __("Enter the number of leaves you want to allocate for the period.")
+ }
+];
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 3a6539e..52ee463 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -219,7 +219,8 @@
"fieldname": "leave_policy_assignment",
"fieldtype": "Link",
"label": "Leave Policy Assignment",
- "options": "Leave Policy Assignment"
+ "options": "Leave Policy Assignment",
+ "read_only": 1
},
{
"fetch_from": "employee.company",
@@ -236,7 +237,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-06-03 15:28:26.335104",
+ "modified": "2021-10-01 15:28:26.335104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 9ccb915..9e8cb55 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.add_fetch('employee','employee_name','employee_name');
-cur_frm.add_fetch('employee','company','company');
+cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
+cur_frm.add_fetch('employee', 'company', 'company');
frappe.ui.form.on("Leave Application", {
setup: function(frm) {
@@ -19,7 +19,6 @@
frm.set_query("employee", erpnext.queries.employee);
},
onload: function(frm) {
-
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
@@ -42,9 +41,9 @@
},
validate: function(frm) {
- if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){
+ if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1) {
frm.doc.half_day_date = frm.doc.from_date;
- }else if (frm.doc.half_day == 0){
+ } else if (frm.doc.half_day == 0) {
frm.doc.half_day_date = "";
}
frm.toggle_reqd("half_day_date", frm.doc.half_day == 1);
@@ -79,14 +78,14 @@
__("Allocated Leaves")
);
frm.dashboard.show();
- let allowed_leave_types = Object.keys(leave_details);
+ let allowed_leave_types = Object.keys(leave_details);
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
- frm.set_query('leave_type', function(){
+ frm.set_query('leave_type', function() {
return {
- filters : [
+ filters: [
['leave_type_name', 'in', allowed_leave_types]
]
};
@@ -99,7 +98,7 @@
frm.trigger("calculate_total_days");
}
cur_frm.set_intro("");
- if(frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
+ if (frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
frm.set_intro(__("Fill the form and save it"));
}
@@ -118,7 +117,7 @@
},
leave_approver: function(frm) {
- if(frm.doc.leave_approver){
+ if (frm.doc.leave_approver) {
frm.set_value("leave_approver_name", frappe.user.full_name(frm.doc.leave_approver));
}
},
@@ -131,12 +130,10 @@
if (frm.doc.half_day) {
if (frm.doc.from_date == frm.doc.to_date) {
frm.set_value("half_day_date", frm.doc.from_date);
- }
- else {
+ } else {
frm.trigger("half_day_datepicker");
}
- }
- else {
+ } else {
frm.set_value("half_day_date", "");
}
frm.trigger("calculate_total_days");
@@ -163,11 +160,11 @@
half_day_datepicker.update({
minDate: frappe.datetime.str_to_obj(frm.doc.from_date),
maxDate: frappe.datetime.str_to_obj(frm.doc.to_date)
- })
+ });
},
get_leave_balance: function(frm) {
- if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
+ if (frm.doc.docstatus === 0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
return frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: {
@@ -177,11 +174,10 @@
leave_type: frm.doc.leave_type,
consider_all_leaves_in_the_allocation_period: true
},
- callback: function(r) {
+ callback: function (r) {
if (!r.exc && r.message) {
frm.set_value('leave_balance', r.message);
- }
- else {
+ } else {
frm.set_value('leave_balance', "0");
}
}
@@ -190,12 +186,12 @@
},
calculate_total_days: function(frm) {
- if(frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
+ if (frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
var from_date = Date.parse(frm.doc.from_date);
var to_date = Date.parse(frm.doc.to_date);
- if(to_date < from_date){
+ if (to_date < from_date) {
frappe.msgprint(__("To Date cannot be less than From Date"));
frm.set_value('to_date', '');
return;
@@ -222,7 +218,7 @@
},
set_leave_approver: function(frm) {
- if(frm.doc.employee) {
+ if (frm.doc.employee) {
// server call is done to include holidays in leave days calculations
return frappe.call({
method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver',
@@ -238,3 +234,36 @@
}
}
});
+
+frappe.tour["Leave Application"] = [
+ {
+ fieldname: "employee",
+ title: "Employee",
+ description: __("Select the Employee.")
+ },
+ {
+ fieldname: "leave_type",
+ title: "Leave Type",
+ description: __("Select type of leave the employee wants to apply for, like Sick Leave, Privilege Leave, Casual Leave, etc.")
+ },
+ {
+ fieldname: "from_date",
+ title: "From Date",
+ description: __("Select the start date for your Leave Application.")
+ },
+ {
+ fieldname: "to_date",
+ title: "To Date",
+ description: __("Select the end date for your Leave Application.")
+ },
+ {
+ fieldname: "half_day",
+ title: "Half Day",
+ description: __("To apply for a Half Day check 'Half Day' and select the Half Day Date")
+ },
+ {
+ fieldname: "leave_approver",
+ title: "Leave Approver",
+ description: __("Select your Leave Approver i.e. the person who approves or rejects your leaves.")
+ }
+];
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 9e6fc6d..349ed7a 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -76,6 +76,7 @@
# notify leave applier about approval
if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
self.notify_employee()
+
self.create_leave_ledger_entry()
self.reload()
@@ -108,7 +109,13 @@
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
- if allowed_role not in frappe.get_roles():
+ user = frappe.get_doc("User", frappe.session.user)
+ user_roles = [d.role for d in user.roles]
+ if not allowed_role:
+ frappe.throw(_("Backdated Leave Application is restricted. Please set the {} in {}").format(
+ frappe.bold("Role Allowed to Create Backdated Leave Application"), get_link_to_form("HR Settings", "HR Settings")))
+
+ if (allowed_role and allowed_role not in user_roles):
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index b9c785a..629b20e 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -121,6 +121,7 @@
application = self.get_application(_test_records[0])
application.insert()
+ application.reload()
application.status = "Approved"
self.assertRaises(LeaveDayBlockedError, application.submit)
diff --git a/erpnext/hr/doctype/leave_type/leave_type.js b/erpnext/hr/doctype/leave_type/leave_type.js
index 8622309..b930ded 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.js
+++ b/erpnext/hr/doctype/leave_type/leave_type.js
@@ -2,3 +2,37 @@
refresh: function(frm) {
}
});
+
+
+frappe.tour["Leave Type"] = [
+ {
+ fieldname: "max_leaves_allowed",
+ title: "Maximum Leave Allocation Allowed",
+ description: __("This field allows you to set the maximum number of leaves that can be allocated annually for this Leave Type while creating the Leave Policy")
+ },
+ {
+ fieldname: "max_continuous_days_allowed",
+ title: "Maximum Consecutive Leaves Allowed",
+ description: __("This field allows you to set the maximum number of consecutive leaves an Employee can apply for.")
+ },
+ {
+ fieldname: "is_optional_leave",
+ title: "Is Optional Leave",
+ description: __("Optional Leaves are holidays that Employees can choose to avail from a list of holidays published by the company.")
+ },
+ {
+ fieldname: "is_compensatory",
+ title: "Is Compensatory Leave",
+ description: __("Leaves you can avail against a holiday you worked on. You can claim Compensatory Off Leave using Compensatory Leave request. Click") + " <a href='https://docs.erpnext.com/docs/v13/user/manual/en/human-resources/compensatory-leave-request' target='_blank'>here</a> " + __('to know more')
+ },
+ {
+ fieldname: "allow_encashment",
+ title: "Allow Encashment",
+ description: __("From here, you can enable encashment for the balance leaves.")
+ },
+ {
+ fieldname: "is_earned_leave",
+ title: "Is Earned Leaves",
+ description: __("Earned Leaves are leaves earned by an Employee after working with the company for a certain amount of time. Enabling this will allocate leaves on pro-rata basis by automatically updating Leave Allocation for leaves of this type at intervals set by 'Earned Leave Frequency.")
+ }
+];
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 8f2ae6e..06ca4cd 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -50,7 +50,7 @@
{
"fieldname": "max_leaves_allowed",
"fieldtype": "Int",
- "label": "Max Leaves Allowed"
+ "label": "Maximum Leave Allocation Allowed"
},
{
"fieldname": "applicable_after",
@@ -61,7 +61,7 @@
"fieldname": "max_continuous_days_allowed",
"fieldtype": "Int",
"in_list_view": 1,
- "label": "Maximum Continuous Days Applicable",
+ "label": "Maximum Consecutive Leaves Allowed",
"oldfieldname": "max_days_allowed",
"oldfieldtype": "Data"
},
@@ -87,6 +87,7 @@
},
{
"default": "0",
+ "description": "These leaves are holidays permitted by the company however, availing it is optional for an Employee.",
"fieldname": "is_optional_leave",
"fieldtype": "Check",
"label": "Is Optional Leave"
@@ -205,6 +206,7 @@
},
{
"depends_on": "eval:doc.is_ppl == 1",
+ "description": "For a day of leave taken, if you still pay (say) 50% of the daily salary, then enter 0.50 in this field.",
"fieldname": "fraction_of_daily_salary_per_leave",
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
@@ -214,7 +216,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2021-08-12 16:10:36.464690",
+ "modified": "2021-10-02 11:59:40.503359",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
diff --git a/erpnext/hr/doctype/skill_assessment/__init__.py b/erpnext/hr/doctype/skill_assessment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/__init__.py
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.json b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
new file mode 100644
index 0000000..8b935c4
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:07:39.656289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "rating"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Rating",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 17:18:14.032298",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Skill Assessment",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.py b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
new file mode 100644
index 0000000..3b74c4e
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SkillAssessment(Document):
+ pass
diff --git a/erpnext/hr/module_onboarding/human_resource/human_resource.json b/erpnext/hr/module_onboarding/human_resource/human_resource.json
index 518c002..cd11bd1 100644
--- a/erpnext/hr/module_onboarding/human_resource/human_resource.json
+++ b/erpnext/hr/module_onboarding/human_resource/human_resource.json
@@ -13,17 +13,14 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/human-resources",
"idx": 0,
"is_complete": 0,
- "modified": "2020-07-08 14:05:47.018799",
+ "modified": "2021-05-19 05:32:01.794628",
"modified_by": "Administrator",
"module": "HR",
"name": "Human Resource",
"owner": "Administrator",
"steps": [
{
- "step": "Create Department"
- },
- {
- "step": "Create Designation"
+ "step": "HR Settings"
},
{
"step": "Create Holiday list"
@@ -32,6 +29,9 @@
"step": "Create Employee"
},
{
+ "step": "Data import"
+ },
+ {
"step": "Create Leave Type"
},
{
@@ -39,9 +39,6 @@
},
{
"step": "Create Leave Application"
- },
- {
- "step": "HR Settings"
}
],
"subtitle": "Employee, Leaves, and more.",
diff --git a/erpnext/hr/onboarding_step/create_employee/create_employee.json b/erpnext/hr/onboarding_step/create_employee/create_employee.json
index 3aa33c6..4782818 100644
--- a/erpnext/hr/onboarding_step/create_employee/create_employee.json
+++ b/erpnext/hr/onboarding_step/create_employee/create_employee.json
@@ -1,18 +1,20 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Show Tour",
"creation": "2020-05-14 11:43:25.561152",
+ "description": "<h3>Employee</h3>\n\nAn individual who works and is recognized for his rights and duties in your company is your Employee. You can manage the Employee master. It captures the demographic, personal and professional details, joining and leave details, etc.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 12:26:28.629074",
+ "modified": "2021-05-19 04:50:02.240321",
"modified_by": "Administrator",
"name": "Create Employee",
"owner": "Administrator",
"reference_document": "Employee",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create Employee",
"validate_action": 0
diff --git a/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
index 32472b4..a08e85f 100644
--- a/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
+++ b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
@@ -1,18 +1,20 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Show Tour",
"creation": "2020-05-28 11:47:34.700174",
+ "description": "<h3>Holiday List.</h3>\n\nHoliday List is a list which contains the dates of holidays. Most organizations have a standard Holiday List for their employees. However, some of them may have different holiday lists based on different Locations or Departments. In ERPNext, you can configure multiple Holiday Lists.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 12:25:38.068582",
+ "modified": "2021-05-19 04:19:52.305199",
"modified_by": "Administrator",
"name": "Create Holiday list",
"owner": "Administrator",
"reference_document": "Holiday List",
+ "show_form_tour": 0,
"show_full_form": 1,
"title": "Create Holiday List",
"validate_action": 0
diff --git a/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
index fa9941e..0b0ce3f 100644
--- a/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
+++ b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
@@ -1,18 +1,20 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Show Tour",
"creation": "2020-05-14 11:48:56.123718",
+ "description": "<h3>Leave Allocation</h3>\n\nLeave Allocation enables you to allocate a specific number of leaves of a particular type to an Employee so that, an employee will be able to create a Leave Application only if Leaves are allocated. ",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 11:48:56.123718",
+ "modified": "2021-05-19 04:22:34.220238",
"modified_by": "Administrator",
"name": "Create Leave Allocation",
"owner": "Administrator",
"reference_document": "Leave Allocation",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create Leave Allocation",
"validate_action": 0
diff --git a/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
index 1ed074e..af63aa5 100644
--- a/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
+++ b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
@@ -1,18 +1,20 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Show Tour",
"creation": "2020-05-14 11:49:45.400764",
+ "description": "<h3>Leave Application</h3>\n\nLeave Application is a formal document created by an Employee to apply for Leaves for a particular time period based on there leave allocation and leave type according to there need.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 11:49:45.400764",
+ "modified": "2021-05-19 04:39:09.893474",
"modified_by": "Administrator",
"name": "Create Leave Application",
"owner": "Administrator",
"reference_document": "Leave Application",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create Leave Application",
"validate_action": 0
diff --git a/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
index 8cbfc5c..397f5cd 100644
--- a/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
+++ b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
@@ -1,18 +1,20 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Show Tour",
"creation": "2020-05-27 11:17:31.119312",
+ "description": "<h3>Leave Type</h3>\n\nLeave type is defined based on many factors and features like encashment, earned leaves, partially paid, without pay and, a lot more. To check other options and to define your leave type click on Show Tour.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-20 11:17:31.119312",
+ "modified": "2021-05-19 04:32:48.135406",
"modified_by": "Administrator",
"name": "Create Leave Type",
"owner": "Administrator",
"reference_document": "Leave Type",
+ "show_form_tour": 0,
"show_full_form": 1,
"title": "Create Leave Type",
"validate_action": 0
diff --git a/erpnext/hr/onboarding_step/data_import/data_import.json b/erpnext/hr/onboarding_step/data_import/data_import.json
new file mode 100644
index 0000000..ac343c6
--- /dev/null
+++ b/erpnext/hr/onboarding_step/data_import/data_import.json
@@ -0,0 +1,21 @@
+{
+ "action": "Watch Video",
+ "action_label": "",
+ "creation": "2021-05-19 05:29:16.809610",
+ "description": "<h3>Data Import</h3>\n\nData import is the tool to migrate your existing data like Employee, Customer, Supplier, and a lot more to our ERPNext system.\nGo through the video for a detailed explanation of this tool.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-05-19 05:29:16.809610",
+ "modified_by": "Administrator",
+ "name": "Data import",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Data Import",
+ "validate_action": 1,
+ "video_url": "https://www.youtube.com/watch?v=DQyqeurPI64"
+}
\ No newline at end of file
diff --git a/erpnext/hr/onboarding_step/hr_settings/hr_settings.json b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
index 0a1d0ba..355664f 100644
--- a/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
+++ b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
@@ -1,18 +1,20 @@
{
- "action": "Update Settings",
+ "action": "Show Form Tour",
+ "action_label": "Explore",
"creation": "2020-05-28 13:13:52.427711",
+ "description": "<h3>HR Settings</h3>\n\nHr Settings consists of major settings related to Employee Lifecycle, Leave Management, etc. Click on Explore, to explore Hr Settings.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
- "modified": "2020-05-20 11:16:42.430974",
+ "modified": "2021-05-18 07:02:05.747548",
"modified_by": "Administrator",
"name": "HR Settings",
"owner": "Administrator",
"reference_document": "HR Settings",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "HR Settings",
"validate_action": 0
diff --git a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
index 8ccef6a..afe273f 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
+++ b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
@@ -89,13 +89,14 @@
"width": "160px"
},
{
+ "allow_on_submit": 1,
"columns": 2,
+ "default": "Pending",
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
- "options": "Pending\nPartially Completed\nFully Completed",
- "read_only": 1
+ "options": "Pending\nPartially Completed\nFully Completed"
},
{
"fieldname": "column_break_3",
@@ -125,10 +126,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-27 16:07:25.905015",
+ "modified": "2021-09-16 21:25:22.506485",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 8a92413..5f5c20a 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -215,7 +215,32 @@
label: __('Qty To Manufacture'),
fieldname: 'qty',
reqd: 1,
- default: 1
+ default: 1,
+ onchange: () => {
+ const { quantity, items: rm } = frm.doc;
+ const variant_items_map = rm.reduce((acc, item) => {
+ acc[item.item_code] = item.qty;
+ return acc;
+ }, {});
+ const mf_qty = cur_dialog.fields_list.filter(
+ (f) => f.df.fieldname === "qty"
+ )[0]?.value;
+ const items = cur_dialog.fields.filter(
+ (f) => f.fieldname === "items"
+ )[0]?.data;
+
+ if (!items) {
+ return;
+ }
+
+ items.forEach((item) => {
+ item.qty =
+ (variant_items_map[item.item_code] * mf_qty) /
+ quantity;
+ });
+
+ cur_dialog.refresh();
+ }
});
}
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 232e3a0..7cfec97 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1133,7 +1133,8 @@
query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"):
- query_filters["is_stock_item"] = 1
+ or_cond_filters["is_stock_item"] = 1
+ or_cond_filters["has_variants"] = 1
return frappe.get_list("Item",
fields = fields, filters=query_filters,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index cee796e..5dfd6be 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -309,3 +309,9 @@
erpnext.patches.v14_0.update_opportunity_currency_fields
erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
+erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
+erpnext.patches.v13_0.create_custom_field_for_finance_book
+erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
+erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
+erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
+erpnext.patches.v13_0.add_default_interview_notification_templates
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
new file mode 100644
index 0000000..5e8a27f
--- /dev/null
+++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py
new file mode 100644
index 0000000..313b0e9
--- /dev/null
+++ b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py
@@ -0,0 +1,21 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_field = {
+ 'Finance Book': [
+ {
+ 'fieldname': 'for_income_tax',
+ 'label': 'For Income Tax',
+ 'fieldtype': 'Check',
+ 'insert_after': 'finance_book_name',
+ 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
+ }
+ ]
+ }
+ create_custom_fields(custom_field, update=1)
diff --git a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py
new file mode 100644
index 0000000..aeb8d8e
--- /dev/null
+++ b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py
@@ -0,0 +1,76 @@
+from typing import List, NewType
+
+import frappe
+
+StockEntryCode = NewType("StockEntryCode", str)
+
+
+def execute():
+ stock_entry_codes = find_broken_stock_entries()
+
+ for stock_entry_code in stock_entry_codes:
+ patched_stock_entry = patch_additional_cost(stock_entry_code)
+ create_repost_item_valuation(patched_stock_entry)
+
+
+def find_broken_stock_entries() -> List[StockEntryCode]:
+ period_closing_date = frappe.db.get_value(
+ "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
+ )
+
+ stock_entries_to_patch = frappe.db.sql(
+ """
+ select se.name, sum(sed.additional_cost) as item_additional_cost, se.total_additional_costs
+ from `tabStock Entry` se
+ join `tabStock Entry Detail` sed
+ on sed.parent = se.name
+ where
+ se.docstatus = 1 and
+ se.posting_date > %s
+ group by
+ sed.parent
+ having
+ item_additional_cost != se.total_additional_costs
+ """,
+ period_closing_date,
+ as_dict=True,
+ )
+
+ return [d.name for d in stock_entries_to_patch]
+
+
+def patch_additional_cost(code: StockEntryCode):
+ stock_entry = frappe.get_doc("Stock Entry", code)
+ stock_entry.distribute_additional_costs()
+ stock_entry.update_valuation_rate()
+ stock_entry.set_total_incoming_outgoing_value()
+ stock_entry.set_total_amount()
+ stock_entry.db_update()
+ for item in stock_entry.items:
+ item.db_update()
+ return stock_entry
+
+
+def create_repost_item_valuation(stock_entry):
+ from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
+
+ # turn on recalculate flag so reposting corrects the incoming/outgoing rates.
+ frappe.db.set_value(
+ "Stock Ledger Entry",
+ {"voucher_no": stock_entry.name, "actual_qty": (">", 0)},
+ "recalculate_rate",
+ 1,
+ update_modified=False,
+ )
+
+ create_repost_item_valuation_entry(
+ args=frappe._dict(
+ {
+ "posting_date": stock_entry.posting_date,
+ "posting_time": stock_entry.posting_time,
+ "voucher_type": stock_entry.doctype,
+ "voucher_no": stock_entry.name,
+ "company": stock_entry.company,
+ }
+ )
+ )
diff --git a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
new file mode 100644
index 0000000..fa8a864
--- /dev/null
+++ b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+
+
+def execute():
+ frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance')
+ frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance')
+
+ purchase_invoices = frappe.db.sql("""
+ select
+ parenttype as type, parent as name
+ from
+ `tabPurchase Invoice Advance`
+ where
+ ref_exchange_rate = 1
+ and docstatus = 1
+ and ifnull(exchange_gain_loss, '') != ''
+ group by
+ parent
+ """, as_dict=1)
+
+ sales_invoices = frappe.db.sql("""
+ select
+ parenttype as type, parent as name
+ from
+ `tabSales Invoice Advance`
+ where
+ ref_exchange_rate = 1
+ and docstatus = 1
+ and ifnull(exchange_gain_loss, '') != ''
+ group by
+ parent
+ """, as_dict=1)
+
+ if purchase_invoices + sales_invoices:
+ frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
+
+ for invoice in purchase_invoices + sales_invoices:
+ doc = frappe.get_doc(invoice.type, invoice.name)
+ doc.docstatus = 2
+ doc.make_gl_entries()
+ for advance in doc.advances:
+ if advance.ref_exchange_rate == 1:
+ advance.db_set('exchange_gain_loss', 0, False)
+ doc.docstatus = 1
+ doc.make_gl_entries()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
new file mode 100644
index 0000000..9887ad9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
@@ -0,0 +1,10 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail")
+ frappe.db.sql("""
+ UPDATE `tabMaintenance Schedule Detail`
+ SET completion_status = 'Pending'
+ WHERE docstatus < 2
+ """)
diff --git a/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py
new file mode 100644
index 0000000..fd48c0d
--- /dev/null
+++ b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+from erpnext.regional.india.setup import create_custom_fields, get_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice': get_custom_fields().get('Sales Invoice')
+ }
+
+ create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index bff36a4..9ed6686 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -171,8 +171,6 @@
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
- self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
-
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 2a8870b..df970f3 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -144,6 +144,9 @@
if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
+ def on_trash(self):
+ frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
+
def update_percent_complete(self):
if self.percent_complete_method == "Manual":
if self.status == "Completed":
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index ebc1326..c64ac8d 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -9,6 +9,8 @@
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.task.test_task import create_task
+from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
@@ -96,6 +98,21 @@
self.assertEqual(len(tasks), 2)
+ def test_project_linking_with_sales_order(self):
+ so = make_sales_order()
+ project = make_project_from_so(so.name)
+
+ project.save()
+ self.assertEqual(project.sales_order, so.name)
+
+ so.reload()
+ self.assertEqual(so.project, project.name)
+
+ project.delete()
+
+ so.reload()
+ self.assertFalse(so.project)
+
def get_project(name, template):
project = frappe.get_doc(dict(
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 91b1247..63fd8a1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -345,26 +345,14 @@
}
scan_barcode() {
- let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
-
- let show_description = function(idx, exist = null) {
- if (exist) {
- frappe.show_alert({
- message: __('Row #{0}: Qty increased by 1', [idx]),
- indicator: 'green'
- });
- } else {
- frappe.show_alert({
- message: __('Row #{0}: Item added', [idx]),
- indicator: 'green'
- });
- }
- }
+ let me = this;
if(this.frm.doc.scan_barcode) {
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
- args: { search_value: this.frm.doc.scan_barcode }
+ args: {
+ search_value: this.frm.doc.scan_barcode
+ }
}).then(r => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
@@ -375,49 +363,96 @@
return;
}
- let cur_grid = this.frm.fields_dict.items.grid;
-
- let row_to_modify = null;
- const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
- const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
-
- if (existing_item_row) {
- row_to_modify = existing_item_row;
- } else if (blank_item_row) {
- row_to_modify = blank_item_row;
- }
-
- if (!row_to_modify) {
- // add new row
- row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
- }
-
- show_description(row_to_modify.idx, row_to_modify.item_code);
-
- this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
- frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
- item_code: data.item_code,
- qty: (row_to_modify.qty || 0) + 1
- });
-
- ['serial_no', 'batch_no', 'barcode'].forEach(field => {
- if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
-
- let value = (row_to_modify[field] && field === "serial_no")
- ? row_to_modify[field] + '\n' + data[field] : data[field];
-
- frappe.model.set_value(row_to_modify.doctype,
- row_to_modify.name, field, value);
- }
- });
-
- scan_barcode_field.set_value('');
- refresh_field("items");
+ me.modify_table_after_scan(data);
});
}
return false;
}
+ modify_table_after_scan(data) {
+ let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
+ let cur_grid = this.frm.fields_dict.items.grid;
+ let row_to_modify = null;
+
+ // Check if batch is scanned and table has batch no field
+ let batch_no_scan = Boolean(data.batch_no) && frappe.meta.has_field(cur_grid.doctype, "batch_no");
+
+ if (batch_no_scan) {
+ row_to_modify = this.get_batch_row_to_modify(data.batch_no);
+ } else {
+ // serial or barcode scan
+ row_to_modify = this.get_row_to_modify_on_scan(row_to_modify, data);
+ }
+
+ if (!row_to_modify) {
+ // add new row if new item/batch is scanned
+ row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
+ }
+
+ this.show_scan_message(row_to_modify.idx, row_to_modify.item_code);
+ this.set_scanned_values(row_to_modify, data, scan_barcode_field);
+ }
+
+ set_scanned_values(row_to_modify, data, scan_barcode_field) {
+ // increase qty and set scanned value and item in row
+ this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
+ item_code: data.item_code,
+ qty: (row_to_modify.qty || 0) + 1
+ });
+
+ ['serial_no', 'batch_no', 'barcode'].forEach(field => {
+ if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
+ let is_serial_no = row_to_modify[field] && field === "serial_no";
+ let value = data[field];
+
+ if (is_serial_no) {
+ value = row_to_modify[field] + '\n' + data[field];
+ }
+
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, field, value);
+ }
+ });
+
+ scan_barcode_field.set_value('');
+ refresh_field("items");
+ }
+
+ get_row_to_modify_on_scan(row_to_modify, data) {
+ // get an existing item row to increment or blank row to modify
+ const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
+ const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
+
+ if (existing_item_row) {
+ row_to_modify = existing_item_row;
+ } else if (blank_item_row) {
+ row_to_modify = blank_item_row;
+ }
+
+ return row_to_modify;
+ }
+
+ get_batch_row_to_modify(batch_no) {
+ // get row if batch already exists in table
+ const existing_batch_row = this.frm.doc.items.find(d => d.batch_no === batch_no);
+ return existing_batch_row || null;
+ }
+
+ show_scan_message (idx, exist = null) {
+ // show new row or qty increase toast
+ if (exist) {
+ frappe.show_alert({
+ message: __('Row #{0}: Qty increased by 1', [idx]),
+ indicator: 'green'
+ });
+ } else {
+ frappe.show_alert({
+ message: __('Row #{0}: Item added', [idx]),
+ indicator: 'green'
+ });
+ }
+ }
+
apply_default_taxes() {
var me = this;
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
@@ -617,6 +652,7 @@
me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse)
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
+ me.apply_price_list(item, true);
}, undefined, !frappe.flags.hide_serial_batch_dialog);
}
},
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index 6286732..7b35819 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -63,7 +63,7 @@
});
node.parent.append(node_card);
- node.$link = $(`#${node.id}`);
+ node.$link = $(`[id="${node.id}"]`);
}
show() {
@@ -223,7 +223,7 @@
let node = undefined;
$.each(r.message, (_i, data) => {
- if ($(`#${data.id}`).length)
+ if ($(`[id="${data.id}"]`).length)
return;
node = new me.Node({
@@ -263,7 +263,7 @@
this.refresh_connectors(node.parent_id);
// rebuild incoming connections
- let grandparent = $(`#${node.parent_id}`).attr('data-parent');
+ let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent);
}
@@ -282,7 +282,7 @@
show_active_path(node) {
// mark node parent on active path
- $(`#${node.parent_id}`).addClass('active-path');
+ $(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node, deep=false) {
@@ -317,7 +317,7 @@
render_child_nodes(node, child_nodes) {
const last_level = this.$hierarchy.find('.level:last').index();
- const current_level = $(`#${node.id}`).parent().parent().parent().index();
+ const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === current_level) {
this.$hierarchy.append(`
@@ -382,7 +382,7 @@
node.$children = $('<ul class="node-children"></ul>');
const last_level = this.$hierarchy.find('.level:last').index();
- const node_level = $(`#${node.id}`).parent().parent().parent().index();
+ const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === node_level) {
this.$hierarchy.append(`
@@ -489,7 +489,7 @@
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
- const parent = $(`#${parent_id}`);
+ const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -513,7 +513,7 @@
}
collapse_previous_level_nodes(node) {
- let node_parent = $(`#${node.parent_id}`);
+ let node_parent = $(`[id="${node.parent_id}"]`);
let previous_level_nodes = node_parent.parent().parent().children('li');
let node_card = undefined;
@@ -545,7 +545,7 @@
setup_node_click_action(node) {
let me = this;
- let node_element = $(`#${node.id}`);
+ let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
const is_sibling = me.selected_node.parent_id === node.parent_id;
@@ -563,7 +563,7 @@
}
setup_edit_node_action(node) {
- let node_element = $(`#${node.id}`);
+ let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -572,7 +572,7 @@
}
remove_levels_after_node(node) {
- let level = $(`#${node.id}`).parent().parent().parent().index();
+ let level = $(`[id="${node.id}"]`).parent().parent().parent().index();
level = $('.hierarchy > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -595,7 +595,7 @@
const parent = $(path).data('parent');
const child = $(path).data('child');
- if ($(`#${parent}`).length && $(`#${child}`).length)
+ if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
index b1a8879..0a8ba78 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
@@ -54,7 +54,7 @@
});
node.parent.append(node_card);
- node.$link = $(`#${node.id}`);
+ node.$link = $(`[id="${node.id}"]`);
node.$link.addClass('mobile-node');
}
@@ -184,7 +184,7 @@
this.refresh_connectors(node.parent_id, node.id);
// rebuild incoming connections of parent
- let grandparent = $(`#${node.parent_id}`).attr('data-parent');
+ let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent, node.parent_id);
}
@@ -221,7 +221,7 @@
show_active_path(node) {
// mark node parent on active path
- $(`#${node.parent_id}`).addClass('active-path');
+ $(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node) {
@@ -256,7 +256,7 @@
if (child_nodes) {
$.each(child_nodes, (_i, data) => {
this.add_node(node, data);
- $(`#${data.id}`).addClass('active-child');
+ $(`[id="${data.id}"]`).addClass('active-child');
setTimeout(() => {
this.add_connector(node.id, data.id);
@@ -293,9 +293,9 @@
let connector = undefined;
- if ($(`#${parent_id}`).hasClass('active')) {
+ if ($(`[id="${parent_id}"]`).hasClass('active')) {
connector = this.get_connector_for_active_node(parent_node, child_node);
- } else if ($(`#${parent_id}`).hasClass('active-path')) {
+ } else if ($(`[id="${parent_id}"]`).hasClass('active-path')) {
connector = this.get_connector_for_collapsed_node(parent_node, child_node);
}
@@ -351,7 +351,7 @@
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
- const parent = $(`#${parent_id}`);
+ const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -374,7 +374,7 @@
setup_node_click_action(node) {
let me = this;
- let node_element = $(`#${node.id}`);
+ let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
let el = undefined;
@@ -398,7 +398,7 @@
}
setup_edit_node_action(node) {
- let node_element = $(`#${node.id}`);
+ let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -512,7 +512,7 @@
}
remove_levels_after_node(node) {
- let level = $(`#${node.id}`).parent().parent().index();
+ let level = $(`[id="${node.id}"]`).parent().parent().index();
level = $('.hierarchy-mobile > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -533,7 +533,7 @@
const parent = $(path).data('parent');
const child = $(path).data('child');
- if ($(`#${parent}`).length && $(`#${child}`).length)
+ if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 490a7c4..fef1e76 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -31,6 +31,14 @@
.carousel-control-prev,
.carousel-control-next {
opacity: 1;
+ width: 8%;
+
+ @media (max-width: 1200px) {
+ width: 10%;
+ }
+ @media (max-width: 768px) {
+ width: 15%;
+ }
}
.carousel-body {
@@ -43,6 +51,8 @@
.carousel-content {
max-width: 400px;
+ margin-left: 5rem;
+ margin-right: 5rem;
}
.card {
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 633064c..afb1b07 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -132,6 +132,10 @@
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
+ custom_fields = get_custom_fields()
+ create_custom_fields(custom_fields, update=update)
+
+def get_custom_fields():
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
allow_on_submit=1, print_hide=1, fetch_if_empty=1)
@@ -165,12 +169,12 @@
dict(fieldname='gst_category', label='GST Category',
fieldtype='Select', insert_after='gst_section', print_hide=1,
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
- fetch_from='customer.gst_category', fetch_if_empty=1),
+ fetch_from='customer.gst_category', fetch_if_empty=1, length=25),
dict(fieldname='export_type', label='Export Type',
fieldtype='Select', insert_after='gst_category', print_hide=1,
depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type',
- fetch_if_empty=1),
+ fetch_if_empty=1, length=25),
]
delivery_note_gst_category = [
@@ -181,18 +185,18 @@
]
invoice_gst_fields = [
- dict(fieldname='invoice_copy', label='Invoice Copy',
+ dict(fieldname='invoice_copy', label='Invoice Copy', length=30,
fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1,
options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'),
- dict(fieldname='reverse_charge', label='Reverse Charge',
+ dict(fieldname='reverse_charge', label='Reverse Charge', length=2,
fieldtype='Select', insert_after='invoice_copy', print_hide=1,
options='Y\nN', default='N'),
- dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN',
+ dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', length=15,
fieldtype='Data', insert_after='export_type', print_hide=1),
dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'),
dict(fieldname='reason_for_issuing_document', label='Reason For Issuing document',
fieldtype='Select', insert_after='gst_col_break', print_hide=1,
- depends_on='eval:doc.is_return==1',
+ depends_on='eval:doc.is_return==1', length=45,
options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others')
]
@@ -230,25 +234,25 @@
sales_invoice_gst_fields = [
dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
fieldtype='Data', insert_after='customer_address', read_only=1,
- fetch_from='customer_address.gstin', print_hide=1),
+ fetch_from='customer_address.gstin', print_hide=1, length=15),
dict(fieldname='customer_gstin', label='Customer GSTIN',
fieldtype='Data', insert_after='shipping_address_name',
- fetch_from='shipping_address_name.gstin', print_hide=1),
+ fetch_from='shipping_address_name.gstin', print_hide=1, length=15),
dict(fieldname='place_of_supply', label='Place of Supply',
fieldtype='Data', insert_after='customer_gstin',
- print_hide=1, read_only=1),
+ print_hide=1, read_only=1, length=50),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', insert_after='company_address',
- fetch_from='company_address.gstin', print_hide=1, read_only=1),
+ fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15),
]
sales_invoice_shipping_fields = [
dict(fieldname='port_code', label='Port Code',
fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1,
- depends_on="eval:doc.gst_category=='Overseas' "),
+ depends_on="eval:doc.gst_category=='Overseas' ", length=15),
dict(fieldname='shipping_bill_number', label=' Shipping Bill Number',
fieldtype='Data', insert_after='port_code', print_hide=1,
- depends_on="eval:doc.gst_category=='Overseas' "),
+ depends_on="eval:doc.gst_category=='Overseas' ", length=50),
dict(fieldname='shipping_bill_date', label='Shipping Bill Date',
fieldtype='Date', insert_after='shipping_bill_number', print_hide=1,
depends_on="eval:doc.gst_category=='Overseas' "),
@@ -354,7 +358,8 @@
'insert_after': 'transporter',
'fetch_from': 'transporter.gst_transporter_id',
'print_hide': 1,
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 20
},
{
'fieldname': 'driver',
@@ -370,7 +375,8 @@
'fieldtype': 'Data',
'insert_after': 'driver',
'print_hide': 1,
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 30
},
{
'fieldname': 'vehicle_no',
@@ -378,7 +384,8 @@
'fieldtype': 'Data',
'insert_after': 'lr_no',
'print_hide': 1,
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 10
},
{
'fieldname': 'distance',
@@ -395,7 +402,7 @@
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
- 'fieldtype': 'Data',
+ 'fieldtype': 'Small Text',
'insert_after': 'transporter_col_break',
'fetch_from': 'transporter.name',
'read_only': 1,
@@ -409,12 +416,13 @@
'options': '\nRoad\nAir\nRail\nShip',
'insert_after': 'transporter_name',
'print_hide': 1,
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 5
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
- 'fieldtype': 'Data',
+ 'fieldtype': 'Small Text',
'insert_after': 'mode_of_transport',
'fetch_from': 'driver.full_name',
'print_hide': 1,
@@ -437,7 +445,8 @@
'default': 'Regular',
'insert_after': 'lr_date',
'print_hide': 1,
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 30
},
{
'fieldname': 'ewaybill',
@@ -446,7 +455,8 @@
'depends_on': 'eval:(doc.docstatus === 1)',
'allow_on_submit': 1,
'insert_after': 'tax_id',
- 'translatable': 0
+ 'translatable': 0,
+ 'length': 20
}
]
@@ -663,9 +673,19 @@
'fieldtype': 'Data',
'insert_after': 'email'
}
+ ],
+ 'Finance Book': [
+ {
+ 'fieldname': 'for_income_tax',
+ 'label': 'For Income Tax',
+ 'fieldtype': 'Check',
+ 'insert_after': 'finance_book_name',
+ 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
+ }
]
}
- create_custom_fields(custom_fields, update=update)
+
+ return custom_fields
def make_fixtures(company=None):
docs = []
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 091cc884..9493614 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -249,6 +249,9 @@
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
destination_gstin = party_details.supplier_gstin
+ if not destination_gstin or party_details.gstin:
+ return False
+
if party_details.gstin == destination_gstin:
return True
else:
@@ -857,12 +860,13 @@
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
if depreciable_value == asset.gross_purchase_amount:
- # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
- diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
- if diff <= 180:
- rate_of_depreciation = rate_of_depreciation / 2
- frappe.msgprint(
- _('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
+ if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'):
+ # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
+ diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
+ if diff <= 180:
+ rate_of_depreciation = rate_of_depreciation / 2
+ frappe.msgprint(
+ _('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 85282ca..7c7ed9a 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1480,6 +1480,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1512,7 +1513,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-01 15:12:24.115483",
+ "modified": "2021-09-28 13:09:51.515542",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index 6dd0fb1..55d5ec8 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -62,6 +62,13 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 907967c..c473395 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -264,16 +264,26 @@
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html"))
- records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,
'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}]
- records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,
'subject': _("Leave Status Notification"), 'owner': frappe.session.user}]
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response,
+ 'subject': _('Interview Reminder'), 'owner': frappe.session.user}]
+
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
+ 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
+
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
- records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,
'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}]
# Records for the Supplier Scorecard
@@ -317,6 +327,14 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
+
hr_settings.save()
def update_item_variant_settings():
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
index 1e3d0d0..e560f4a 100644
--- a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
+++ b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
@@ -1,7 +1,7 @@
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
- 'text-centre': align == 'Center',
+ 'text-centre': align == 'Centre',
'text-left': align == 'Left',
}) -%}
@@ -15,7 +15,7 @@
<div class="carousel-body container d-flex {{ align_class }}">
<div class="carousel-content align-self-center">
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
- {%- if subtitle -%}<p class="text-muted mt-2">{{ subtitle }}</p>{%- endif -%}
+ {%- if subtitle -%}<p class="{{ heading_class }} mt-2">{{ subtitle }}</p>{%- endif -%}
{%- if action -%}
<a href="{{ action }}" class="btn btn-primary mt-3">
{{ label }}
@@ -27,12 +27,14 @@
</div>
{%- endmacro -%}
-<div id="{{ slider_name }}" class="section-carousel carousel slide" data-ride="carousel">
+{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}
+
+<div id="{{ hero_slider_id }}" class="section-carousel carousel slide" data-ride="carousel">
{%- if show_indicators -%}
<ol class="carousel-indicators">
{%- for index in ['1', '2', '3', '4', '5'] -%}
{%- if values['slide_' + index + '_image'] -%}
- <li data-target="#{{ slider_name }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
+ <li data-target="#{{ hero_slider_id }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
{%- endif -%}
{%- endfor -%}
</ol>
@@ -54,7 +56,7 @@
{%- endfor -%}
</div>
{%- if show_controls -%}
- <a class="carousel-control-prev" href="#{{ slider_name }}" role="button" data-slide="prev">
+ <a class="carousel-control-prev" href="#{{ hero_slider_id }}" role="button" data-slide="prev">
<div class="carousel-control">
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
@@ -62,7 +64,7 @@
</div>
<span class="sr-only">Previous</span>
</a>
- <a class="carousel-control-next" href="#{{ slider_name }}" role="button" data-slide="next">
+ <a class="carousel-control-next" href="#{{ hero_slider_id }}" role="button" data-slide="next">
<div class="carousel-control">
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
@@ -73,13 +75,12 @@
{%- endif -%}
</div>
-<script type="text/javascript">
- $('.carousel').carousel({
- interval: false,
- pause: "hover",
- wrap: true
- })
+<script>
+ frappe.ready(function () {
+ $('.carousel').carousel({
+ interval: false,
+ pause: "hover",
+ wrap: true
+ })
+ });
</script>
-
-<style>
-</style>
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index fdc8763..9bf142c 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1277,6 +1277,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1308,7 +1309,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-08-27 20:14:40.215231",
+ "modified": "2021-09-28 13:10:09.761714",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index 8665986..805286d 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -36,7 +36,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Qty"
+ "label": "Qty",
+ "reqd": 1
},
{
"fieldname": "picked_qty",
@@ -180,7 +181,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-06-24 17:18:57.357120",
+ "modified": "2021-09-28 12:02:16.923056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 1a59734..112dded 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -1140,6 +1140,7 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1149,7 +1150,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2021-08-17 20:16:40.849885",
+ "modified": "2021-09-28 13:11:10.181328",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 62b3a6a..d86e52f 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form, now, today
+from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today
from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
@@ -126,6 +126,9 @@
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def repost_entries():
+ if not in_configured_timeslot():
+ return
+
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
@@ -144,3 +147,26 @@
WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
""", now(), as_dict=1)
+
+
+def in_configured_timeslot(repost_settings=None, current_time=None):
+ """Check if current time is in configured timeslot for reposting."""
+
+ if repost_settings is None:
+ repost_settings = frappe.get_cached_doc("Stock Reposting Settings")
+
+ if not repost_settings.limit_reposting_timeslot:
+ return True
+
+ if get_weekday() == repost_settings.limits_dont_apply_on:
+ return True
+
+ start_time = repost_settings.start_time
+ end_time = repost_settings.end_time
+
+ now_time = current_time or nowtime()
+
+ if start_time < end_time:
+ return end_time >= now_time >= start_time
+ else:
+ return now_time >= start_time or now_time <= end_time
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index c70a9ec..c086f93 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -1,11 +1,72 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-# import frappe
import unittest
+import frappe
+
+from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
+ in_configured_timeslot,
+)
+
class TestRepostItemValuation(unittest.TestCase):
- pass
+ def test_repost_time_slot(self):
+ repost_settings = frappe.get_doc("Stock Reposting Settings")
+
+ positive_cases = [
+ {"limit_reposting_timeslot": 0},
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "18:00:00",
+ "end_time": "09:00:00",
+ "current_time": "20:00:00",
+ },
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "09:00:00",
+ "end_time": "18:00:00",
+ "current_time": "12:00:00",
+ },
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "23:00:00",
+ "end_time": "09:00:00",
+ "current_time": "2:00:00",
+ },
+ ]
+
+ for case in positive_cases:
+ repost_settings.update(case)
+ self.assertTrue(
+ in_configured_timeslot(repost_settings, case.get("current_time")),
+ msg=f"Exepcted true from : {case}",
+ )
+
+ negative_cases = [
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "18:00:00",
+ "end_time": "09:00:00",
+ "current_time": "09:01:00",
+ },
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "09:00:00",
+ "end_time": "18:00:00",
+ "current_time": "19:00:00",
+ },
+ {
+ "limit_reposting_timeslot": 1,
+ "start_time": "23:00:00",
+ "end_time": "09:00:00",
+ "current_time": "22:00:00",
+ },
+ ]
+
+ for case in negative_cases:
+ repost_settings.update(case)
+ self.assertFalse(
+ in_configured_timeslot(repost_settings, case.get("current_time")),
+ msg=f"Exepcted false from : {case}",
+ )
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 7cb9665..157904b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -548,44 +548,7 @@
calculate_basic_amount: function(frm, item) {
item.basic_amount = flt(flt(item.transfer_qty) * flt(item.basic_rate),
precision("basic_amount", item));
-
- frm.events.calculate_amount(frm);
- },
-
- calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
- let total_basic_amount = 0;
- if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
- total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) {
- return i.is_finished_item ? flt(i.basic_amount) : 0;
- })
- );
- } else {
- total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) {
- return i.t_warehouse ? flt(i.basic_amount) : 0;
- })
- );
- }
- for (let i in frm.doc.items) {
- let item = frm.doc.items[i];
-
- if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
- item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
- } else {
- item.additional_cost = 0;
- }
-
- item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
-
- if (flt(item.transfer_qty)) {
- item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
- precision("valuation_rate", item));
- }
- }
-
- refresh_field('items');
},
calculate_total_additional_costs: function(frm) {
@@ -781,11 +744,6 @@
amount: function(frm, cdt, cdn) {
frm.events.set_base_amount(frm, cdt, cdn);
- // Adding this check because same table in used in LCV
- // This causes an error if you try to post an LCV immediately after a Stock Entry
- if (frm.doc.doctype == 'Stock Entry') {
- frm.events.calculate_amount(frm);
- }
},
expense_account: function(frm, cdt, cdn) {
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 1c9b961..bd7d22b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -555,22 +555,27 @@
def distribute_additional_costs(self):
# If no incoming items, set additional costs blank
- if not any([d.item_code for d in self.items if d.t_warehouse]):
+ if not any(d.item_code for d in self.items if d.t_warehouse):
self.additional_costs = []
- self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")])
+ self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
if self.purpose in ("Repack", "Manufacture"):
- incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
+ incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
- incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+ incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
- if incoming_items_cost:
- for d in self.get("items"):
- if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
- d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
- else:
- d.additional_cost = 0
+ if not incoming_items_cost:
+ return
+
+ for d in self.get("items"):
+ if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
+ d.additional_cost = 0
+ continue
+ elif not d.t_warehouse:
+ d.additional_cost = 0
+ continue
+ d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
def update_valuation_rate(self):
for d in self.get("items"):
@@ -805,7 +810,11 @@
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
- total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+ if self.purpose in ("Repack", "Manufacture"):
+ total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
+ else:
+ total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
+
divide_based_on = total_basic_amount
if self.get("additional_costs") and not total_basic_amount:
@@ -816,20 +825,24 @@
for t in self.get("additional_costs"):
for d in self.get("items"):
- if d.t_warehouse:
- item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
- item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
- "amount": 0.0,
- "base_amount": 0.0
- })
+ if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
+ continue
+ elif not d.t_warehouse:
+ continue
- multiply_based_on = d.basic_amount if total_basic_amount else d.qty
+ item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
+ item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
+ "amount": 0.0,
+ "base_amount": 0.0
+ })
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
- flt(t.amount * multiply_based_on) / divide_based_on
+ multiply_based_on = d.basic_amount if total_basic_amount else d.qty
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
- flt(t.base_amount * multiply_based_on) / divide_based_on
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
+ flt(t.amount * multiply_based_on) / divide_based_on
+
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
+ flt(t.base_amount * multiply_based_on) / divide_based_on
if item_account_wise_additional_cost:
for d in self.get("items"):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 46619eb..c9d0af5 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -837,6 +837,39 @@
frappe.db.set_default("allow_negative_stock", 0)
+ def test_additional_cost_distribution_manufacture(self):
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RM", basic_amount=10),
+ frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
+ frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X")
+ ],
+ )
+
+ se.distribute_additional_costs()
+
+ distributed_costs = [d.additional_cost for d in se.items]
+ self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
+
+ def test_additional_cost_distribution_non_manufacture(self):
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Material Receipt",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
+ frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X")
+ ],
+ )
+
+ se.distribute_additional_costs()
+
+ distributed_costs = [d.additional_cost for d in se.items]
+ self.assertEqual([40.0, 60.0], distributed_costs)
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
diff --git a/erpnext/stock/doctype/stock_reposting_settings/__init__.py b/erpnext/stock/doctype/stock_reposting_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/__init__.py
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
new file mode 100644
index 0000000..42d0723
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Stock Reposting Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
new file mode 100644
index 0000000..2474059
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -0,0 +1,72 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-10-01 10:56:30.814787",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "scheduling_section",
+ "limit_reposting_timeslot",
+ "start_time",
+ "end_time",
+ "limits_dont_apply_on"
+ ],
+ "fields": [
+ {
+ "fieldname": "scheduling_section",
+ "fieldtype": "Section Break",
+ "label": "Scheduling"
+ },
+ {
+ "depends_on": "limit_reposting_timeslot",
+ "fieldname": "start_time",
+ "fieldtype": "Time",
+ "label": "Start Time",
+ "mandatory_depends_on": "limit_reposting_timeslot"
+ },
+ {
+ "depends_on": "limit_reposting_timeslot",
+ "fieldname": "end_time",
+ "fieldtype": "Time",
+ "label": "End Time",
+ "mandatory_depends_on": "limit_reposting_timeslot"
+ },
+ {
+ "depends_on": "limit_reposting_timeslot",
+ "fieldname": "limits_dont_apply_on",
+ "fieldtype": "Select",
+ "label": "Limits don't apply on",
+ "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday"
+ },
+ {
+ "default": "0",
+ "fieldname": "limit_reposting_timeslot",
+ "fieldtype": "Check",
+ "label": "Limit timeslot for Stock Reposting"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-10-01 11:27:28.981594",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Reposting Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
new file mode 100644
index 0000000..bab521d
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from frappe.model.document import Document
+from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
+
+
+class StockRepostingSettings(Document):
+
+
+ def validate(self):
+ self.set_minimum_reposting_time_slot()
+
+ def set_minimum_reposting_time_slot(self):
+ """Ensure that timeslot for reposting is at least 12 hours."""
+ if not self.limit_reposting_timeslot:
+ return
+
+ start_time = get_datetime(self.start_time)
+ end_time = get_datetime(self.end_time)
+
+ if start_time > end_time:
+ end_time = add_to_date(end_time, days=1, as_datetime=True)
+
+ diff = time_diff_in_hours(end_time, start_time)
+
+ if diff < 10:
+ self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
new file mode 100644
index 0000000..fad74d3
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestStockRepostingSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/templates/print_formats/includes/total.html b/erpnext/templates/print_formats/includes/total.html
index 8179980..879203b 100644
--- a/erpnext/templates/print_formats/includes/total.html
+++ b/erpnext/templates/print_formats/includes/total.html
@@ -7,7 +7,7 @@
</div>
{% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
- <label>{{ _(doc.meta.get_label('total')) }}</label></div>
+ <label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right">
{{ doc.get_formatted("total", doc) }}
</div>
diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py
index 76c7608..9c8c371 100644
--- a/erpnext/tests/ui_test_helpers.py
+++ b/erpnext/tests/ui_test_helpers.py
@@ -7,6 +7,8 @@
create_company()
create_missing_designation()
+ frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'")
+
emp1 = create_employee('Test Employee 1', 'CEO')
emp2 = create_employee('Test Employee 2', 'CTO')
emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1)