Merge branch 'develop' into putaway
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index 9a6c389..65c5ff1 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -2,7 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension', {
-
refresh: function(frm) {
frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list;
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index f888d9e..52e9ff8 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -203,7 +203,7 @@
return all_dimensions
@frappe.whitelist()
-def get_dimension_filters():
+def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type
FROM `tabAccounting Dimension`
@@ -214,6 +214,18 @@
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1)
+ if with_cost_center_and_project:
+ dimension_filters.extend([
+ {
+ 'fieldname': 'cost_center',
+ 'document_type': 'Cost Center'
+ },
+ {
+ 'fieldname': 'project',
+ 'document_type': 'Project'
+ }
+ ])
+
default_dimensions_map = {}
for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {})
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index 104880f..fc1d7e3 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -11,37 +11,7 @@
class TestAccountingDimension(unittest.TestCase):
def setUp(self):
- frappe.set_user("Administrator")
-
- if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- dimension = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Department",
- }).insert()
- else:
- dimension1 = frappe.get_doc("Accounting Dimension", "Department")
- dimension1.disabled = 0
- dimension1.save()
-
- if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
- dimension1 = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Location",
- })
-
- dimension1.append("dimension_defaults", {
- "company": "_Test Company",
- "reference_document": "Location",
- "default_dimension": "Block 1",
- "mandatory_for_bs": 1
- })
-
- dimension1.insert()
- dimension1.save()
- else:
- dimension1 = frappe.get_doc("Accounting Dimension", "Location")
- dimension1.disabled = 0
- dimension1.save()
+ create_dimension()
def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1)
@@ -101,6 +71,38 @@
def tearDown(self):
disable_dimension()
+def create_dimension():
+ frappe.set_user("Administrator")
+
+ if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
+ frappe.get_doc({
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ }).insert()
+ else:
+ dimension = frappe.get_doc("Accounting Dimension", "Department")
+ dimension.disabled = 0
+ dimension.save()
+
+ if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
+ dimension1 = frappe.get_doc({
+ "doctype": "Accounting Dimension",
+ "document_type": "Location",
+ })
+
+ dimension1.append("dimension_defaults", {
+ "company": "_Test Company",
+ "reference_document": "Location",
+ "default_dimension": "Block 1",
+ "mandatory_for_bs": 1
+ })
+
+ dimension1.insert()
+ dimension1.save()
+ else:
+ dimension1 = frappe.get_doc("Accounting Dimension", "Location")
+ dimension1.disabled = 0
+ dimension1.save()
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py b/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
new file mode 100644
index 0000000..74b7b51
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -0,0 +1,82 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Accounting Dimension Filter', {
+ refresh: function(frm, cdt, cdn) {
+ if (frm.doc.accounting_dimension) {
+ frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
+ }
+
+ let help_content =
+ `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ <tr><td>
+ <p>
+ <i class="fa fa-hand-right"></i>
+ {{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
+ </p>
+ </td></tr>
+ </table>`;
+
+ frm.set_df_property('dimension_filter_help', 'options', help_content);
+ },
+ onload: function(frm) {
+ frm.set_query('applicable_on_account', 'accounts', function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
+
+ frappe.db.get_list('Accounting Dimension',
+ {fields: ['document_type']}).then((res) => {
+ let options = ['Cost Center', 'Project'];
+
+ res.forEach((dimension) => {
+ options.push(dimension.document_type);
+ });
+
+ frm.set_df_property('accounting_dimension', 'options', options);
+ });
+
+ frm.trigger('setup_filters');
+ },
+
+ setup_filters: function(frm) {
+ let filters = {};
+
+ if (frm.doc.accounting_dimension) {
+ frappe.model.with_doctype(frm.doc.accounting_dimension, function() {
+ if (frappe.model.is_tree(frm.doc.accounting_dimension)) {
+ filters['is_group'] = 0;
+ }
+
+ if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) {
+ filters['company'] = frm.doc.company;
+ }
+
+ frm.set_query('dimension_value', 'dimensions', function() {
+ return {
+ filters: filters
+ };
+ });
+ });
+ }
+ },
+
+ accounting_dimension: function(frm) {
+ frm.clear_table("dimensions");
+ let row = frm.add_child("dimensions");
+ row.accounting_dimension = frm.doc.accounting_dimension;
+ frm.refresh_field("dimensions");
+ frm.trigger('setup_filters');
+ },
+});
+
+frappe.ui.form.on('Allowed Dimension', {
+ dimensions_add: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ row.accounting_dimension = frm.doc.accounting_dimension;
+ frm.refresh_field("dimensions");
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
new file mode 100644
index 0000000..c0327ad
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
@@ -0,0 +1,134 @@
+{
+ "actions": [],
+ "autoname": "format:{accounting_dimension}-{#####}",
+ "creation": "2020-11-08 18:28:11.906146",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "accounting_dimension",
+ "disabled",
+ "column_break_2",
+ "company",
+ "allow_or_restrict",
+ "section_break_4",
+ "accounts",
+ "column_break_6",
+ "dimensions",
+ "section_break_10",
+ "dimension_filter_help"
+ ],
+ "fields": [
+ {
+ "fieldname": "accounting_dimension",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Accounting Dimension",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "hide_border": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "allow_or_restrict",
+ "fieldtype": "Select",
+ "label": "Allow Or Restrict Dimension",
+ "options": "Allow\nRestrict",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "accounts",
+ "fieldtype": "Table",
+ "label": "Applicable On Account",
+ "options": "Applicable On Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval:doc.accounting_dimension",
+ "fieldname": "dimensions",
+ "fieldtype": "Table",
+ "label": "Applicable Dimension",
+ "options": "Allowed Dimension",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dimension_filter_help",
+ "fieldtype": "HTML",
+ "label": "Dimension Filter Help",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-16 15:27:23.659285",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Accounting Dimension Filter",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
new file mode 100644
index 0000000..6aef9ca
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _, scrub
+from frappe.model.document import Document
+
+class AccountingDimensionFilter(Document):
+ def validate(self):
+ self.validate_applicable_accounts()
+
+ def validate_applicable_accounts(self):
+ accounts = frappe.db.sql(
+ """
+ SELECT a.applicable_on_account as account
+ FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d
+ WHERE d.name = a.parent
+ and d.name != %s
+ and d.accounting_dimension = %s
+ """, (self.name, self.accounting_dimension), as_dict=1)
+
+ account_list = [d.account for d in accounts]
+
+ for account in self.get('accounts'):
+ if account.applicable_on_account in account_list:
+ frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
+ account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
+
+def get_dimension_filter_map():
+ filters = frappe.db.sql("""
+ SELECT
+ a.applicable_on_account, d.dimension_value, p.accounting_dimension,
+ p.allow_or_restrict, a.is_mandatory
+ FROM
+ `tabApplicable On Account` a, `tabAllowed Dimension` d,
+ `tabAccounting Dimension Filter` p
+ WHERE
+ p.name = a.parent
+ AND p.disabled = 0
+ AND p.name = d.parent
+ """, as_dict=1)
+
+ dimension_filter_map = {}
+
+ for f in filters:
+ f.fieldname = scrub(f.accounting_dimension)
+
+ build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
+ f.allow_or_restrict, f.is_mandatory)
+
+ return dimension_filter_map
+
+def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
+ map_object.setdefault((dimension, account), {
+ 'allowed_dimensions': [],
+ 'is_mandatory': is_mandatory,
+ 'allow_or_restrict': allow_or_restrict
+ })
+ map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
new file mode 100644
index 0000000..7877abd
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
+from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
+
+class TestAccountingDimensionFilter(unittest.TestCase):
+ def setUp(self):
+ create_dimension()
+ create_accounting_dimension_filter()
+ self.invoice_list = []
+
+ def test_allowed_dimension_validation(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.items[0].cost_center = 'Main - _TC'
+ si.department = 'Accounts - _TC'
+ si.location = 'Block 1'
+ si.save()
+
+ self.assertRaises(InvalidAccountDimensionError, si.submit)
+ self.invoice_list.append(si)
+
+ def test_mandatory_dimension_validation(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.department = ''
+ si.location = 'Block 1'
+
+ # Test with no department for Sales Account
+ si.items[0].department = ''
+ si.items[0].cost_center = '_Test Cost Center 2 - _TC'
+ si.save()
+
+ self.assertRaises(MandatoryAccountDimensionError, si.submit)
+ self.invoice_list.append(si)
+
+ def tearDown(self):
+ disable_dimension_filter()
+ disable_dimension()
+
+ for si in self.invoice_list:
+ si.load_from_db()
+ if si.docstatus == 1:
+ si.cancel()
+
+def create_accounting_dimension_filter():
+ if not frappe.db.get_value('Accounting Dimension Filter',
+ {'accounting_dimension': 'Cost Center'}):
+ frappe.get_doc({
+ 'doctype': 'Accounting Dimension Filter',
+ 'accounting_dimension': 'Cost Center',
+ 'allow_or_restrict': 'Allow',
+ 'company': '_Test Company',
+ 'accounts': [{
+ 'applicable_on_account': 'Sales - _TC',
+ }],
+ 'dimensions': [{
+ 'accounting_dimension': 'Cost Center',
+ 'dimension_value': '_Test Cost Center 2 - _TC'
+ }]
+ }).insert()
+ else:
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc.disabled = 0
+ doc.save()
+
+ if not frappe.db.get_value('Accounting Dimension Filter',
+ {'accounting_dimension': 'Department'}):
+ frappe.get_doc({
+ 'doctype': 'Accounting Dimension Filter',
+ 'accounting_dimension': 'Department',
+ 'allow_or_restrict': 'Allow',
+ 'company': '_Test Company',
+ 'accounts': [{
+ 'applicable_on_account': 'Sales - _TC',
+ 'is_mandatory': 1
+ }],
+ 'dimensions': [{
+ 'accounting_dimension': 'Department',
+ 'dimension_value': 'Accounts - _TC'
+ }]
+ }).insert()
+ else:
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc.disabled = 0
+ doc.save()
+
+def disable_dimension_filter():
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc.disabled = 1
+ doc.save()
+
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc.disabled = 1
+ doc.save()
diff --git a/erpnext/accounts/doctype/allowed_dimension/__init__.py b/erpnext/accounts/doctype/allowed_dimension/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/allowed_dimension/__init__.py
diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json
new file mode 100644
index 0000000..7fe2a3c
--- /dev/null
+++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2020-11-08 18:22:36.001131",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "accounting_dimension",
+ "dimension_value"
+ ],
+ "fields": [
+ {
+ "fieldname": "accounting_dimension",
+ "fieldtype": "Link",
+ "label": "Accounting Dimension",
+ "options": "DocType",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dimension_value",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "options": "accounting_dimension",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-23 09:56:19.744200",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Allowed Dimension",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py
new file mode 100644
index 0000000..c2afc1a
--- /dev/null
+++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, 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 AllowedDimension(Document):
+ pass
diff --git a/erpnext/accounts/doctype/applicable_on_account/__init__.py b/erpnext/accounts/doctype/applicable_on_account/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/applicable_on_account/__init__.py
diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json
new file mode 100644
index 0000000..95e98d0
--- /dev/null
+++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json
@@ -0,0 +1,46 @@
+{
+ "actions": [],
+ "creation": "2020-11-08 18:20:00.944449",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "applicable_on_account",
+ "is_mandatory"
+ ],
+ "fields": [
+ {
+ "fieldname": "applicable_on_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Accounts",
+ "options": "Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "columns": 2,
+ "default": "0",
+ "fieldname": "is_mandatory",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-22 19:55:13.324136",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Applicable On Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py
new file mode 100644
index 0000000..0fccaf3
--- /dev/null
+++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, 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 ApplicableOnAccount(Document):
+ pass
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index de9498e..49b2b18 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -1,5 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', {
onload: function(frm) {
@@ -20,7 +21,12 @@
frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm);
}
- },
+ if (frm.doc.plaid_access_token) {
+ frm.add_custom_button(__('Refresh Plaid Link'), () => {
+ new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
+ });
+ }
+ }
});
@@ -40,4 +46,79 @@
frm.doc.name).options = options;
frm.fields_dict.bank_transaction_mapping.grid.refresh();
-};
\ No newline at end of file
+};
+
+erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
+ constructor(access_token) {
+ this.access_token = access_token;
+ this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
+ this.init_config();
+ }
+
+ async init_config() {
+ this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
+ this.token = await this.get_link_token_for_update();
+ this.init_plaid();
+ }
+
+ async get_link_token_for_update() {
+ const token = frappe.xcall(
+ 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
+ { access_token: this.access_token }
+ )
+ if (!token) {
+ frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
+ }
+ return token;
+ }
+
+ init_plaid() {
+ const me = this;
+ me.loadScript(me.plaidUrl)
+ .then(() => {
+ me.onScriptLoaded(me);
+ })
+ .then(() => {
+ if (me.linkHandler) {
+ me.linkHandler.open();
+ }
+ })
+ .catch((error) => {
+ me.onScriptError(error);
+ });
+ }
+
+ loadScript(src) {
+ return new Promise(function (resolve, reject) {
+ if (document.querySelector("script[src='" + src + "']")) {
+ resolve();
+ return;
+ }
+ const el = document.createElement('script');
+ el.type = 'text/javascript';
+ el.async = true;
+ el.src = src;
+ el.addEventListener('load', resolve);
+ el.addEventListener('error', reject);
+ el.addEventListener('abort', reject);
+ document.head.appendChild(el);
+ });
+ }
+
+ onScriptLoaded(me) {
+ me.linkHandler = Plaid.create({
+ env: me.plaid_env,
+ token: me.token,
+ onSuccess: me.plaid_success
+ });
+ }
+
+ onScriptError(error) {
+ frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
+ console.log(error);
+ }
+
+ plaid_success(token, response) {
+ frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+ }
+};
diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js
index cadf1e7..e162e32 100644
--- a/erpnext/accounts/doctype/budget/budget.js
+++ b/erpnext/accounts/doctype/budget/budget.js
@@ -1,24 +1,9 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', {
onload: function(frm) {
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
-
- frm.set_query("project", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
-
frm.set_query("account", "accounts", function() {
return {
filters: {
@@ -26,16 +11,18 @@
report_type: "Profit and Loss",
is_group: 0
}
- }
- })
-
+ };
+ });
+
frm.set_query("monthly_distribution", function() {
return {
filters: {
fiscal_year: frm.doc.fiscal_year
}
- }
- })
+ };
+ });
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index cd88b11..c5ec23c 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -122,8 +122,10 @@
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate())
+ "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit)
@@ -147,8 +149,11 @@
budget = make_budget(budget_against="Project")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate())
+ "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
+ project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit)
@@ -184,9 +189,11 @@
if month > 9:
month = 9
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")
+ "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
+ project=project)
self.assertTrue(frappe.db.get_value("GL Entry",
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@@ -289,7 +296,7 @@
budget = frappe.new_doc("Budget")
if budget_against == "Project":
- budget.project = "_Test Project"
+ budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else:
budget.cost_center =cost_center or "_Test Cost Center - _TC"
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index c441274..a749f0e 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -11,8 +11,10 @@
from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_fiscal_year
-from erpnext.exceptions import InvalidAccountCurrency
+from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts
+from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
+from six import iteritems
exclude_from_linked_with = True
class GLEntry(Document):
@@ -39,6 +41,7 @@
if not from_repost:
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
+ self.validate_allowed_dimensions()
validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj)
@@ -76,11 +79,9 @@
.format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self):
-
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
-
if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname):
@@ -93,6 +94,25 @@
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account))
+ def validate_allowed_dimensions(self):
+ dimension_filter_map = get_dimension_filter_map()
+ for key, value in iteritems(dimension_filter_map):
+ dimension = key[0]
+ account = key[1]
+
+ if self.account == account:
+ if value['is_mandatory'] and not self.get(dimension):
+ frappe.throw(_("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
+
+ if value['allow_or_restrict'] == 'Allow':
+ if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
+ frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ else:
+ if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
+ frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
def check_pl_account(self):
if self.is_opening=='Yes' and \
@@ -137,9 +157,10 @@
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
- if self.cost_center and _check_is_group():
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
- .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
+ and self.cost_center and _check_is_group():
+ frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
+ be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
index acc308e..3d80a97 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
@@ -20,7 +20,8 @@
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
},
{
- 'items': ['Item']
+ 'label': _('Stock'),
+ 'items': ['Item Groups', 'Item']
}
]
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index ff12967..37b03f3 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -120,6 +120,8 @@
}
}
});
+
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
voucher_type: function(frm){
@@ -197,6 +199,7 @@
this.load_defaults();
this.setup_queries();
this.setup_balance_formatter();
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
onload_post_render: function() {
@@ -222,15 +225,6 @@
return erpnext.journal_entry.account_query(me.frm);
});
- me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) {
- return {
- filters: {
- company: me.frm.doc.company,
- is_group: 0
- }
- };
- });
-
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
@@ -406,6 +400,8 @@
}
}
cur_frm.cscript.update_totals(doc);
+
+ erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts');
},
});
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index b56f8e5..5f003e0 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -160,7 +160,7 @@
self.assertFalse(gle)
def test_reverse_journal_entry(self):
- from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
+ from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
@@ -299,15 +299,20 @@
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
- project = make_project({
- 'project_name': 'Journal Entry Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
+
+ if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
+ project = make_project({
+ 'project_name': 'Journal Entry Project',
+ 'project_template_name': 'Test Project Template',
+ 'start_date': '2020-01-01'
+ })
+ project_name = project.name
+ else:
+ project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
for d in jv.accounts:
- d.project = project.project_name
+ d.project = project_name
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
jv.cheque_no = "112233"
@@ -317,10 +322,10 @@
expected_values = {
"_Test Cash - _TC": {
- "project": project.project_name
+ "project": project_name
},
"_Test Bank - _TC": {
- "project": project.project_name
+ "project": project_name
}
}
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
index 524a671..f90f867 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
@@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
@@ -46,20 +48,17 @@
};
});
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- };
- });
-
frm.set_value("company", frappe.defaults.get_user_default("Company"));
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) {
frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules."));
}
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
});
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index 3ce5701..c087980 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -36,6 +36,8 @@
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange');
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -100,6 +102,7 @@
}
})
}
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
invoice_type: function(frm) {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index e117471..f5c488d 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -1,6 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %}
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
@@ -8,6 +9,8 @@
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
setup: function(frm) {
@@ -88,15 +91,6 @@
}
});
- frm.set_query("cost_center", "deductions", function() {
- return {
- filters: {
- "is_group": 0,
- "company": frm.doc.company
- }
- }
- });
-
frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@@ -167,6 +161,7 @@
company: function(frm) {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
contact_person: function(frm) {
@@ -401,6 +396,8 @@
set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) {
+
+ var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -427,6 +424,14 @@
if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm);
+
+ if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
+ && frm.doc.paid_amount != frm.doc.received_amount) {
+ if (company_currency != frm.doc.paid_from_account_currency &&
+ frm.doc.payment_type == "Pay") {
+ frm.doc.paid_amount = frm.doc.received_amount;
+ }
+ }
}
},
() => {
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 7dd5b01..a74fa06 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -8,7 +8,7 @@
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
- get_dimension_filters)
+ get_dimensions)
class PeriodClosingVoucher(AccountsController):
def validate(self):
@@ -58,7 +58,7 @@
for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
- dimension_filters, default_dimensions = get_dimension_filters()
+ dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields)
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 7f4f755..efdeb1a 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -57,6 +57,8 @@
}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -67,6 +69,7 @@
company: function(frm) {
frm.trigger("toggle_display_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
toggle_display_account_head: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 7830cfd..4a952a3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -26,6 +26,11 @@
};
});
},
+
+ company: function() {
+ erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ },
+
onload: function() {
this._super();
@@ -41,6 +46,8 @@
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh: function(doc) {
@@ -498,7 +505,7 @@
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Purchase Invoice': 'Debit Note',
+ 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment'
}
@@ -511,15 +518,6 @@
}
}
}
-
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company,
- is_group: 0
- }
- };
- });
},
onload: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index c0506ba..2c088ce 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -426,26 +426,31 @@
)
def test_total_purchase_cost_for_project(self):
- make_project({'project_name':'_Test Project'})
+ if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
+ project = make_project({'project_name':'_Test Project for Purchase'})
+ else:
+ project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
- from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""")
+ from `tabPurchase Invoice Item`
+ where project = '{0}'
+ and docstatus=1""".format(project.name))
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
- pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project")
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000)
- pi1 = make_purchase_invoice(qty=10, project="_Test Project")
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ pi1 = make_purchase_invoice(qty=10, project=project.name)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500)
pi1.cancel()
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000)
pi.cancel()
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice_with_perpetual_inventory(self):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
@@ -860,17 +865,17 @@
})
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
- pi.items[0].project = item_project.project_name
- pi.project = project.project_name
+ pi.items[0].project = item_project.name
+ pi.project = project.name
pi.submit()
expected_values = {
"Creditors - _TC": {
- "project": project.project_name
+ "project": project.name
},
"_Test Account Cost for Goods Sold - _TC": {
- "project": item_project.project_name
+ "project": item_project.name
}
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 5efc32e..72199a9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -5,14 +5,17 @@
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %};
-
-
frappe.provide("erpnext.accounts");
+
+
erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({
setup: function(doc) {
this.setup_posting_date_time_check();
this._super(doc);
},
+ company: function() {
+ erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ },
onload: function() {
var me = this;
this._super();
@@ -33,6 +36,7 @@
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh: function(doc, dt, dn) {
@@ -571,15 +575,6 @@
};
});
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company,
- is_group: 0
- }
- };
- });
-
frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
@@ -592,7 +587,7 @@
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
- 'Sales Invoice': 'Sales Return',
+ 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request',
'Payment Entry': 'Payment'
},
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 50eb400..566734e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -179,7 +179,7 @@
# this sequence because outstanding may get -ve
self.make_gl_entries()
-
+
if self.update_stock == 1:
self.repost_future_sle_and_gle()
@@ -261,10 +261,10 @@
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
-
+
if self.update_stock == 1:
self.repost_future_sle_and_gle()
-
+
frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@@ -551,7 +551,7 @@
def add_remarks(self):
if not self.remarks:
if self.po_no and self.po_date:
- self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
+ self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
@@ -1699,6 +1699,7 @@
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1)
+@frappe.whitelist()
def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index eb223ee..5435d3b 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1573,17 +1573,17 @@
})
sales_invoice = create_sales_invoice(do_not_save=1)
- sales_invoice.items[0].project = item_project.project_name
- sales_invoice.project = project.project_name
+ sales_invoice.items[0].project = item_project.name
+ sales_invoice.project = project.name
sales_invoice.submit()
expected_values = {
"Debtors - _TC": {
- "project": project.project_name
+ "project": project.name
},
"Sales - _TC": {
- "project": item_project.project_name
+ "project": item_project.name
}
}
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
index d0904ee..8e4b806 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
@@ -1,16 +1,18 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.ui.form.on('Shipping Rule', {
- refresh: function(frm) {
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
+frappe.provide('erpnext.accounts.dimensions');
+frappe.ui.form.on('Shipping Rule', {
+ onload: function(frm) {
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
+ refresh: function(frm) {
frm.set_query("account", function() {
return {
filters: {
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 552a5d4..e023b47 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -446,7 +446,7 @@
if not self.generate_invoice_at_period_start:
return False
- if self.is_new_subscription():
+ if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True
# Check invoice dates and make sure it doesn't have outstanding invoices
diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
index 57a1231..7195c7e 100644
--- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
+++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
@@ -59,23 +59,111 @@
def get_columns(filters):
return [
- _("Payment Document") + ":: 100",
- _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140",
- _("Party Type") + "::100",
- _("Party") + ":Dynamic Link/Party Type:140",
- _("Posting Date") + ":Date:100",
- _("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"),
- _("Invoice Posting Date") + ":Date:130",
- _("Payment Due Date") + ":Date:130",
- _("Debit") + ":Currency:120",
- _("Credit") + ":Currency:120",
- _("Remarks") + "::150",
- _("Age") +":Int:40",
- "0-30:Currency:100",
- "30-60:Currency:100",
- "60-90:Currency:100",
- _("90-Above") + ":Currency:100",
- _("Delay in payment (Days)") + "::150"
+ {
+ "fieldname": "payment_document",
+ "label": _("Payment Document Type"),
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "fieldname": "payment_entry",
+ "label": _("Payment Document"),
+ "fieldtype": "Dynamic Link",
+ "options": "payment_document",
+ "width": 160
+ },
+ {
+ "fieldname": "party_type",
+ "label": _("Party Type"),
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "fieldname": "party",
+ "label": _("Party"),
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
+ "width": 160
+ },
+ {
+ "fieldname": "posting_date",
+ "label": _("Posting Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "invoice",
+ "label": _("Invoice"),
+ "fieldtype": "Link",
+ "options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
+ "width": 160
+ },
+ {
+ "fieldname": "invoice_posting_date",
+ "label": _("Invoice Posting Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "due_date",
+ "label": _("Payment Due Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "debit",
+ "label": _("Debit"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "credit",
+ "label": _("Credit"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "remarks",
+ "label": _("Remarks"),
+ "fieldtype": "Data",
+ "width": 200
+ },
+ {
+ "fieldname": "age",
+ "label": _("Age"),
+ "fieldtype": "Int",
+ "width": 50
+ },
+ {
+ "fieldname": "range1",
+ "label": "0-30",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range2",
+ "label": "30-60",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range3",
+ "label": "60-90",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range4",
+ "label": _("90 Above"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "delay_in_payment",
+ "label": _("Delay in payment (Days)"),
+ "fieldtype": "Int",
+ "width": 100
+ }
]
def get_conditions(filters):
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index cae150c..afbd9b4 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -48,7 +48,7 @@
def import_disease_tasks(self, disease, start_date):
disease_doc = frappe.get_doc('Disease', disease)
- self.create_task(disease_doc.treatment_task, self.name, start_date)
+ self.create_task(disease_doc.treatment_task, self.project, start_date)
def create_project(self, period, crop_tasks):
project = frappe.get_doc({
diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
index 5510d5a..763b403 100644
--- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
@@ -71,4 +71,4 @@
def check_project_creation():
- return True if frappe.db.exists('Project', 'Basil from seed 2017') else False
+ return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index b2318a2..6f1bb28 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -2,6 +2,7 @@
// For license information, please see license.txt
frappe.provide("erpnext.asset");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset', {
onload: function(frm) {
@@ -32,13 +33,11 @@
};
});
- frm.set_query("cost_center", function() {
- return {
- "filters": {
- "company": frm.doc.company,
- }
- };
- });
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
setup: function(frm) {
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index a6e6974..79c8861 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Asset Value Adjustment', {
setup: function(frm) {
frm.add_fetch('company', 'cost_center', 'cost_center');
@@ -13,11 +15,19 @@
}
});
},
+
onload: function(frm) {
if(frm.is_new() && frm.doc.asset) {
frm.trigger("set_current_asset_value");
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
asset: function(frm) {
frm.trigger("set_current_asset_value");
},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 20faded..14534ef 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying");
-
+frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", {
@@ -30,6 +30,10 @@
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
set_schedule_date(frm);
if (!frm.doc.transaction_date){
@@ -39,6 +43,8 @@
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}
});
@@ -58,8 +64,8 @@
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() {
this.frm.custom_make_buttons = {
- 'Purchase Receipt': 'Receipt',
- 'Purchase Invoice': 'Invoice',
+ 'Purchase Receipt': 'Purchase Receipt',
+ 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
}
diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
index e17973c..ba8535a 100644
--- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js
+++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
@@ -75,62 +75,70 @@
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Supplier" || tree_type == "Item") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Supplier") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else if (tree_type == "Item") {
+ row_values = data
+ .slice(5, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
- entry = {
- 'name':row_name,
- 'values':row_values
- }
+ entry = {
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
-
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
- found = true;
- new_datasets.splice(i,1);
- break;
+ let element_found = new_datasets.some((element, index, array)=>{
+ if(element.name == row_name){
+ array.splice(index, 1)
+ return true
}
- }
+ return false
+ })
- if(!found){
+ if (!element_found) {
new_datasets.push(entry);
}
-
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- },500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
+ },
});
}
}
diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py
index 47b4866..a73cb0d 100644
--- a/erpnext/buying/utils.py
+++ b/erpnext/buying/utils.py
@@ -35,9 +35,10 @@
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate
- if last_purchase_rate:
- frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
- (flt(last_purchase_rate), d.item_code))
+ frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
+
+
+
def validate_for_items(doc):
items = []
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 8fe3816..e3aac9a 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -493,6 +493,41 @@
'company': filters.get("company", "")
})
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
+ from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
+ dimension_filters = get_dimension_filter_map()
+ dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
+ query_filters = []
+
+ meta = frappe.get_meta(doctype)
+ if meta.is_tree:
+ query_filters.append(['is_group', '=', 0])
+
+ if meta.has_field('company'):
+ query_filters.append(['company', '=', filters.get('company')])
+
+ if txt:
+ query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
+
+ if dimension_filters:
+ if dimension_filters['allow_or_restrict'] == 'Allow':
+ query_selector = 'in'
+ else:
+ query_selector = 'not in'
+
+ if len(dimension_filters['allowed_dimensions']) == 1:
+ dimensions = tuple(dimension_filters['allowed_dimensions'] * 2)
+ else:
+ dimensions = tuple(dimension_filters['allowed_dimensions'])
+
+ query_filters.append(['name', query_selector, dimensions])
+
+ output = frappe.get_all(doctype, filters=query_filters)
+ result = [d.name for d in output]
+
+ return [(d,) for d in set(result)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 7979226..a048d6e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -328,6 +328,7 @@
target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
+ target_doc.price_list_rate = 0
elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@@ -353,6 +354,7 @@
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name
+ target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 85cfb95..812021f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -233,7 +233,7 @@
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"),
- 'incoming_rate': p.incoming_rate
+ 'incoming_rate': p.get("incoming_rate")
}))
else:
il.append(frappe._dict({
@@ -252,7 +252,7 @@
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"),
- 'incoming_rate': d.incoming_rate
+ 'incoming_rate': d.get("incoming_rate")
}))
return il
diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py
index c257215..813f0a0 100644
--- a/erpnext/controllers/tests/test_item_variant.py
+++ b/erpnext/controllers/tests/test_item_variant.py
@@ -6,6 +6,7 @@
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
+from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
from six import string_types
@@ -56,6 +57,8 @@
qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template
+
+ create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', {
"specification": "Moisture",
"value": "< 5%",
diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js
index 75dd446..65b5fa6 100644
--- a/erpnext/education/doctype/fee_schedule/fee_schedule.js
+++ b/erpnext/education/doctype/fee_schedule/fee_schedule.js
@@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Schedule', {
setup: function(frm) {
frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account');
@@ -8,6 +9,10 @@
frm.add_fetch('fee_structure', 'cost_center', 'cost_center');
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
frm.set_query('receivable_account', function(doc) {
return {
@@ -50,6 +55,8 @@
}
}
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js
index b331c6d..310c410 100644
--- a/erpnext/education/doctype/fee_structure/fee_structure.js
+++ b/erpnext/education/doctype/fee_structure/fee_structure.js
@@ -1,6 +1,8 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Fee Structure', {
setup: function(frm) {
frm.add_fetch('company', 'default_receivable_account', 'receivable_account');
@@ -8,6 +10,10 @@
frm.add_fetch('company', 'cost_center', 'cost_center');
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
frm.set_query('academic_term', function() {
return {
@@ -35,6 +41,8 @@
}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js
index aaf42b4..ac66acd 100644
--- a/erpnext/education/doctype/fees/fees.js
+++ b/erpnext/education/doctype/fees/fees.js
@@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Fees", {
setup: function(frm) {
@@ -9,15 +10,19 @@
frm.add_fetch("fee_structure", "cost_center", "cost_center");
},
- onload: function(frm){
- frm.set_query("academic_term",function(){
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
+ onload: function(frm) {
+ frm.set_query("academic_term", function() {
return{
- "filters":{
+ "filters": {
"academic_year": (frm.doc.academic_year)
}
};
});
- frm.set_query("fee_structure",function(){
+ frm.set_query("fee_structure", function() {
return{
"filters":{
"academic_year": (frm.doc.academic_year)
@@ -45,6 +50,8 @@
if (!frm.doc.posting_date) {
frm.doc.posting_date = frappe.datetime.get_today();
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index 6fbcd8a..886a7d8 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -124,21 +124,24 @@
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
- if filters.get('program'):
- return frappe.db.sql("""select course, course_name from `tabProgram Course`
- where parent = %(program)s and course like %(txt)s {match_cond}
- order by
- if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
- idx desc,
- `tabProgram Course`.course asc
- limit {start}, {page_len}""".format(
- match_cond=get_match_cond(doctype),
- start=start,
- page_len=page_len), {
- "txt": "%{0}%".format(txt),
- "_txt": txt.replace('%', ''),
- "program": filters['program']
- })
+ if not filters.get('program'):
+ frappe.msgprint(_("Please select a Program first."))
+ return []
+
+ return frappe.db.sql("""select course, course_name from `tabProgram Course`
+ where parent = %(program)s and course like %(txt)s {match_cond}
+ order by
+ if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
+ idx desc,
+ `tabProgram Course`.course asc
+ limit {start}, {page_len}""".format(
+ match_cond=get_match_cond(doctype),
+ start=start,
+ page_len=page_len), {
+ "txt": "%{0}%".format(txt),
+ "_txt": txt.replace('%', ''),
+ "program": filters['program']
+ })
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 8d4b510..66d0e5f 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -29,14 +29,11 @@
response = self.client.Item.public_token.exchange(public_token)
access_token = response["access_token"]
return access_token
-
- def get_link_token(self):
+
+ def get_token_request(self, update_mode=False):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
- token_request = {
+ args = {
"client_name": self.client_name,
- "client_id": self.settings.plaid_client_id,
- "secret": self.settings.plaid_secret,
- "products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes,
@@ -45,6 +42,20 @@
}
}
+ if update_mode:
+ args["access_token"] = self.access_token
+ else:
+ args.update({
+ "client_id": self.settings.plaid_client_id,
+ "secret": self.settings.plaid_secret,
+ "products": self.products,
+ })
+
+ return args
+
+ def get_link_token(self, update_mode=False):
+ token_request = self.get_token_request(update_mode)
+
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 22a4004..bbc2ca8 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -12,9 +12,25 @@
refresh: function (frm) {
if (frm.doc.enabled) {
- frm.add_custom_button('Link a new bank account', () => {
+ frm.add_custom_button(__('Link a new bank account'), () => {
new erpnext.integrations.plaidLink(frm);
});
+
+ frm.add_custom_button(__("Sync Now"), () => {
+ frappe.call({
+ method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
+ freeze: true,
+ callback: () => {
+ let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
+
+ frappe.msgprint({
+ title: __("Sync Started"),
+ message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
+ alert: 1
+ });
+ }
+ });
+ }).addClass("btn-primary");
}
}
});
@@ -30,10 +46,18 @@
this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
- this.token = await this.frm.call("get_link_token").then(resp => resp.message);
+ this.token = await this.get_link_token();
this.init_plaid();
}
+ async get_link_token() {
+ const token = await this.frm.call("get_link_token").then(resp => resp.message);
+ if (!token) {
+ frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
+ }
+ return token;
+ }
+
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
@@ -78,8 +102,8 @@
}
onScriptError(error) {
- frappe.msgprint("There was an issue connecting to Plaid's authentication server");
- frappe.msgprint(error);
+ frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
+ console.log(error);
}
plaid_success(token, response) {
@@ -107,4 +131,4 @@
});
}, __("Select a company"), __("Continue"));
}
-};
+};
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index e535e81..70c7f3f 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -166,7 +166,6 @@
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id
-
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None
@@ -228,13 +227,23 @@
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
-
if settings.enabled == 1 and settings.automatic_sync == 1:
- plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
+ enqueue_synchronization()
- for plaid_account in plaid_accounts:
- frappe.enqueue(
- "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
- bank=plaid_account.bank,
- bank_account=plaid_account.name
- )
+@frappe.whitelist()
+def enqueue_synchronization():
+ plaid_accounts = frappe.get_all("Bank Account",
+ filters={"integration_id": ["!=", ""]},
+ fields=["name", "bank"])
+
+ for plaid_account in plaid_accounts:
+ frappe.enqueue(
+ "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
+ bank=plaid_account.bank,
+ bank_account=plaid_account.name
+ )
+
+@frappe.whitelist()
+def get_link_token_for_update(access_token):
+ plaid = PlaidConnector(access_token)
+ return plaid.get_link_token(update_mode=True)
diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py
index d92af5d..04291cd 100644
--- a/erpnext/exceptions.py
+++ b/erpnext/exceptions.py
@@ -6,3 +6,5 @@
class InvalidAccountCurrency(frappe.ValidationError): pass
class InvalidCurrency(frappe.ValidationError): pass
class PartyDisabled(frappe.ValidationError):pass
+class InvalidAccountDimensionError(frappe.ValidationError): pass
+class MandatoryAccountDimensionError(frappe.ValidationError): pass
diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
index 0104386..ddf1bce 100644
--- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
+++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
@@ -17,6 +17,9 @@
"enable_free_follow_ups",
"max_visits",
"valid_days",
+ "inpatient_settings_section",
+ "allow_discharge_despite_unbilled_services",
+ "do_not_bill_inpatient_encounters",
"healthcare_service_items",
"inpatient_visit_charge_item",
"op_consulting_charge_item",
@@ -302,11 +305,28 @@
"fieldname": "enable_free_follow_ups",
"fieldtype": "Check",
"label": "Enable Free Follow-ups"
+ },
+ {
+ "fieldname": "inpatient_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Inpatient Settings"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_discharge_despite_unbilled_services",
+ "fieldtype": "Check",
+ "label": "Allow Discharge Despite Unbilled Healthcare Services"
+ },
+ {
+ "default": "0",
+ "fieldname": "do_not_bill_inpatient_encounters",
+ "fieldtype": "Check",
+ "label": "Do Not Bill Patient Encounters for Inpatients"
}
],
"issingle": 1,
"links": [],
- "modified": "2020-07-08 15:17:21.543218",
+ "modified": "2021-01-13 09:04:35.877700",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Settings",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
index ca97489..a7b06b1 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
@@ -5,6 +5,7 @@
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
+ frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => {
return {
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
index dd4c423..b1a6ee4 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
@@ -139,7 +139,6 @@
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
- "read_only": 1,
"reqd": 1
},
{
@@ -180,7 +179,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-03 13:22:37.820707",
+ "modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
index 70ae713..bba5213 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -15,8 +15,6 @@
self.validate_medication_orders()
def get_medication_orders(self):
- self.validate_datetime_filters()
-
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
@@ -27,22 +25,6 @@
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
- def validate_datetime_filters(self):
- if self.from_date and self.to_date:
- self.validate_from_to_dates('from_date', 'to_date')
-
- if self.from_date and getdate(self.from_date) > getdate():
- frappe.throw(_('From Date cannot be after the current date.'))
-
- if self.to_date and getdate(self.to_date) > getdate():
- frappe.throw(_('To Date cannot be after the current date.'))
-
- if self.from_time and self.from_time > nowtime():
- frappe.throw(_('From Time cannot be after the current time.'))
-
- if self.to_time and self.to_time > nowtime():
- frappe.throw(_('To Time cannot be after the current time.'))
-
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])
diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
index bc76970..dc549a6 100644
--- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
-from frappe.utils import today, now_datetime, getdate, get_datetime
+from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond
@@ -113,6 +113,7 @@
inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True)
+
@frappe.whitelist()
def schedule_discharge(args):
discharge_order = json.loads(args)
@@ -126,16 +127,19 @@
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
+
def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order:
inpatient_record.set(key, ip_order[key])
+
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child:
table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname))
+
def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -144,54 +148,88 @@
inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
+
def discharge_patient(inpatient_record):
- validate_invoiced_inpatient(inpatient_record)
+ validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True)
-def validate_invoiced_inpatient(inpatient_record):
- pending_invoices = []
+
+def validate_inpatient_invoicing(inpatient_record):
+ if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
+ return
+
+ pending_invoices = get_pending_invoices(inpatient_record)
+
+ if pending_invoices:
+ message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
+
+ formatted_doc_rows = ''
+
+ for doctype, docnames in pending_invoices.items():
+ formatted_doc_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(doctype, docnames)
+
+ message += """
+ <table class='table'>
+ <thead>
+ <th>{0}</th>
+ <th>{1}</th>
+ </thead>
+ {2}
+ </table>
+ """.format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
+
+ frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
+
+
+def get_pending_invoices(inpatient_record):
+ pending_invoices = {}
if inpatient_record.inpatient_occupancies:
service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
- if inpatient_occupancy.invoiced != 1:
+ if not inpatient_occupancy.invoiced:
if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit
else:
service_unit_names = inpatient_occupancy.service_unit
if service_unit_names:
- pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")")
+ pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs:
- doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record)
+ doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
- if pending_invoices:
- frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
- .join(pending_invoices)), title=_('Unbilled Invoices'))
+ return pending_invoices
+
def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list:
doc_ids = False
for doc_name in doc_name_list:
+ doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids:
- doc_ids += ", "+doc_name.name
+ doc_ids += ", " + doc_link
else:
- doc_ids = doc_name.name
+ doc_ids = doc_link
if doc_ids:
- pending_invoices.append(doc + " (" + doc_ids + ")")
+ pending_invoices[doc] = doc_ids
return pending_invoices
-def get_inpatient_docs_not_invoiced(doc, inpatient_record):
+
+def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
+
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted'
@@ -203,6 +241,7 @@
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
+
def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit
@@ -212,6 +251,7 @@
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
+
def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -221,6 +261,7 @@
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
index 70706ad..10990d4 100644
--- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
@@ -8,6 +8,8 @@
from frappe.utils import now_datetime, today
from frappe.utils.make_random import get_random
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter
+from erpnext.healthcare.utils import get_encounters_to_invoice
class TestInpatientRecord(unittest.TestCase):
def test_admit_and_discharge(self):
@@ -40,6 +42,60 @@
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
+ def test_allow_discharge_despite_unbilled_services(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1)
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ # Discharge
+ schedule_discharge(frappe.as_json({"patient": patient}))
+ self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
+
+ ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
+ # Should not validate Pending Invoices
+ ip_record.discharge()
+
+ self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
+ self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
+
+ setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0)
+
+ def test_do_not_bill_patient_encounters_for_inpatients(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1)
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ # Patient Encounter
+ patient_encounter = create_patient_encounter()
+ encounters = get_encounters_to_invoice(patient, "_Test Company")
+ encounter_ids = [entry.reference_name for entry in encounters]
+ self.assertFalse(patient_encounter.name in encounter_ids)
+
+ # Discharge
+ schedule_discharge(frappe.as_json({"patient": patient}))
+ self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
+
+ ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
+ mark_invoiced_inpatient_occupancy(ip_record)
+ discharge_patient(ip_record)
+ setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0)
+
def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient()
@@ -63,6 +119,13 @@
inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True)
+
+def setup_inpatient_settings(key, value):
+ settings = frappe.get_single("Healthcare Settings")
+ settings.set(key, value)
+ settings.save()
+
+
def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record')
@@ -78,6 +141,7 @@
inpatient_record.scheduled_date = today()
return inpatient_record
+
def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
if not service_unit:
@@ -105,6 +169,7 @@
return service_unit.name
return service_unit
+
def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
@@ -116,6 +181,7 @@
return service_unit_type.name
return service_unit_type
+
def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient:
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index 3df7ba1..b681ed1 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -23,8 +23,10 @@
self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled')
- create_encounter(appointment)
+ encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ encounter.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index a061c66..7fb159d 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -5,10 +5,10 @@
import frappe
import unittest
-from frappe.utils import getdate, flt
+from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
-from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
+from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
@@ -28,6 +28,15 @@
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
+ patient, medical_department, practitioner = create_healthcare_docs()
+ appointment = create_appointment(patient, practitioner, nowdate())
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session = frappe.get_doc(session)
+ session.submit()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ session.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
+
def test_therapy_plan_from_template(self):
patient = create_patient()
template = create_therapy_plan_template()
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index bc0ff1a..ac01c60 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -47,7 +47,7 @@
@frappe.whitelist()
-def make_therapy_session(therapy_plan, patient, therapy_type, company):
+def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session')
@@ -58,6 +58,7 @@
therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises
+ therapy_session.appointment = appointment
if frappe.flags.in_test:
therapy_session.start_date = today()
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
index a2b01c9..fd20003 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
@@ -19,6 +19,15 @@
}
};
});
+
+ frm.set_query('appointment', function() {
+
+ return {
+ filters: {
+ 'status': ['in', ['Open', 'Scheduled']]
+ }
+ };
+ });
},
refresh: function(frm) {
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
index 85d0970..c000544 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
@@ -43,7 +43,14 @@
self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
+ def on_update(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
+
def on_cancel(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
+
self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False):
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index 96282f5..6a499aa 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -77,11 +77,13 @@
def get_encounters_to_invoice(patient, company):
+ if not isinstance(patient, str):
+ patient = patient.name
encounters_to_invoice = []
encounters = frappe.get_list(
'Patient Encounter',
fields=['*'],
- filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1}
+ filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1}
)
if encounters:
for encounter in encounters:
@@ -90,6 +92,10 @@
income_account = None
service_item = None
if encounter.practitioner:
+ if encounter.inpatient_record and \
+ frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
+ continue
+
service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
income_account = get_income_account(encounter.practitioner, encounter.company)
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 4f1c04f..dc2aaa4 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2020-10-16 15:02:04.283657",
+ "modified": "2021-01-01 16:54:33.477439",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
@@ -855,7 +855,6 @@
"write": 1
}
],
- "quick_entry": 1,
"search_fields": "employee_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 4e9ee3b..336e13c 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -38,7 +38,8 @@
onboarding.insert()
onboarding.submit()
- self.assertEqual(onboarding.project, 'Employee Onboarding : Test Researcher - test@researcher.com')
+ project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
+ self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 221300b..629341f 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -2,11 +2,21 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.hr");
+frappe.provide("erpnext.accounts.dimensions");
-erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({
- expense_type: function(doc, cdt, cdn) {
+frappe.ui.form.on('Expense Claim', {
+ onload: function(frm) {
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+});
+
+frappe.ui.form.on('Expense Claim Detail', {
+ expense_type: function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
- if(!doc.company) {
+ if (!frm.doc.company) {
d.expense_type = "";
frappe.msgprint(__("Please set the Company"));
this.frm.refresh_fields();
@@ -20,7 +30,7 @@
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: {
"expense_claim_type": d.expense_type,
- "company": doc.company
+ "company": frm.doc.company
},
callback: function(r) {
if (r.message) {
@@ -32,8 +42,6 @@
}
});
-$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm}));
-
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee','employee_name','employee_name');
cur_frm.add_fetch('expense_type','description','description');
@@ -167,15 +175,6 @@
};
});
- frm.set_query("cost_center", "expenses", function() {
- return {
- filters: {
- "company": frm.doc.company,
- "is_group": 0
- }
- };
- });
-
frm.set_query("payable_account", function() {
return {
filters: {
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 4a0908d..f9e3a44 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -20,35 +20,36 @@
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
- frappe.get_doc({
+ project = frappe.get_doc({
"project_name": "_Test Project 1",
"doctype": "Project"
- }).save()
+ })
+ project.save()
task = frappe.get_doc(dict(
doctype = 'Task',
subject = '_Test Project Task 1',
status = 'Open',
- project = '_Test Project 1'
+ project = project.name
)).insert()
task_name = task.name
payable_account = get_payable_account(company_name)
- make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name)
+ make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
- expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name)
+ expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
expense_claim2.cancel()
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
def test_expense_claim_status(self):
payable_account = get_payable_account(company_name)
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index c13548a..1360fd1 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -11,15 +11,24 @@
"field_order": [
"applicant_name",
"email_id",
+ "phone_number",
+ "country",
"status",
"column_break_3",
"job_title",
"source",
"source_name",
+ "applicant_rating",
"section_break_6",
"notes",
"cover_letter",
- "resume_attachment"
+ "resume_attachment",
+ "resume_link",
+ "section_break_16",
+ "currency",
+ "column_break_18",
+ "lower_range",
+ "upper_range"
],
"fields": [
{
@@ -91,12 +100,65 @@
"fieldtype": "Data",
"label": "Notes",
"read_only": 1
+ },
+ {
+ "fieldname": "phone_number",
+ "fieldtype": "Data",
+ "label": "Phone Number",
+ "options": "Phone"
+ },
+ {
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "label": "Country",
+ "options": "Country"
+ },
+ {
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "label": "Resume Link"
+ },
+ {
+ "fieldname": "applicant_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Applicant Rating"
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break",
+ "label": "Salary Expectation"
+ },
+ {
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "label": "Lower Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "label": "Upper Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
}
],
"icon": "fa fa-user",
"idx": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-01-13 16:19:39.113330",
+ "modified": "2020-09-18 12:39:02.557563",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json
index 4437e02..b8f6df6 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.json
+++ b/erpnext/hr/doctype/job_opening/job_opening.json
@@ -1,456 +1,188 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:route",
- "beta": 0,
- "creation": "2013-01-15 16:13:36",
- "custom": 0,
- "description": "Description of a Job Opening",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:route",
+ "creation": "2013-01-15 16:13:36",
+ "description": "Description of a Job Opening",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "job_title",
+ "company",
+ "status",
+ "column_break_5",
+ "designation",
+ "department",
+ "staffing_plan",
+ "planned_vacancies",
+ "section_break_6",
+ "publish",
+ "route",
+ "column_break_12",
+ "job_application_route",
+ "section_break_14",
+ "description",
+ "section_break_16",
+ "currency",
+ "lower_range",
+ "upper_range",
+ "column_break_20",
+ "publish_salary_range"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "job_title",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Job Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Job Title",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Open\nClosed",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Open\nClosed"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_5",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "designation",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Designation",
- "length": 0,
- "no_copy": 0,
- "options": "Designation",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "department",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "staffing_plan",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Staffing Plan",
- "length": 0,
- "no_copy": 0,
- "options": "Staffing Plan",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "staffing_plan",
+ "fieldtype": "Link",
+ "label": "Staffing Plan",
+ "options": "Staffing Plan",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "staffing_plan",
- "fieldname": "planned_vacancies",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Planned number of Positions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "staffing_plan",
+ "fieldname": "planned_vacancies",
+ "fieldtype": "Int",
+ "label": "Planned number of Positions",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "publish",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Publish on website",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "publish",
+ "fieldtype": "Check",
+ "label": "Publish on website"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "publish",
- "fieldname": "route",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Route",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "depends_on": "publish",
+ "fieldname": "route",
+ "fieldtype": "Data",
+ "label": "Route",
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Job profile, qualifications required etc.",
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "description": "Job profile, qualifications required etc.",
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "in_list_view": 1,
+ "label": "Description"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "label": "Lower Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "label": "Upper Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "publish",
+ "description": "Route to the custom Job Application Webform",
+ "fieldname": "job_application_route",
+ "fieldtype": "Data",
+ "label": "Job Application Route"
+ },
+ {
+ "default": "0",
+ "fieldname": "publish_salary_range",
+ "fieldtype": "Check",
+ "label": "Publish Salary Range"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-bookmark",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-05-20 15:38:44.705823",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Job Opening",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-bookmark",
+ "idx": 1,
+ "links": [],
+ "modified": "2020-09-18 11:23:29.488923",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Job Opening",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Guest",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "read": 1,
+ "role": "Guest"
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index 00883d7..1e89767 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -43,9 +43,8 @@
current_count = designation_counts['employee_count'] + designation_counts['job_openings']
if self.planned_vacancies <= current_count:
- frappe.throw(_("Job Openings for designation {0} already open \
- or hiring completed as per Staffing Plan {1}"
- .format(self.designation, self.staffing_plan)))
+ frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
+ self.designation, self.staffing_plan))
def get_context(self, context):
context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
@@ -56,7 +55,8 @@
context.get_list = get_job_openings
def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
- fields = ['name', 'status', 'job_title', 'description']
+ fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range',
+ 'lower_range', 'upper_range', 'currency', 'job_application_route']
filters = filters or {}
filters.update({
diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
index 5da8cc8..c015101 100644
--- a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
+++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
@@ -1,9 +1,18 @@
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
+ {%- if doc.publish_salary_range -%}
+ <p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
+ {% endif %}
<div>
- <a class="btn btn-primary"
- href="/job_application?new=1&job_title={{ doc.name }}">
+ {%- if doc.job_application_route -%}
+ <a class='btn btn-primary'
+ href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
+ {% else %}
+ <a class='btn btn-primary'
+ href='/job_application?new=1&job_title={{ doc.name }}'>
+ {{ _("Apply Now") }}</a>
+ {% endif %}
</div>
</div>
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 4b31501..3a300c0 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -11,6 +11,7 @@
"employee",
"employee_name",
"department",
+ "company",
"column_break1",
"leave_type",
"from_date",
@@ -219,6 +220,15 @@
"label": "Leave Policy Assignment",
"options": "Leave Policy Assignment",
"read_only": 1
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
}
],
"icon": "fa fa-ok",
@@ -226,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-20 14:25:10.314323",
+ "modified": "2021-01-04 18:46:13.184104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
index 4abba5f..d74760a 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType",
"engine": "InnoDB",
@@ -8,6 +9,7 @@
"leave_type",
"transaction_type",
"transaction_name",
+ "company",
"leaves",
"column_break_7",
"from_date",
@@ -106,12 +108,22 @@
"fieldtype": "Link",
"label": "Holiday List",
"options": "Holiday List"
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2020-09-04 12:16:36.569066",
+ "links": [],
+ "modified": "2021-01-04 18:47:45.146652",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Ledger Entry",
diff --git a/erpnext/hr/web_form/job_application/job_application.json b/erpnext/hr/web_form/job_application/job_application.json
index f630570..512ba5c 100644
--- a/erpnext/hr/web_form/job_application/job_application.json
+++ b/erpnext/hr/web_form/job_application/job_application.json
@@ -1,86 +1,200 @@
{
- "accept_payment": 0,
- "allow_comments": 1,
- "allow_delete": 0,
- "allow_edit": 1,
- "allow_incomplete": 0,
- "allow_multiple": 1,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 0,
- "creation": "2016-09-10 02:53:16.598314",
- "doc_type": "Job Applicant",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "",
- "is_standard": 1,
- "login_required": 0,
- "max_attachment_size": 0,
- "modified": "2016-12-20 00:21:44.081622",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "job-application",
- "owner": "Administrator",
- "published": 1,
- "route": "job_application",
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_message": "Thank you for applying.",
- "success_url": "/jobs",
- "title": "Job Application",
+ "accept_payment": 0,
+ "allow_comments": 1,
+ "allow_delete": 0,
+ "allow_edit": 1,
+ "allow_incomplete": 0,
+ "allow_multiple": 1,
+ "allow_print": 0,
+ "amount": 0.0,
+ "amount_based_on_field": 0,
+ "apply_document_permissions": 0,
+ "client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n",
+ "creation": "2016-09-10 02:53:16.598314",
+ "doc_type": "Job Applicant",
+ "docstatus": 0,
+ "doctype": "Web Form",
+ "idx": 0,
+ "introduction_text": "",
+ "is_standard": 1,
+ "login_required": 0,
+ "max_attachment_size": 0,
+ "modified": "2020-10-07 19:27:17.143355",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "job-application",
+ "owner": "Administrator",
+ "published": 1,
+ "route": "job_application",
+ "route_to_success_link": 0,
+ "show_attachments": 0,
+ "show_in_grid": 0,
+ "show_sidebar": 1,
+ "sidebar_items": [],
+ "success_message": "Thank you for applying.",
+ "success_url": "/jobs",
+ "title": "Job Application",
"web_form_fields": [
{
- "fieldname": "job_title",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Job Opening",
- "max_length": 0,
- "max_value": 0,
- "options": "",
- "read_only": 1,
- "reqd": 0
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Job Opening",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "",
+ "read_only": 1,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
{
- "fieldname": "applicant_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Applicant Name",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "applicant_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Applicant Name",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
{
- "fieldname": "email_id",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Email Address",
- "max_length": 0,
- "max_value": 0,
- "options": "Email",
- "read_only": 0,
- "reqd": 1
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "email_id",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Email Address",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Email",
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
{
- "fieldname": "cover_letter",
- "fieldtype": "Text",
- "hidden": 0,
- "label": "Cover Letter",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "phone_number",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Phone Number",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Phone",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
{
- "fieldname": "resume_attachment",
- "fieldtype": "Attach",
- "hidden": 0,
- "label": "Resume Attachment",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "label": "Country of Residence",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Country",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "cover_letter",
+ "fieldtype": "Text",
+ "hidden": 0,
+ "label": "Cover Letter",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Resume Link",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "label": "Expected Salary Range per month",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "label": "Currency",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "label": "Lower Range",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "label": "Upper Range",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
}
]
}
\ No newline at end of file
diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json
index fc59c19..75036bd 100644
--- a/erpnext/loan_management/desk_page/loan/loan.json
+++ b/erpnext/loan_management/desk_page/loan/loan.json
@@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Interest Accrual\",\n \"is_query_report\": true,\n \"label\": \"Loan Interest Report\",\n \"name\": \"Loan Interest Report\",\n \"route\": \"#query-report/Loan Interest Report\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Exposure\",\n \"name\": \"Loan Security Exposure\",\n \"route\": \"#query-report/Loan Security Exposure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Applicant-Wise Loan Security Exposure\",\n \"name\": \"Applicant-Wise Loan Security Exposure\",\n \"route\": \"#query-report/Applicant-Wise Loan Security Exposure\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@@ -38,7 +38,7 @@
"idx": 0,
"is_standard": 1,
"label": "Loan",
- "modified": "2020-10-17 12:59:50.336085",
+ "modified": "2021-01-17 07:21:22.092184",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 2e0a4d1..e607d4f 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -202,7 +202,9 @@
# checking greater than 0 as there may be some minor precision error
if pending_amount < write_off_limit:
- # update status as loan closure requested
+ # Auto create loan write off and update status as loan closure requested
+ write_off = make_loan_write_off(loan)
+ write_off.submit()
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@@ -336,13 +338,13 @@
return unpledge_request
def validate_employee_currency_with_company_currency(applicant, company):
- from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
- if not applicant:
- frappe.throw(_("Please select Applicant"))
- if not company:
- frappe.throw(_("Please select Company"))
- employee_currency = get_employee_currency(applicant)
- company_currency = erpnext.get_company_currency(company)
- if employee_currency != company_currency:
- frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
- .format(applicant, employee_currency))
+ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
+ if not applicant:
+ frappe.throw(_("Please select Applicant"))
+ if not company:
+ frappe.throw(_("Please select Company"))
+ employee_currency = get_employee_currency(applicant)
+ company_currency = erpnext.get_company_currency(company)
+ if employee_currency != company_currency:
+ frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
+ .format(applicant, employee_currency))
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 8b1f9a2..f3c9db6 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -321,7 +321,7 @@
self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertTrue(amounts['pending_principal_amount'] < 0)
+ self.assertEqual(amounts['pending_principal_amount'], 0)
self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0)
@@ -362,6 +362,27 @@
unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1)
+ def test_santined_loan_security_unpledge(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ self.assertEquals(loan.loan_amount, 1000000)
+
+ unpledge_map = {'Test Security 1': 4000}
+ unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
+ unpledge_request.submit()
+ unpledge_request.status = 'Approved'
+ unpledge_request.save()
+ unpledge_request.submit()
+
def test_disbursal_check_with_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
@@ -452,7 +473,7 @@
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertTrue(amounts['pending_principal_amount'] < 0.0)
+ self.assertEqual(amounts['pending_principal_amount'], 0.0)
def test_partial_unaccrued_interest_payment(self):
pledge = [{
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
index f157f0d..185bf7a 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
@@ -22,6 +22,7 @@
"paid_principal_amount",
"column_break_14",
"interest_amount",
+ "total_pending_interest_amount",
"paid_interest_amount",
"penalty_amount",
"section_break_15",
@@ -172,13 +173,19 @@
"hidden": 1,
"label": "Last Accrual Date",
"read_only": 1
+ },
+ {
+ "fieldname": "total_pending_interest_amount",
+ "fieldtype": "Currency",
+ "label": "Total Pending Interest Amount",
+ "options": "Company:company:default_currency"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-07 05:49:25.448875",
+ "modified": "2021-01-10 00:15:21.544140",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index d17f5af..7d7992d 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -100,6 +100,8 @@
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
+ pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure')
+
args = frappe._dict({
'loan': loan.name,
'applicant_type': loan.applicant_type,
@@ -108,7 +110,8 @@
'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest,
- 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
+ 'total_pending_interest_amount': pending_amounts['interest_amount'],
+ 'penalty_amount': pending_amounts['penalty_amount'],
'process_loan_interest': process_loan_interest,
'posting_date': posting_date,
'accrual_type': accrual_type
@@ -202,6 +205,7 @@
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
+ loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision)
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
index 46a6440..85e008a 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -37,10 +37,8 @@
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
-
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
-
loan.submit()
first_date = '2019-10-01'
@@ -50,11 +48,46 @@
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
-
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
-
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
-
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
+
+ def test_accumulated_amounts(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+ loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
+ posting_date=get_first_day(nowdate()))
+ loan.submit()
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ no_of_days = date_diff(last_date, first_date) + 1
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
+ loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
+
+ self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0))
+
+ next_start_date = '2019-10-31'
+ next_end_date = '2019-11-29'
+
+ no_of_days = date_diff(next_end_date, next_start_date) + 1
+ process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date)
+ new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+
+ total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0)
+
+ loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name,
+ 'process_loan_interest_accrual': process})
+ self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount)
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 415ba99..ac30c91 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -377,7 +377,7 @@
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
- amounts["unaccrued_interest"] = unaccrued_interest
+ amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
if final_due_date:
amounts["due_date"] = final_due_date
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
index a55b482..b6e8763 100644
--- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"column_break_2",
"uom",
@@ -79,10 +80,18 @@
"label": "Loan Security Type",
"options": "Loan Security Type",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-06-11 03:41:33.900340",
+ "modified": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Price",
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index 61c418d..c4c2d68 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -44,10 +44,16 @@
"valid_upto": (">=", get_datetime())
}, as_list=1))
- total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
- 'total_interest_payable', 'written_off_amount'])
+ loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
+ 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
- pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
+ if loan_details.status == 'Disbursed':
+ pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
+ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
+ else:
+ pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
+ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
+
security_value = 0
unpledge_qty_map = {}
ltv_ratio = 0
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index 18a9731..3ef5304 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -144,17 +144,17 @@
},
{
"allow_on_submit": 1,
- "description": "Pending amount that will be automatically ignored on loan closure request ",
+ "description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
- "label": "Write Off Amount ",
+ "label": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-26 07:13:55.029811",
+ "modified": "2021-01-17 06:51:26.082879",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json
index 801e3a3..c23479c 100644
--- a/erpnext/loan_management/doctype/pledge/pledge.json
+++ b/erpnext/loan_management/doctype/pledge/pledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"loan_security_code",
"uom",
@@ -85,11 +86,18 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:15.424937",
+ "modified": "2021-01-17 07:41:12.452514",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
index ffc3671..3feb305 100644
--- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
@@ -30,7 +30,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-02-01 08:14:05.845161",
+ "modified": "2021-01-17 03:59:14.494557",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Security Shortfall",
@@ -45,7 +45,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
},
{
@@ -57,7 +59,9 @@
"read": 1,
"report": 1,
"role": "Loan Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
index 3e7e778..a0b3a79 100644
--- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
+++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"qty",
"loan_security_price",
"amount",
@@ -56,12 +57,19 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:37.542344",
+ "modified": "2021-01-17 07:29:01.671722",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
index 0035668..0091e6c 100644
--- a/erpnext/loan_management/doctype/unpledge/unpledge.json
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"loan_security_code",
"haircut",
@@ -61,12 +62,19 @@
"fieldtype": "Percent",
"label": "Haircut",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:28.106961",
+ "modified": "2021-01-17 07:36:20.212342",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js
new file mode 100644
index 0000000..73d60c4
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Applicant-Wise Loan Security Exposure"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json
new file mode 100644
index 0000000..a778cd7
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-15 23:48:38.913514",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-15 23:48:38.913514",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Applicant-Wise Loan Security Exposure",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Security",
+ "report_name": "Applicant-Wise Loan Security Exposure",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
new file mode 100644
index 0000000..6d7c3b7
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from frappe.utils import get_datetime, flt
+from six import iteritems
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_data(filters)
+ return columns, data
+
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
+ {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
+ {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
+ {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
+ {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
+ {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
+ {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
+ {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_data(filters):
+ data = []
+ loan_security_details = get_loan_security_details(filters)
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
+ loan_security_details)
+
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for key, qty in iteritems(pledge_values):
+ row = {}
+ current_value = flt(qty * loan_security_details.get(key[1])['latest_price'])
+ row.update(loan_security_details.get(key[1]))
+ row.update({
+ 'applicant_type': applicant_type_map.get(key[0]),
+ 'applicant_name': key[0],
+ 'total_qty': qty,
+ 'current_value': current_value,
+ 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2),
+ 'currency': currency
+ })
+
+ data.append(row)
+
+ return data
+
+def get_loan_security_details(filters):
+ security_detail_map = {}
+
+ loan_security_price_map = frappe._dict(frappe.db.sql("""
+ SELECT loan_security, loan_security_price
+ FROM `tabLoan Security Price` t1
+ WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
+ WHERE t1.loan_security = t2.loan_security)
+ """, as_list=1))
+
+ loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
+ 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
+ 'disabled'])
+
+ for security in loan_security_details:
+ security.update({'latest_price': flt(loan_security_price_map.get(security.loan_security))})
+ security_detail_map.setdefault(security.loan_security, security)
+
+ return security_detail_map
+
+def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
+ current_pledges = {}
+ total_value_map = {}
+ applicant_type_map = {}
+ applicant_wise_unpledges = {}
+ conditions = ""
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ unpledges = frappe.db.sql("""
+ SELECT up.applicant, u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY up.applicant, u.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for unpledge in unpledges:
+ applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty)
+
+ pledges = frappe.db.sql("""
+ SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY lp.applicant, p.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for security in pledges:
+ current_pledges.setdefault((security.applicant, security.loan_security), security.qty)
+ total_value_map.setdefault(security.applicant, 0.0)
+ applicant_type_map.setdefault(security.applicant, security.applicant_type)
+
+ current_pledges[(security.applicant, security.loan_security)] -= \
+ applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
+
+ total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
+ * loan_security_details.get(security.loan_security)['latest_price']
+
+ return current_pledges, total_value_map, applicant_type_map
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_interest_report/__init__.py b/erpnext/loan_management/report/loan_interest_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/__init__.py
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js
new file mode 100644
index 0000000..a227b6d
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Interest Report"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json
new file mode 100644
index 0000000..321d606
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2021-01-10 02:03:26.742693",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-10 02:03:26.742693",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Interest Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Interest Accrual",
+ "report_name": "Loan Interest Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
new file mode 100644
index 0000000..aa0325e
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -0,0 +1,124 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from frappe.utils import flt, getdate, add_days
+
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_active_loan_details(filters)
+ return columns, data
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
+ {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
+ {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
+ {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
+ {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100},
+ {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
+ {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_active_loan_details(filters):
+
+ filter_obj = {"status": ("!=", "Closed")}
+ if filters.get('company'):
+ filter_obj.update({'company': filters.get('company')})
+
+ loan_details = frappe.get_all("Loan",
+ fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type",
+ "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid",
+ "total_interest_payable", "written_off_amount", "status"],
+ filters=filter_obj)
+
+ loan_list = [d.loan for d in loan_details]
+
+ sanctioned_amount_map = get_sanctioned_amount_map()
+ penal_interest_rate_map = get_penal_interest_rate_map()
+ payments = get_payments(loan_list)
+ accrual_map = get_interest_accruals(loan_list)
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for loan in loan_details:
+ loan.update({
+ "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
+ "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \
+ - flt(loan.total_interest_payable) - flt(loan.written_off_amount),
+ "total_repayment": flt(payments.get(loan.loan)),
+ "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
+ "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
+ "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
+ "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
+ "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
+ "currency": currency
+ })
+
+ loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ + loan['penalty']
+
+ return loan_details
+
+def get_sanctioned_amount_map():
+ return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"],
+ as_list=1))
+
+def get_payments(loans):
+ return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"],
+ filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1))
+
+def get_interest_accruals(loans):
+ accrual_map = {}
+
+ interest_accruals = frappe.get_all("Loan Interest Accrual",
+ fields=["loan", "interest_amount", "posting_date", "penalty_amount",
+ "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc")
+
+ for entry in interest_accruals:
+ accrual_map.setdefault(entry.loan, {
+ "accrued_interest": 0.0,
+ "undue_interest": 0.0,
+ "interest_outstanding": 0.0,
+ "last_accrual_date": '',
+ "due_date": ''
+ })
+
+ if entry.accrual_type == 'Regular':
+ if not accrual_map[entry.loan]['due_date']:
+ accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1)
+ if not accrual_map[entry.loan]['last_accrual_date']:
+ accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date
+
+ due_date = accrual_map[entry.loan]['due_date']
+ last_accrual_date = accrual_map[entry.loan]['last_accrual_date']
+
+ if due_date and getdate(entry.posting_date) < getdate(due_date):
+ accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount
+ else:
+ accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount
+
+ accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
+
+ if last_accrual_date and getdate(entry.posting_date) == last_accrual_date:
+ accrual_map[entry.loan]["penalty"] = entry.penalty_amount
+
+ return accrual_map
+
+def get_penal_interest_rate_map():
+ return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
index b63cc8e..c6f6b99 100644
--- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
@@ -103,7 +103,7 @@
loan_repayments = frappe.get_all("Loan Repayment",
filters = query_filters,
- fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount",
+ fields=["posting_date", "applicant", "name", "against_loan", "payable_amount",
"pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
)
diff --git a/erpnext/loan_management/report/loan_security_exposure/__init__.py b/erpnext/loan_management/report/loan_security_exposure/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/__init__.py
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js
new file mode 100644
index 0000000..777f296
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Security Exposure"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json
new file mode 100644
index 0000000..d4dca08
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-16 08:08:01.694583",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-16 08:08:01.694583",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Security Exposure",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Security",
+ "report_name": "Loan Security Exposure",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
new file mode 100644
index 0000000..3ef10c0
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import erpnext
+from frappe import _
+from frappe.utils import flt
+from six import iteritems
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details, get_applicant_wise_total_loan_security_qty
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_data(filters)
+ return columns, data
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
+ {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
+ {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
+ {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
+ {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
+ {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_data(filters):
+ data = []
+ loan_security_details = get_loan_security_details(filters)
+ current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for security, value in iteritems(current_pledges):
+ row = {}
+ current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price'])
+ row.update(loan_security_details.get(security))
+ row.update({
+ 'total_qty': value['qty'],
+ 'current_value': current_value,
+ 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
+ 'pledged_applicant_count': value['applicant_count'],
+ 'currency': currency
+ })
+
+ data.append(row)
+
+ return data
+
+
+def get_company_wise_loan_security_details(filters, loan_security_details):
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
+ loan_security_details)
+
+ total_portfolio_value = 0
+ security_wise_map = {}
+ for key, qty in iteritems(pledge_values):
+ security_wise_map.setdefault(key[1], {
+ 'qty': 0.0,
+ 'applicant_count': 0.0
+ })
+
+ security_wise_map[key[1]]['qty'] += qty
+ if qty:
+ security_wise_map[key[1]]['applicant_count'] += 1
+
+ total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price'])
+
+ return security_wise_map, total_portfolio_value
+
+
+
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 1c4b7a1..15affd8 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -411,7 +411,7 @@
cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate;
-cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
+cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
get_bom_material_detail(doc, cdt, cdn, false);
};
@@ -419,17 +419,22 @@
if (doc.is_default) cur_frm.set_value("is_active", 1);
};
-var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) {
+var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
+ if (!doc.company) {
+ frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
+ }
+
var d = locals[cdt][cdn];
if (d.item_code) {
return frappe.call({
doc: doc,
method: "get_bom_material_detail",
args: {
- 'item_code': d.item_code,
- 'bom_no': d.bom_no != null ? d.bom_no: '',
+ "company": doc.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no != null ? d.bom_no: '',
"scrap_items": scrap_items,
- 'qty': d.qty,
+ "qty": d.qty,
"stock_qty": d.stock_qty,
"include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom,
@@ -468,7 +473,7 @@
}
if (d.bom_no) {
- frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item"));
+ frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
get_bom_material_detail(doc, cdt, cdn, scrap_items);
} else {
erpnext.bom.calculate_rm_cost(doc);
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6363242..03beedb 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -65,6 +65,10 @@
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
+
+ if not self.company:
+ frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
+
self.clear_operations()
self.validate_main_item()
self.validate_currency()
@@ -125,6 +129,7 @@
self.validate_bom_currecny(item)
ret = self.get_bom_material_detail({
+ "company": self.company,
"item_code": item.item_code,
"item_name": item.item_name,
"bom_no": item.bom_no,
@@ -213,6 +218,7 @@
for d in self.get("items"):
rate = self.get_rm_rate({
+ "company": self.company,
"item_code": d.item_code,
"bom_no": d.bom_no,
"qty": d.qty,
@@ -611,10 +617,20 @@
""" Get weighted average of valuation rate from all warehouses """
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
- for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin`
- where item_code=%s""", args['item_code'], as_dict=1):
- total_qty += flt(d.actual_qty)
- total_value += flt(d.stock_value)
+ item_bins = frappe.db.sql("""
+ select
+ bin.actual_qty, bin.stock_value
+ from
+ `tabBin` bin, `tabWarehouse` warehouse
+ where
+ bin.item_code=%(item)s
+ and bin.warehouse = warehouse.name
+ and warehouse.company=%(company)s""",
+ {"item": args['item_code'], "company": args['company']}, as_dict=1)
+
+ for d in item_bins:
+ total_qty += flt(d.actual_qty)
+ total_value += flt(d.stock_value)
if total_qty:
valuation_rate = total_value / total_qty
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index cc93bf9..ca530bb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -456,10 +456,10 @@
if data and len(data):
dates = [d.posting_datetime for d in data]
- self.actual_start_date = min(dates)
+ self.db_set('actual_start_date', min(dates))
if self.status == "Completed":
- self.actual_end_date = max(dates)
+ self.db_set('actual_end_date', max(dates))
self.set_lead_time()
@@ -725,6 +725,7 @@
args.update(item_data)
args["rate"] = get_bom_item_rate({
+ "company": wo_doc.company,
"item_code": args.get("item_code"),
"qty": args.get("required_qty"),
"uom": args.get("stock_uom"),
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 75ebcbc..1c6758e 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -20,6 +20,7 @@
_("Item") + ":Link/Item:150",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
+ _("BOM UoM") + "::160",
_("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200",
@@ -32,7 +33,7 @@
bom = filters.get("bom")
table = "`tabBOM Item`"
- qty_field = "qty"
+ qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0:
@@ -40,7 +41,6 @@
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
- qty_field = "stock_qty"
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@@ -59,6 +59,7 @@
bom_item.item_code,
bom_item.description ,
bom_item.{qty_field},
+ bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f2e4f72..7b2428e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -742,3 +742,6 @@
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
+erpnext.patches.v13_0.update_project_template_tasks
+erpnext.patches.v13_0.set_company_in_leave_ledger_entry
+erpnext.patches.v13_0.convert_qi_parameter_to_link_field
diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
new file mode 100644
index 0000000..289b6a7
--- /dev/null
+++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter')
+
+ # get all distinct parameters from QI readigs table
+ reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"])
+ reading_params = [d.specification for d in reading_params]
+
+ # get all distinct parameters from QI Template as some may be unused in QI
+ template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"])
+ template_params = [d.specification for d in template_params]
+
+ params = list(set(reading_params + template_params))
+
+ for parameter in params:
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py
new file mode 100644
index 0000000..66857c4
--- /dev/null
+++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py
@@ -0,0 +1,7 @@
+import frappe
+
+def execute():
+ frappe.reload_doc('HR', 'doctype', 'Leave Allocation')
+ frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry')
+ frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""")
+ frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
new file mode 100644
index 0000000..5fa0623
--- /dev/null
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("projects", "doctype", "project_template")
+ frappe.reload_doc("projects", "doctype", "project_template_task")
+ frappe.reload_doc("projects", "doctype", "project_template")
+ frappe.reload_doc("projects", "doctype", "task")
+
+ for template_name in frappe.db.sql("""
+ select
+ name
+ from
+ `tabProject Template` """,
+ as_dict=1):
+
+ template = frappe.get_doc("Project Template", template_name.name)
+ replace_tasks = False
+ new_tasks = []
+ for task in template.tasks:
+ if task.subject:
+ replace_tasks = True
+ new_task = frappe.get_doc(dict(
+ doctype = "Task",
+ subject = task.subject,
+ start = task.start,
+ duration = task.duration,
+ task_weight = task.task_weight,
+ description = task.description,
+ is_template = 1
+ )).insert()
+ new_tasks.append(new_task)
+
+ if replace_tasks:
+ template.tasks = []
+ for tsk in new_tasks:
+ template.append("tasks", {
+ "task": tsk.name,
+ "subject": tsk.subject
+ })
+ template.save()
\ No newline at end of file
diff --git a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
index ad043dd..97e217a 100644
--- a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
+++ b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
@@ -5,11 +5,11 @@
import frappe
def execute():
- # udpate sales cycle
+ # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
- # udpate purchase cycle
+ # update purchase cycle
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 2288a27..45f9aa1 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -3,6 +3,8 @@
var in_progress = false;
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Payroll Entry', {
onload: function (frm) {
if (!frm.doc.posting_date) {
@@ -10,6 +12,7 @@
}
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.events.department_filters(frm);
frm.events.payroll_payable_account_filters(frm);
},
@@ -46,7 +49,7 @@
}
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
}
- if ((frm.doc.employees || []).length) {
+ if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit').then(() => {
@@ -129,21 +132,6 @@
"company": frm.doc.company
}
};
- }),
- frm.set_query("cost_center", function () {
- return {
- filters: {
- "is_group": 0,
- company: frm.doc.company
- }
- };
- }),
- frm.set_query("project", function () {
- return {
- filters: {
- company: frm.doc.company
- }
- };
});
},
@@ -183,6 +171,7 @@
company: function (frm) {
frm.events.clear_employee_table(frm);
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
currency: function (frm) {
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index a25a6e7..6bcd4e0 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -21,6 +21,9 @@
if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True)
+ def validate(self):
+ self.number_of_employees = len(self.employees)
+
def on_submit(self):
self.create_salary_slips()
@@ -113,7 +116,7 @@
for d in employees:
self.append('employees', d)
- self.number_of_employees = len(employees)
+ self.number_of_employees = len(self.employees)
if self.validate_attendance:
return self.validate_employee_attendance()
@@ -145,8 +148,8 @@
"""
self.check_permission('write')
self.created = 1
- emp_list = [d.employee for d in self.get_emp_list()]
- if emp_list:
+ employees = [emp.employee for emp in self.employees]
+ if employees:
args = frappe._dict({
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
"payroll_frequency": self.payroll_frequency,
@@ -160,10 +163,10 @@
"exchange_rate": self.exchange_rate,
"currency": self.currency
})
- if len(emp_list) > 30:
- frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args)
+ if len(employees) > 30:
+ frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
else:
- create_salary_slips_for_employees(emp_list, args, publish_progress=False)
+ create_salary_slips_for_employees(employees, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually
self.reload()
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index d7893d0..46f6cd8 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt
+from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee
@@ -88,6 +88,8 @@
period_start = joining_date
if relieving_date and getdate(relieving_date) < getdate(period_end):
period_end = relieving_date
+ if month_diff(period_end, start_date) > 1:
+ start_date = add_months(start_date, - (month_diff(period_end, start_date)+1))
total_sub_periods, remaining_sub_periods = 0.0, 0.0
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index 8e05bb2..51fb359 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -151,7 +151,6 @@
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
- calculate_totals(frm);
frm.trigger("set_dynamic_labels");
},
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 99d8a83..183ad13 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -143,8 +143,8 @@
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct()
- payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
- return [payroll_based_on, consider_unmarked_attendance_as]
+ ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
+ return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet:
@@ -424,16 +424,19 @@
def calculate_net_pay(self):
if self.salary_structure:
self.calculate_component_amounts("earnings")
- self.gross_pay = self.get_component_totals("earnings")
+ self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
if self.salary_structure:
self.calculate_component_amounts("deductions")
- self.total_deduction = self.get_component_totals("deductions")
- self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.set_loan_repayment()
+ self.set_component_amounts_based_on_payment_days()
+ self.set_net_pay()
+ def set_net_pay(self):
+ self.total_deduction = self.get_component_totals("deductions")
+ self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay)
self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
@@ -455,8 +458,6 @@
else:
self.add_tax_components(payroll_period)
- self.set_component_amounts_based_on_payment_days(component_type)
-
def add_structure_components(self, component_type):
data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type):
@@ -576,7 +577,7 @@
'default_amount': amount if not struct_row.get("is_additional_component") else 0,
'depends_on_payment_days' : struct_row.depends_on_payment_days,
'salary_component' : struct_row.salary_component,
- 'abbr' : struct_row.abbr,
+ 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
'additional_salary': additional_salary,
'do_not_include_in_total' : struct_row.do_not_include_in_total,
'is_tax_applicable': struct_row.is_tax_applicable,
@@ -813,7 +814,7 @@
cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or
- getdate(self.end_date) > relieving_date
+ (relieving_date and getdate(self.end_date) > relieving_date)
)):
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount"))
@@ -946,15 +947,21 @@
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row
- def get_component_totals(self, component_type):
+ def get_component_totals(self, component_type, depends_on_payment_days=0):
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
total = 0.0
for d in self.get(component_type):
if not d.do_not_include_in_total:
- d.amount = flt(d.amount, d.precision("amount"))
- total += d.amount
+ if depends_on_payment_days:
+ amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
+ else:
+ amount = flt(d.amount, d.precision("amount"))
+ total += amount
return total
- def set_component_amounts_based_on_payment_days(self, component_type):
+ def set_component_amounts_based_on_payment_days(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@@ -964,8 +971,9 @@
if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
- for d in self.get(component_type):
- d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
+ for component_type in ("earnings", "deductions"):
+ for d in self.get(component_type):
+ d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
def set_loan_repayment(self):
self.total_loan_repayment = 0
@@ -1089,17 +1097,17 @@
self.calculate_net_pay()
def set_totals(self):
- self.gross_pay = 0
+ self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet()
else:
- self.total_deduction = 0
+ self.total_deduction = 0.0
if self.earnings:
for earning in self.earnings:
- self.gross_pay += flt(earning.amount)
+ self.gross_pay += flt(earning.amount, earning.precision("amount"))
if self.deductions:
for deduction in self.deductions:
- self.total_deduction += flt(deduction.amount)
+ self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
self.set_base_totals()
@@ -1145,8 +1153,10 @@
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', period_start_date],
- 'end_date' : ['<', period_end_date]})
-
+ 'end_date' : ['<', period_end_date],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
+ })
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
@@ -1160,7 +1170,9 @@
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', first_day_of_the_month],
- 'end_date' : ['<', self.start_date]
+ 'end_date' : ['<', self.start_date],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
})
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index d6fb419..4368c03 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -318,7 +318,7 @@
year_to_date = 0
for slip in salary_slips:
- year_to_date += slip.net_pay
+ year_to_date += flt(slip.net_pay)
self.assertEqual(slip.year_to_date, year_to_date)
def test_tax_for_payroll_period(self):
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index f3cecd9..3cdfcb2 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -2,12 +2,13 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
- "autoname": "field:project_name",
+ "autoname": "naming_series:",
"creation": "2013-03-07 11:55:07",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "naming_series",
"project_name",
"status",
"project_type",
@@ -440,13 +441,24 @@
"fieldtype": "Text",
"label": "Message",
"mandatory_depends_on": "collect_progress"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "PROJ-.####",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
}
],
"icon": "fa fa-puzzle-piece",
"idx": 29,
+ "index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2020-04-08 22:11:14.552615",
+ "modified": "2020-09-02 11:54:01.223620",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -488,5 +500,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "customer",
+ "title_field": "project_name",
"track_seen": 1
}
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 5bbd29c..60f85b0 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -13,6 +13,7 @@
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
class Project(Document):
def get_feed(self):
@@ -54,17 +55,64 @@
self.project_type = template.project_type
# create tasks from template
+ project_tasks = []
+ tmp_task_details = []
for task in template.tasks:
- frappe.get_doc(dict(
- doctype = 'Task',
- subject = task.subject,
- project = self.name,
- status = 'Open',
- exp_start_date = add_days(self.expected_start_date, task.start),
- exp_end_date = add_days(self.expected_start_date, task.start + task.duration),
- description = task.description,
- task_weight = task.task_weight
- )).insert()
+ template_task_details = frappe.get_doc("Task", task.task)
+ tmp_task_details.append(template_task_details)
+ task = self.create_task_from_template(template_task_details)
+ project_tasks.append(task)
+ self.dependency_mapping(tmp_task_details, project_tasks)
+
+ def create_task_from_template(self, task_details):
+ return frappe.get_doc(dict(
+ doctype = 'Task',
+ subject = task_details.subject,
+ project = self.name,
+ status = 'Open',
+ exp_start_date = self.calculate_start_date(task_details),
+ exp_end_date = self.calculate_end_date(task_details),
+ description = task_details.description,
+ task_weight = task_details.task_weight,
+ type = task_details.type,
+ issue = task_details.issue,
+ is_group = task_details.is_group
+ )).insert()
+
+ def calculate_start_date(self, task_details):
+ self.start_date = add_days(self.expected_start_date, task_details.start)
+ self.start_date = update_if_holiday(self.holiday_list, self.start_date)
+ return self.start_date
+
+ def calculate_end_date(self, task_details):
+ self.end_date = add_days(self.start_date, task_details.duration)
+ return update_if_holiday(self.holiday_list, self.end_date)
+
+ def dependency_mapping(self, template_tasks, project_tasks):
+ for template_task in template_tasks:
+ project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
+ project_task = frappe.get_doc("Task", project_task.name)
+ self.check_depends_on_value(template_task, project_task, project_tasks)
+ self.check_for_parent_tasks(template_task, project_task, project_tasks)
+
+ def check_depends_on_value(self, template_task, project_task, project_tasks):
+ if template_task.get("depends_on") and not project_task.get("depends_on"):
+ for child_task in template_task.get("depends_on"):
+ child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.append("depends_on",{
+ "task": corresponding_project_task[0].name
+ })
+ project_task.save()
+
+ def check_for_parent_tasks(self, template_task, project_task, project_tasks):
+ if template_task.get("parent_task") and not project_task.get("parent_task"):
+ parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.parent_task = corresponding_project_task[0].name
+ project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
@@ -493,3 +541,9 @@
project.status = status
project.save()
+
+def update_if_holiday(holiday_list, date):
+ holiday_list = holiday_list or get_holiday_list()
+ while is_holiday(holiday_list, date):
+ date = add_days(date, 1)
+ return date
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 0c4f6f1..d85c826 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -7,60 +7,131 @@
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
-from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
-from erpnext.projects.doctype.project.project import set_project_status
-
-from frappe.utils import getdate
+from erpnext.projects.doctype.project_template.test_project_template import make_project_template
+from erpnext.projects.doctype.project.project import update_if_holiday
+from erpnext.projects.doctype.task.test_task import create_task
+from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase):
- def test_project_with_template(self):
- frappe.db.sql('delete from tabTask where project = "Test Project with Template"')
- frappe.delete_doc('Project', 'Test Project with Template')
+ def test_project_with_template_having_no_parent_and_depend_tasks(self):
+ project_name = "Test Project with Template - No Parent and Dependend Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
- project = get_project('Test Project with Template')
+ task1 = task_exists("Test Template Task with No Parent and Dependency")
+ if not task1:
+ task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
- tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+ template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
- task1 = tasks[0]
- self.assertEqual(task1.subject, 'Task 1')
- self.assertEqual(task1.description, 'Task 1 description')
- self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
- self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
+ self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
+ self.assertEqual(len(tasks), 1)
- self.assertEqual(len(tasks), 4)
- task4 = tasks[3]
- self.assertEqual(task4.subject, 'Task 4')
- self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
+ def test_project_template_having_parent_child_tasks(self):
+ project_name = "Test Project with Template - Tasks with Parent-Child Relation"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
-def get_project(name):
- template = get_project_template()
+ task1 = task_exists("Test Template Task Parent")
+ if not task1:
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
+
+ task2 = task_exists("Test Template Task Child 1")
+ if not task2:
+ task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
+
+ task3 = task_exists("Test Template Task Child 2")
+ if not task3:
+ task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
+
+ template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
+
+ self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
+
+ self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
+ self.assertEqual(tasks[1].parent_task, tasks[0].name)
+
+ self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
+ self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
+ self.assertEqual(tasks[2].parent_task, tasks[0].name)
+
+ self.assertEqual(len(tasks), 3)
+
+ def test_project_template_having_dependent_tasks(self):
+ project_name = "Test Project with Template - Dependent Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
+
+ task1 = task_exists("Test Template Task for Dependency")
+ if not task1:
+ task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
+
+ task2 = task_exists("Test Template Task with Dependency")
+ if not task2:
+ task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
+
+ template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
+
+ self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
+ self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
+
+ self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
+
+ self.assertEqual(len(tasks), 2)
+
+def get_project(name, template):
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = name,
status = 'Open',
project_template = template.name,
- expected_start_date = '2019-01-01'
+ expected_start_date = nowdate()
)).insert()
return project
def make_project(args):
args = frappe._dict(args)
- if args.project_template_name:
- template = make_project_template(args.project_template_name)
- else:
- template = get_project_template()
+
+ if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}):
+ return frappe.get_doc("Project", {"project_name": args.project_name})
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
- project_template = template.name,
expected_start_date = args.start_date
))
- if not frappe.db.exists("Project", args.project_name):
- project.insert()
+ if args.project_template_name:
+ template = make_project_template(args.project_template_name)
+ project.project_template = template.name
- return project
\ No newline at end of file
+ project.insert()
+
+ return project
+
+def task_exists(subject):
+ result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
+ if not len(result):
+ return False
+ return frappe.get_doc("Task", result[0].name)
+
+def calculate_end_date(project, start, duration):
+ start = add_days(project.expected_start_date, start)
+ start = update_if_holiday(project.holiday_list, start)
+ end = add_days(start, duration)
+ end = update_if_holiday(project.holiday_list, end)
+ return getdate(end)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js
index d7a876d..3d3c15c 100644
--- a/erpnext/projects/doctype/project_template/project_template.js
+++ b/erpnext/projects/doctype/project_template/project_template.js
@@ -5,4 +5,23 @@
// refresh: function(frm) {
// }
+ setup: function (frm) {
+ frm.set_query("task", "tasks", function () {
+ return {
+ filters: {
+ "is_template": 1
+ }
+ };
+ });
+ }
+});
+
+frappe.ui.form.on('Project Template Task', {
+ task: function (frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ frappe.db.get_value("Task", row.task, "subject", (value) => {
+ row.subject = value.subject;
+ refresh_field("tasks");
+ });
+ }
});
diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py
index ac78135..aace402 100644
--- a/erpnext/projects/doctype/project_template/project_template.py
+++ b/erpnext/projects/doctype/project_template/project_template.py
@@ -3,8 +3,28 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+import frappe
from frappe.model.document import Document
+from frappe import _
+from frappe.utils import get_link_to_form
class ProjectTemplate(Document):
- pass
+
+ def validate(self):
+ self.validate_dependencies()
+
+ def validate_dependencies(self):
+ for task in self.tasks:
+ task_details = frappe.get_doc("Task", task.task)
+ if task_details.depends_on:
+ for dependency_task in task_details.depends_on:
+ if not self.check_dependent_task_presence(dependency_task.task):
+ task_details_format = get_link_to_form("Task",task_details.name)
+ dependency_task_format = get_link_to_form("Task", dependency_task.task)
+ frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
+
+ def check_dependent_task_presence(self, task):
+ for task_details in self.tasks:
+ if task_details.task == task:
+ return True
+ return False
diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py
index 2c5831a..95663cd 100644
--- a/erpnext/projects/doctype/project_template/test_project_template.py
+++ b/erpnext/projects/doctype/project_template/test_project_template.py
@@ -5,44 +5,25 @@
import frappe
import unittest
+from erpnext.projects.doctype.task.test_task import create_task
class TestProjectTemplate(unittest.TestCase):
pass
-def get_project_template():
- if not frappe.db.exists('Project Template', 'Test Project Template'):
- frappe.get_doc(dict(
- doctype = 'Project Template',
- name = 'Test Project Template',
- tasks = [
- dict(subject='Task 1', description='Task 1 description',
- start=0, duration=3),
- dict(subject='Task 2', description='Task 2 description',
- start=0, duration=2),
- dict(subject='Task 3', description='Task 3 description',
- start=2, duration=4),
- dict(subject='Task 4', description='Task 4 description',
- start=3, duration=2),
- ]
- )).insert()
-
- return frappe.get_doc('Project Template', 'Test Project Template')
-
def make_project_template(project_template_name, project_tasks=[]):
if not frappe.db.exists('Project Template', project_template_name):
- frappe.get_doc(dict(
- doctype = 'Project Template',
- name = project_template_name,
- tasks = project_tasks or [
- dict(subject='Task 1', description='Task 1 description',
- start=0, duration=3),
- dict(subject='Task 2', description='Task 2 description',
- start=0, duration=2),
- dict(subject='Task 3', description='Task 3 description',
- start=2, duration=4),
- dict(subject='Task 4', description='Task 4 description',
- start=3, duration=2),
+ project_tasks = project_tasks or [
+ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
+ create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
]
- )).insert()
+ doc = frappe.get_doc(dict(
+ doctype = 'Project Template',
+ name = project_template_name
+ ))
+ for task in project_tasks:
+ doc.append("tasks",{
+ "task": task.name
+ })
+ doc.insert()
return frappe.get_doc('Project Template', project_template_name)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json
index 8644d89..69530b1 100644
--- a/erpnext/projects/doctype/project_template_task/project_template_task.json
+++ b/erpnext/projects/doctype/project_template_task/project_template_task.json
@@ -1,203 +1,41 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2019-02-18 17:24:41.830096",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "task",
+ "subject"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "columns": 2,
+ "fieldname": "task",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Task",
+ "options": "Task",
+ "reqd": 1
+ },
+ {
+ "columns": 6,
"fieldname": "subject",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
+ "fieldtype": "Read Only",
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Subject",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "start",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Begin On (Days)",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "duration",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Duration (Days)",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "task_weight",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Task Weight",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Subject"
}
],
- "has_web_view": 0,
- "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": "2019-02-18 18:30:22.688966",
+ "links": [],
+ "modified": "2021-01-07 15:13:40.995071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",
- "name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 27f1a71..160cc58 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -12,6 +12,7 @@
"issue",
"type",
"is_group",
+ "is_template",
"column_break0",
"status",
"priority",
@@ -22,9 +23,11 @@
"sb_timeline",
"exp_start_date",
"expected_time",
+ "start",
"column_break_11",
"exp_end_date",
"progress",
+ "duration",
"is_milestone",
"sb_details",
"description",
@@ -112,7 +115,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled"
+ "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled"
},
{
"fieldname": "priority",
@@ -360,6 +363,24 @@
"label": "Completed By",
"no_copy": 1,
"options": "User"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_template",
+ "fieldtype": "Check",
+ "label": "Is Template"
+ },
+ {
+ "depends_on": "is_template",
+ "fieldname": "start",
+ "fieldtype": "Int",
+ "label": "Begin On (Days)"
+ },
+ {
+ "depends_on": "is_template",
+ "fieldname": "duration",
+ "fieldtype": "Int",
+ "label": "Duration (Days)"
}
],
"icon": "fa fa-check",
@@ -367,7 +388,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
- "modified": "2020-07-03 12:36:04.960457",
+ "modified": "2020-12-28 11:32:58.714991",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index fb84094..a2095c9 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,291 +17,312 @@
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
- nsm_parent_field = 'parent_task'
+ nsm_parent_field = 'parent_task'
- def get_feed(self):
- return '{0}: {1}'.format(_(self.status), self.subject)
+ def get_feed(self):
+ return '{0}: {1}'.format(_(self.status), self.subject)
- def get_customer_details(self):
- cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
- if cust:
- ret = {'customer_name': cust and cust[0][0] or ''}
- return ret
+ def get_customer_details(self):
+ cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+ if cust:
+ ret = {'customer_name': cust and cust[0][0] or ''}
+ return ret
- def validate(self):
- self.validate_dates()
- self.validate_parent_project_dates()
- self.validate_progress()
- self.validate_status()
- self.update_depends_on()
+ def validate(self):
+ self.validate_dates()
+ self.validate_parent_project_dates()
+ self.validate_progress()
+ self.validate_status()
+ self.update_depends_on()
+ self.validate_dependencies_for_template_task()
- def validate_dates(self):
- if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
- frappe.bold("Expected End Date")))
+ def validate_dates(self):
+ if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
- if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
- frappe.bold("Actual End Date")))
+ if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
- def validate_parent_project_dates(self):
- if not self.project or frappe.flags.in_test:
- return
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
- expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+ expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
- if expected_end_date:
- validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
- validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+ if expected_end_date:
+ validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
- def validate_status(self):
- if self.status!=self.get_db_value("status") and self.status == "Completed":
- for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+ def validate_status(self):
+ if self.is_template and self.status != "Template":
+ self.status = "Template"
+ if self.status!=self.get_db_value("status") and self.status == "Completed":
+ for d in self.depends_on:
+ if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+ frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
- close_all_assignments(self.doctype, self.name)
+ close_all_assignments(self.doctype, self.name)
- def validate_progress(self):
- if flt(self.progress or 0) > 100:
- frappe.throw(_("Progress % for a task cannot be more than 100."))
+ def validate_progress(self):
+ if flt(self.progress or 0) > 100:
+ frappe.throw(_("Progress % for a task cannot be more than 100."))
- if flt(self.progress) == 100:
- self.status = 'Completed'
+ if flt(self.progress) == 100:
+ self.status = 'Completed'
- if self.status == 'Completed':
- self.progress = 100
+ if self.status == 'Completed':
+ self.progress = 100
- def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
- for d in self.depends_on:
- if d.task and not d.task in depends_on_tasks:
- depends_on_tasks += d.task + ","
- self.depends_on_tasks = depends_on_tasks
+ def validate_dependencies_for_template_task(self):
+ if self.is_template:
+ self.validate_parent_template_task()
+ self.validate_depends_on_tasks()
+
+ def validate_parent_template_task(self):
+ if self.parent_task:
+ if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+ parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
+ frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
+
+ def validate_depends_on_tasks(self):
+ if self.depends_on:
+ for task in self.depends_on:
+ if not frappe.db.get_value("Task", task.task, "is_template"):
+ dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
+ frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
- def update_nsm_model(self):
- frappe.utils.nestedset.update_nsm(self)
+ def update_depends_on(self):
+ depends_on_tasks = self.depends_on_tasks or ""
+ for d in self.depends_on:
+ if d.task and d.task not in depends_on_tasks:
+ depends_on_tasks += d.task + ","
+ self.depends_on_tasks = depends_on_tasks
- def on_update(self):
- self.update_nsm_model()
- self.check_recursion()
- self.reschedule_dependent_tasks()
- self.update_project()
- self.unassign_todo()
- self.populate_depends_on()
+ def update_nsm_model(self):
+ frappe.utils.nestedset.update_nsm(self)
- def unassign_todo(self):
- if self.status == "Completed":
- close_all_assignments(self.doctype, self.name)
- if self.status == "Cancelled":
- clear(self.doctype, self.name)
+ def on_update(self):
+ self.update_nsm_model()
+ self.check_recursion()
+ self.reschedule_dependent_tasks()
+ self.update_project()
+ self.unassign_todo()
+ self.populate_depends_on()
- def update_total_expense_claim(self):
- self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
- where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+ def unassign_todo(self):
+ if self.status == "Completed":
+ close_all_assignments(self.doctype, self.name)
+ if self.status == "Cancelled":
+ clear(self.doctype, self.name)
- def update_time_and_costing(self):
- tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
- sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
- sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
- ,self.name, as_dict=1)[0]
- if self.status == "Open":
- self.status = "Working"
- self.total_costing_amount= tl.total_costing_amount
- self.total_billing_amount= tl.total_billing_amount
- self.actual_time= tl.time
- self.act_start_date= tl.start_date
- self.act_end_date= tl.end_date
+ def update_total_expense_claim(self):
+ self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+ where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
- def update_project(self):
- if self.project and not self.flags.from_project:
- frappe.get_cached_doc("Project", self.project).update_project()
+ def update_time_and_costing(self):
+ tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+ sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+ sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+ ,self.name, as_dict=1)[0]
+ if self.status == "Open":
+ self.status = "Working"
+ self.total_costing_amount= tl.total_costing_amount
+ self.total_billing_amount= tl.total_billing_amount
+ self.actual_time= tl.time
+ self.act_start_date= tl.start_date
+ self.act_end_date= tl.end_date
- def check_recursion(self):
- if self.flags.ignore_recursion_check: return
- check_list = [['task', 'parent'], ['parent', 'task']]
- for d in check_list:
- task_list, count = [self.name], 0
- while (len(task_list) > count ):
- tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
- (d[0], d[1], '%s'), cstr(task_list[count]))
- count = count + 1
- for b in tasks:
- if b[0] == self.name:
- frappe.throw(_("Circular Reference Error"), CircularReferenceError)
- if b[0]:
- task_list.append(b[0])
+ def update_project(self):
+ if self.project and not self.flags.from_project:
+ frappe.get_cached_doc("Project", self.project).update_project()
- if count == 15:
- break
+ def check_recursion(self):
+ if self.flags.ignore_recursion_check: return
+ check_list = [['task', 'parent'], ['parent', 'task']]
+ for d in check_list:
+ task_list, count = [self.name], 0
+ while (len(task_list) > count ):
+ tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+ (d[0], d[1], '%s'), cstr(task_list[count]))
+ count = count + 1
+ for b in tasks:
+ if b[0] == self.name:
+ frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+ if b[0]:
+ task_list.append(b[0])
- def reschedule_dependent_tasks(self):
- end_date = self.exp_end_date or self.act_end_date
- if end_date:
- for task_name in frappe.db.sql("""
- select name from `tabTask` as parent
- where parent.project = %(project)s
- and parent.name in (
- select parent from `tabTask Depends On` as child
- where child.task = %(task)s and child.project = %(project)s)
- """, {'project': self.project, 'task':self.name }, as_dict=1):
- task = frappe.get_doc("Task", task_name.name)
- if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
- task_duration = date_diff(task.exp_end_date, task.exp_start_date)
- task.exp_start_date = add_days(end_date, 1)
- task.exp_end_date = add_days(task.exp_start_date, task_duration)
- task.flags.ignore_recursion_check = True
- task.save()
+ if count == 15:
+ break
- def has_webform_permission(self):
- project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
- if project_user:
- return True
+ def reschedule_dependent_tasks(self):
+ end_date = self.exp_end_date or self.act_end_date
+ if end_date:
+ for task_name in frappe.db.sql("""
+ select name from `tabTask` as parent
+ where parent.project = %(project)s
+ and parent.name in (
+ select parent from `tabTask Depends On` as child
+ where child.task = %(task)s and child.project = %(project)s)
+ """, {'project': self.project, 'task':self.name }, as_dict=1):
+ task = frappe.get_doc("Task", task_name.name)
+ if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+ task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+ task.exp_start_date = add_days(end_date, 1)
+ task.exp_end_date = add_days(task.exp_start_date, task_duration)
+ task.flags.ignore_recursion_check = True
+ task.save()
- def populate_depends_on(self):
- if self.parent_task:
- parent = frappe.get_doc('Task', self.parent_task)
- if not self.name in [row.task for row in parent.depends_on]:
- parent.append("depends_on", {
- "doctype": "Task Depends On",
- "task": self.name,
- "subject": self.subject
- })
- parent.save()
+ def has_webform_permission(self):
+ project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+ if project_user:
+ return True
- def on_trash(self):
- if check_if_child_exists(self.name):
- throw(_("Child Task exists for this Task. You can not delete this Task."))
+ def populate_depends_on(self):
+ if self.parent_task:
+ parent = frappe.get_doc('Task', self.parent_task)
+ if self.name not in [row.task for row in parent.depends_on]:
+ parent.append("depends_on", {
+ "doctype": "Task Depends On",
+ "task": self.name,
+ "subject": self.subject
+ })
+ parent.save()
- self.update_nsm_model()
+ def on_trash(self):
+ if check_if_child_exists(self.name):
+ throw(_("Child Task exists for this Task. You can not delete this Task."))
- def after_delete(self):
- self.update_project()
+ self.update_nsm_model()
- def update_status(self):
- if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
- from datetime import datetime
- if self.exp_end_date < datetime.now().date():
- self.db_set('status', 'Overdue', update_modified=False)
- self.update_project()
+ def after_delete(self):
+ self.update_project()
+
+ def update_status(self):
+ if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+ from datetime import datetime
+ if self.exp_end_date < datetime.now().date():
+ self.db_set('status', 'Overdue', update_modified=False)
+ self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
- child_tasks = frappe.get_all("Task", filters={"parent_task": name})
- child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
- return child_tasks
+ child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+ child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+ return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql(""" select name from `tabProject`
- where %(key)s like %(txt)s
- %(mcond)s
- order by name
- limit %(start)s, %(page_len)s""" % {
- 'key': searchfield,
- 'txt': frappe.db.escape('%' + txt + '%'),
- 'mcond':get_match_cond(doctype),
- 'start': start,
- 'page_len': page_len
- })
+ from erpnext.controllers.queries import get_match_cond
+ return frappe.db.sql(""" select name from `tabProject`
+ where %(key)s like %(txt)s
+ %(mcond)s
+ order by name
+ limit %(start)s, %(page_len)s""" % {
+ 'key': searchfield,
+ 'txt': frappe.db.escape('%' + txt + '%'),
+ 'mcond':get_match_cond(doctype),
+ 'start': start,
+ 'page_len': page_len
+ })
@frappe.whitelist()
def set_multiple_status(names, status):
- names = json.loads(names)
- for name in names:
- task = frappe.get_doc("Task", name)
- task.status = status
- task.save()
+ names = json.loads(names)
+ for name in names:
+ task = frappe.get_doc("Task", name)
+ task.status = status
+ task.save()
def set_tasks_as_overdue():
- tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
- for task in tasks:
- if task.status == "Pending Review":
- if getdate(task.review_date) > getdate(today()):
- continue
- frappe.get_doc("Task", task.name).update_status()
+ tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+ for task in tasks:
+ if task.status == "Pending Review":
+ if getdate(task.review_date) > getdate(today()):
+ continue
+ frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
- def set_missing_values(source, target):
- target.append("time_logs", {
- "hours": source.actual_time,
- "completed": source.status == "Completed",
- "project": source.project,
- "task": source.name
- })
+ def set_missing_values(source, target):
+ target.append("time_logs", {
+ "hours": source.actual_time,
+ "completed": source.status == "Completed",
+ "project": source.project,
+ "task": source.name
+ })
- doclist = get_mapped_doc("Task", source_name, {
- "Task": {
- "doctype": "Timesheet"
- }
- }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+ doclist = get_mapped_doc("Task", source_name, {
+ "Task": {
+ "doctype": "Timesheet"
+ }
+ }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
- return doclist
+ return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
- filters = [['docstatus', '<', '2']]
+ filters = [['docstatus', '<', '2']]
- if task:
- filters.append(['parent_task', '=', task])
- elif parent and not is_root:
- # via expand child
- filters.append(['parent_task', '=', parent])
- else:
- filters.append(['ifnull(`parent_task`, "")', '=', ''])
+ if task:
+ filters.append(['parent_task', '=', task])
+ elif parent and not is_root:
+ # via expand child
+ filters.append(['parent_task', '=', parent])
+ else:
+ filters.append(['ifnull(`parent_task`, "")', '=', ''])
- if project:
- filters.append(['project', '=', project])
+ if project:
+ filters.append(['project', '=', project])
- tasks = frappe.get_list(doctype, fields=[
- 'name as value',
- 'subject as title',
- 'is_group as expandable'
- ], filters=filters, order_by='name')
+ tasks = frappe.get_list(doctype, fields=[
+ 'name as value',
+ 'subject as title',
+ 'is_group as expandable'
+ ], filters=filters, order_by='name')
- # return tasks
- return tasks
+ # return tasks
+ return tasks
@frappe.whitelist()
def add_node():
- from frappe.desk.treeview import make_tree_args
- args = frappe.form_dict
- args.update({
- "name_field": "subject"
- })
- args = make_tree_args(**args)
+ from frappe.desk.treeview import make_tree_args
+ args = frappe.form_dict
+ args.update({
+ "name_field": "subject"
+ })
+ args = make_tree_args(**args)
- if args.parent_task == 'All Tasks' or args.parent_task == args.project:
- args.parent_task = None
+ if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+ args.parent_task = None
- frappe.get_doc(args).insert()
+ frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
- data = json.loads(data)
- new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
- new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+ data = json.loads(data)
+ new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+ new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
- for d in data:
- if not d.get("subject"): continue
- new_doc['subject'] = d.get("subject")
- new_task = frappe.get_doc(new_doc)
- new_task.insert()
+ for d in data:
+ if not d.get("subject"): continue
+ new_doc['subject'] = d.get("subject")
+ new_task = frappe.get_doc(new_doc)
+ new_task.insert()
def on_doctype_update():
- frappe.db.add_index("Task", ["lft", "rgt"])
+ frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
- if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
- frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
- if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
- frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js
index 941fe97..39734ee 100644
--- a/erpnext/projects/doctype/task/task_list.js
+++ b/erpnext/projects/doctype/task/task_list.js
@@ -20,7 +20,8 @@
"Pending Review": "orange",
"Working": "orange",
"Completed": "green",
- "Cancelled": "dark grey"
+ "Cancelled": "dark grey",
+ "Template": "blue"
}
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
},
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index 47a28fd..0fad5e8 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -30,14 +30,16 @@
})
def test_reschedule_dependent_task(self):
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10))
task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name)
- task2.get("depends_on")[0].project = "_Test Project"
+ task2.get("depends_on")[0].project = project
task2.save()
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
- task3.get("depends_on")[0].project = "_Test Project"
+ task3.get("depends_on")[0].project = project
task3.save()
task1.update({
@@ -97,14 +99,19 @@
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
-def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True):
+def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
if not frappe.db.exists("Task", subject):
task = frappe.new_doc('Task')
task.status = "Open"
task.subject = subject
task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate()
- task.project = project or "_Test Project"
+ task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"})
+ task.is_template = is_template
+ task.start = begin
+ task.duration = duration
+ task.is_group = is_group
+ task.parent_task = parent_task
if save:
task.save()
else:
@@ -116,5 +123,4 @@
})
if save:
task.save()
-
return task
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index a5ce44d..4cb3804 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -89,10 +89,11 @@
def test_timesheet_billing_based_on_project(self):
emp = make_employee("test_employee_6@salary.com")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
- timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company')
+ timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True)
- sales_invoice.project = '_Test Project'
+ sales_invoice.project = project
sales_invoice.submit()
ts = frappe.get_doc('Timesheet', timesheet.name)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index b068245..b123af5 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -134,7 +134,7 @@
});
},
- project: function(frm) {
+ parent_project: function(frm) {
set_project_in_timelog(frm);
},
@@ -168,8 +168,8 @@
},
time_logs_add: function(frm, cdt, cdn) {
- if(frm.doc.project) {
- frappe.model.set_value(cdt, cdn, 'project', frm.doc.project);
+ if(frm.doc.parent_project) {
+ frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
}
var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
@@ -308,7 +308,9 @@
};
function set_project_in_timelog(frm) {
- if(frm.doc.project){
- erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project");
+ if(frm.doc.parent_project) {
+ $.each(frm.doc.time_logs || [], function(i, item) {
+ frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project);
+ });
}
}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json
index 4c2edf4..b286821 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.json
+++ b/erpnext/projects/doctype/timesheet/timesheet.json
@@ -15,7 +15,7 @@
"column_break_3",
"salary_slip",
"status",
- "project",
+ "parent_project",
"employee_detail",
"employee",
"employee_name",
@@ -261,7 +261,7 @@
"read_only": 1
},
{
- "fieldname": "project",
+ "fieldname": "parent_project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
@@ -271,7 +271,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-29 07:50:35.938231",
+ "modified": "2021-01-08 20:51:14.590080",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index 29f3595..649eb45 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -31,15 +31,6 @@
}
}
});
-
- frm.set_query("cost_center", "taxes", function(doc) {
- return {
- filters: {
- 'company': doc.company,
- "is_group": 0
- }
- }
- });
}
},
validate: function(frm) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2c78b41..2628099 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
+frappe.provide('erpnext.accounts.dimensions');
+
erpnext.TransactionController = erpnext.taxes_and_totals.extend({
setup: function() {
this._super();
@@ -106,6 +108,8 @@
if(!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
}
+
+ erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -159,16 +163,6 @@
};
});
}
- if (this.frm.fields_dict["items"].grid.get_field("cost_center")) {
- this.frm.set_query("cost_center", "items", function(doc) {
- return {
- filters: {
- "company": doc.company,
- "is_group": 0
- }
- };
- });
- }
if (this.frm.fields_dict["items"].grid.get_field("expense_account")) {
this.frm.set_query("expense_account", "items", function(doc) {
@@ -543,6 +537,7 @@
company: me.frm.doc.company,
order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos),
+ is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,
diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js
index 560a561..b635adc 100644
--- a/erpnext/public/js/queries.js
+++ b/erpnext/public/js/queries.js
@@ -115,7 +115,26 @@
["Warehouse", "is_group", "=",0]
]
- }
+ };
+ },
+
+ get_filtered_dimensions: function(doc, child_fields, dimension, company) {
+ let account = '';
+
+ child_fields.forEach((field) => {
+ if (!account) {
+ account = doc[field];
+ }
+ });
+
+ return {
+ query: "erpnext.controllers.queries.get_filtered_dimensions",
+ filters: {
+ 'dimension': dimension,
+ 'account': account,
+ 'company': company
+ }
+ };
}
});
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 891bbe5..c39609b 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -194,15 +194,21 @@
add_dimensions: function(report_name, index) {
let filters = frappe.query_reports[report_name].filters;
- erpnext.dimension_filters.forEach((dimension) => {
- let found = filters.some(el => el.fieldname === dimension['fieldname']);
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ callback: function(r) {
+ let accounting_dimensions = r.message[0];
+ accounting_dimensions.forEach((dimension) => {
+ let found = filters.some(el => el.fieldname === dimension['fieldname']);
- if (!found) {
- filters.splice(index, 0 ,{
- "fieldname": dimension["fieldname"],
- "label": __(dimension["label"]),
- "fieldtype": "Link",
- "options": dimension["document_type"]
+ if (!found) {
+ filters.splice(index, 0, {
+ "fieldname": dimension["fieldname"],
+ "label": __(dimension["label"]),
+ "fieldtype": "Link",
+ "options": dimension["document_type"]
+ });
+ }
});
}
});
diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js
index b6720c0..96e1817 100644
--- a/erpnext/public/js/utils/dimension_tree_filter.js
+++ b/erpnext/public/js/utils/dimension_tree_filter.js
@@ -1,54 +1,83 @@
-frappe.provide('frappe.ui.form');
+frappe.provide('erpnext.accounts');
-let default_dimensions = {};
+erpnext.accounts.dimensions = {
+ setup_dimension_filters(frm, doctype) {
+ this.accounting_dimensions = [];
+ this.default_dimensions = {};
+ this.fetch_custom_dimensions(frm, doctype);
+ },
-let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program",
- "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool",
- "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"];
+ fetch_custom_dimensions(frm, doctype) {
+ let me = this;
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ args: {
+ 'with_cost_center_and_project': true
+ },
+ callback: function(r) {
+ me.accounting_dimensions = r.message[0];
+ me.default_dimensions = r.message[1];
+ me.setup_filters(frm, doctype);
+ }
+ });
+ },
-let child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account",
- "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction",
- "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"];
-
-frappe.call({
- method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters",
- callback: function(r) {
- erpnext.dimension_filters = r.message[0];
- default_dimensions = r.message[1];
- }
-});
-
-doctypes_with_dimensions.forEach((doctype) => {
- frappe.ui.form.on(doctype, {
- onload: function(frm) {
- erpnext.dimension_filters.forEach((dimension) => {
+ setup_filters(frm, doctype) {
+ if (this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => {
- if(frappe.meta.has_field(dimension['document_type'], 'is_group')) {
- frm.set_query(dimension['fieldname'], {
- "is_group": 0
- });
- }
+ let parent_fields = [];
+ frappe.meta.get_docfields(doctype).forEach((df) => {
+ if (df.fieldtype === 'Link' && df.options === 'Account') {
+ parent_fields.push(df.fieldname);
+ } else if (df.fieldtype === 'Table') {
+ this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']);
+ }
+
+ if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
+ this.setup_account_filters(frm, dimension['fieldname'], parent_fields);
+ }
+ });
});
});
- },
+ }
+ },
- company: function(frm) {
- if(frm.doc.company && (Object.keys(default_dimensions || {}).length > 0)
- && default_dimensions[frm.doc.company]) {
- frm.trigger('update_dimension');
- }
- },
+ setup_child_filters(frm, doctype, parentfield, dimension) {
+ let fields = [];
- update_dimension: function(frm) {
- erpnext.dimension_filters.forEach((dimension) => {
- if(frm.is_new()) {
- if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0
- && default_dimensions[frm.doc.company]) {
+ if (frappe.meta.has_field(doctype, dimension)) {
+ frappe.model.with_doctype(doctype, () => {
+ frappe.meta.get_docfields(doctype).forEach((df) => {
+ if (df.fieldtype === 'Link' && df.options === 'Account') {
+ fields.push(df.fieldname);
+ }
+ });
- let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']];
+ frm.set_query(dimension, parentfield, function(doc, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company);
+ });
+ });
+ }
+ },
- if(default_dimension) {
+ setup_account_filters(frm, dimension, fields) {
+ frm.set_query(dimension, function(doc) {
+ return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company);
+ });
+ },
+
+ update_dimension(frm, doctype) {
+ if (this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
+ if (frm.is_new()) {
+ if (frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0
+ && this.default_dimensions[frm.doc.company]) {
+
+ let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']];
+
+ if (default_dimension) {
if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
frm.set_value(dimension['fieldname'], default_dimension);
}
@@ -61,23 +90,14 @@
}
});
}
- });
-});
+ },
-child_docs.forEach((doctype) => {
- frappe.ui.form.on(doctype, {
- items_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]);
- });
- },
-
- accounts_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]);
+ copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {
+ if (frappe.meta.has_field(frm.doctype, fieldname) && this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
+ let row = frappe.get_doc(cdt, cdn);
+ frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]);
});
}
- });
-});
\ No newline at end of file
+ }
+};
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
index 5c1c79d..3034370 100644
--- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
@@ -24,9 +24,8 @@
},
{
"fieldname": "reference_invoice",
- "fieldtype": "Link",
- "label": "Reference Invoice",
- "options": "Sales Invoice"
+ "fieldtype": "Data",
+ "label": "Reference Invoice"
},
{
"fieldname": "headers",
@@ -64,7 +63,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-24 21:09:38.882866",
+ "modified": "2021-01-13 12:06:57.253111",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Request Log",
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
index 4dcb22a..db8bda7 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -7,6 +7,7 @@
"field_order": [
"enable",
"section_break_2",
+ "sandbox_mode",
"credentials",
"auth_token",
"token_expiry"
@@ -41,12 +42,18 @@
"label": "Credentials",
"mandatory_depends_on": "enable",
"options": "E Invoice User"
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox_mode",
+ "fieldtype": "Check",
+ "label": "Sandbox Mode"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-12-22 15:34:57.280044",
+ "modified": "2021-01-13 12:04:49.449199",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
index 5b2b31f..7f76493 100644
--- a/erpnext/regional/germany/accounts_controller.py
+++ b/erpnext/regional/germany/accounts_controller.py
@@ -48,9 +48,6 @@
def missing(field_label, regulation):
"""Notify the user that a required field is missing."""
- context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.'
- msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format(
- field_label=frappe.bold(_(field_label)),
- regulation=regulation
- )
- )
+ translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
+ formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
+ msgprint(formatted_msg)
diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py
new file mode 100644
index 0000000..8bd378c
--- /dev/null
+++ b/erpnext/regional/germany/test_accounts_controller.py
@@ -0,0 +1,12 @@
+import frappe
+import unittest
+from erpnext.regional.germany.accounts_controller import validate_regional
+
+
+class TestAccountsController(unittest.TestCase):
+
+ def setUp(self):
+ self.sales_invoice = frappe.get_last_doc('Sales Invoice')
+
+ def test_validate_regional(self):
+ validate_regional(self.sales_invoice)
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 02ce6c1..d0cac90 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -15,7 +15,7 @@
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
-from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date
+from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
@@ -84,29 +84,32 @@
))
def get_party_details(address_name):
- address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
- gstin = address.get('gstin')
+ d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
- gstin_details = get_gstin_details(gstin)
- legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName')
- location = gstin_details.get('AddrLoc') or address.get('city')
- state_code = gstin_details.get('StateCode')
- pincode = gstin_details.get('AddrPncd')
- address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno'))
- address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
- email_id = address.get('email_id')
- phone = address.get('phone')
- # get last 10 digit
- phone = phone.replace(" ", "")[-10:] if phone else ''
+ if (not d.gstin
+ or not d.city
+ or not d.pincode
+ or not d.address_title
+ or not d.address_line1
+ or not d.gst_state_number):
- if state_code == 97:
+ frappe.throw(
+ msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
+ if d.gst_state_number == 97:
# according to einvoice standard
pincode = 999999
return frappe._dict(dict(
- gstin=gstin, legal_name=legal_name, location=location,
- pincode=pincode, state_code=state_code, address_line1=address_line1,
- address_line2=address_line2, email=email_id, phone=phone
+ gstin=d.gstin, legal_name=d.address_title,
+ location=d.city, pincode=d.pincode,
+ state_code=d.gst_state_number,
+ address_line1=d.address_line1,
+ address_line2=d.address_line2
))
def get_gstin_details(gstin):
@@ -127,14 +130,22 @@
return GSPConnector.get_gstin_details(gstin)
def get_overseas_address_details(address_name):
- address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value(
- 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id']
+ address_title, address_line1, address_line2, city = frappe.db.get_value(
+ 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
)
+ if not address_title or not address_line1 or not city:
+ frappe.throw(
+ msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
return frappe._dict(dict(
- gstin='URP', legal_name=address_title, address_line1=address_line1,
- address_line2=address_line2, email=email_id, phone=phone,
- pincode=999999, state_code=96, place_of_supply=96, location=city
+ gstin='URP', legal_name=address_title, location=city,
+ address_line1=address_line1, address_line2=address_line2,
+ pincode=999999, state_code=96, place_of_supply=96
))
def get_item_list(invoice):
@@ -146,9 +157,10 @@
item.update(d.as_dict())
item.sr_no = d.idx
- item.discount_amount = abs(item.discount_amount * item.qty)
- item.description = d.item_name
+ item.description = d.item_name.replace('"', '\\"')
+
item.qty = abs(item.qty)
+ item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty)
item.gross_amount = abs(item.base_amount)
item.taxable_value = abs(item.base_amount)
@@ -156,6 +168,7 @@
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
+ item.serial_no = ""
item = update_item_taxes(invoice, item)
@@ -205,8 +218,8 @@
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = invoice.discount_amount
- invoice_value_details.round_off = invoice.rounding_adjustment
+ invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@@ -272,7 +285,25 @@
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
))
+def validate_mandatory_fields(invoice):
+ if not invoice.company_address:
+ frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
+ if not invoice.customer_address:
+ frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
+ if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
+ title=_('Missing Fields')
+ )
+ if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
+ title=_('Missing Fields')
+ )
+
def make_einvoice(invoice):
+ validate_mandatory_fields(invoice)
+
schema = read_json('einv_template')
transaction_details = get_transaction_details(invoice)
@@ -291,7 +322,10 @@
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
- shipping_details = get_party_details(invoice.shipping_address_name)
+ if invoice.gst_category == 'Overseas':
+ shipping_details = get_overseas_address_details(invoice.shipping_address_name)
+ else:
+ shipping_details = get_party_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
@@ -351,7 +385,7 @@
# remove empty dicts
einvoice.pop(fieldname, None)
continue
-
+
# convert to int or str
if value_type == 'string':
einvoice[fieldname] = str(value)
@@ -383,15 +417,19 @@
class GSPConnector():
def __init__(self, doctype=None, docname=None):
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+ sandbox_mode = self.e_invoice_settings.sandbox_mode
+
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
self.credentials = self.get_credentials()
- self.base_url = 'https://gsp.adaequare.com'
- self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token'
- self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
- self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
- self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ # authenticate url is same for sandbox & live
+ self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
+ self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
+
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
+ self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
+ self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
@@ -727,7 +765,7 @@
_file = frappe.new_doc('File')
_file.update({
- 'file_name': f'QRCode_{docname}.png',
+ 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
'attached_to_doctype': doctype,
'attached_to_name': docname,
'content': 'qrcode',
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 5321a9a..5261984 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -7,7 +7,7 @@
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states
-from erpnext.accounts.utils import get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today
def setup(company=None, patch=True):
@@ -629,15 +629,20 @@
def set_tax_withholding_category(company):
accounts = []
+ fiscal_year = None
abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
if company and tds_account:
accounts = [dict(company=company, account=tds_account)]
- fiscal_year = get_fiscal_year(today(), company=company)[0]
- docs = get_tds_details(accounts, fiscal_year)
+ try:
+ fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
+ except FiscalYearError:
+ pass
+ docs = get_tds_details(accounts, fiscal_year)
+
for d in docs:
try:
doc = frappe.get_doc(d)
@@ -650,11 +655,14 @@
if accounts:
doc.append("accounts", accounts[0])
- # if fiscal year don't match with any of the already entered data, append rate row
- fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
- if not fy_exist:
- doc.append("rates", d.get('rates')[0])
-
+ if fiscal_year:
+ # if fiscal year don't match with any of the already entered data, append rate row
+ fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
+ if not fy_exist:
+ doc.append("rates", d.get('rates')[0])
+
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_mandatory = True
doc.save()
def set_tds_account(docs, company):
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index f256a66..0d82638 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -48,6 +48,9 @@
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
+ if not doc.gst_state:
+ frappe.throw(_("Please Enter GST state"))
+
if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 661e107..5a0d9c9 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -7,7 +7,7 @@
frappe.ui.form.on('Quotation', {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Sales Order': 'Make Sales Order'
+ 'Sales Order': 'Sales Order'
},
frm.set_query("quotation_to", function() {
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 20ae19f..36b584d 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -25,7 +25,6 @@
def validate(self):
super(Quotation, self).validate()
self.set_status()
- self.update_opportunity()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.set_customer_name()
@@ -50,21 +49,20 @@
lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"])
self.customer_name = company_name or lead_name
- def update_opportunity(self):
+ def update_opportunity(self, status):
for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])):
if opportunity:
- self.update_opportunity_status(opportunity)
+ self.update_opportunity_status(status, opportunity)
if self.opportunity:
- self.update_opportunity_status()
+ self.update_opportunity_status(status)
- def update_opportunity_status(self, opportunity=None):
+ def update_opportunity_status(self, status, opportunity=None):
if not opportunity:
opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity)
- opp.status = None
- opp.set_status(update=True)
+ opp.set_status(status=status, update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
@@ -82,7 +80,7 @@
else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
- self.update_opportunity()
+ self.update_opportunity('Lost')
self.update_lead()
self.save()
@@ -95,7 +93,7 @@
self.company, self.base_grand_total, self)
#update enquiry status
- self.update_opportunity()
+ self.update_opportunity('Quotation')
self.update_lead()
def on_cancel(self):
@@ -105,7 +103,7 @@
#update enquiry status
self.set_status(update=True)
- self.update_opportunity()
+ self.update_opportunity('Open')
self.update_lead()
def print_other_charges(self,docname):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 9388e09..1516dd6 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -158,7 +158,6 @@
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True)
- doc.update_opportunity()
def validate_drop_ship(self):
for d in self.get('items'):
@@ -830,56 +829,49 @@
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers:
- po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
- if len(po) == 0:
- doc = get_mapped_doc("Sales Order", source_name, {
- "Sales Order": {
- "doctype": "Purchase Order",
- "field_no_map": [
- "address_display",
- "contact_display",
- "contact_mobile",
- "contact_email",
- "contact_person",
- "taxes_and_charges",
- "shipping_address",
- "terms"
- ],
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Sales Order Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "sales_order_item"],
- ["parent", "sales_order"],
- ["stock_uom", "stock_uom"],
- ["uom", "uom"],
- ["conversion_factor", "conversion_factor"],
- ["delivery_date", "schedule_date"]
- ],
- "field_no_map": [
- "rate",
- "price_list_rate",
- "item_tax_template",
- "discount_percentage",
- "discount_amount",
- "pricing_rules"
- ],
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ doc = get_mapped_doc("Sales Order", source_name, {
+ "Sales Order": {
+ "doctype": "Purchase Order",
+ "field_no_map": [
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "contact_person",
+ "taxes_and_charges",
+ "shipping_address",
+ "terms"
+ ],
+ "validation": {
+ "docstatus": ["=", 1]
}
- }, target_doc, set_missing_values)
+ },
+ "Sales Order Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "sales_order_item"],
+ ["parent", "sales_order"],
+ ["stock_uom", "stock_uom"],
+ ["uom", "uom"],
+ ["conversion_factor", "conversion_factor"],
+ ["delivery_date", "schedule_date"]
+ ],
+ "field_no_map": [
+ "rate",
+ "price_list_rate",
+ "item_tax_template",
+ "discount_percentage",
+ "discount_amount",
+ "pricing_rules"
+ ],
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ }
+ }, target_doc, set_missing_values)
- doc.insert()
- else:
- suppliers =[]
- if suppliers:
+ doc.insert()
frappe.db.commit()
return doc
- else:
- frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
@@ -1094,4 +1086,4 @@
if not total_produced_qty and frappe.flags.in_patch: return
- frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
\ No newline at end of file
+ frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 643e7cf..e259367 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -772,6 +772,59 @@
so.load_from_db()
so.cancel()
+ def test_drop_shipping_partial_order(self):
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
+ update_status as so_update_status
+
+ # make items
+ po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+ po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": po_item1.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": po_item2.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ # create po for only one item
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1.submit()
+
+ self.assertEqual(so.customer, po1.customer)
+ self.assertEqual(po1.items[0].sales_order, so.name)
+ self.assertEqual(po1.items[0].item_code, po_item1.item_code)
+ #test po item length
+ self.assertEqual(len(po1.items), 1)
+
+ # create po for remaining item
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2.submit()
+
+ # teardown
+ so_update_status("Draft", so.name)
+
+ po1.cancel()
+ po2.cancel()
+ so.load_from_db()
+ so.cancel()
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 0e565a3..9089b53 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -74,67 +74,71 @@
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Customer") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Customer") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
} else if (tree_type == "Item") {
- row_values = data.slice(5,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ row_values = data
+ .slice(5, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
entry = {
- 'name':row_name,
- 'values':row_values
- }
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
-
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
- found = true;
- new_datasets.splice(i,1);
- break;
+ let element_found = new_datasets.some((element, index, array)=>{
+ if(element.name == row_name){
+ array.splice(index, 1)
+ return true
}
- }
+ return false
+ })
- if(!found){
+ if (!element_found) {
new_datasets.push(entry);
}
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- }, 500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
- })
+ },
+ });
},
}
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 5f2658c..ee18042 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -7,13 +7,14 @@
frappe.provide("erpnext.stock");
frappe.provide("erpnext.stock.delivery_note");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Delivery Note", {
setup: function(frm) {
frm.custom_make_buttons = {
'Packing Slip': 'Packing Slip',
'Installation Note': 'Installation Note',
- 'Sales Invoice': 'Invoice',
+ 'Sales Invoice': 'Sales Invoice',
'Stock Entry': 'Return',
'Shipment': 'Shipment'
},
@@ -76,7 +77,7 @@
}
});
-
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
print_without_amount: function(frm) {
@@ -318,6 +319,7 @@
company: function(frm) {
frm.trigger("unhide_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
unhide_account_head: function(frm) {
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index 888bc2d..3e81619 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -8,26 +8,32 @@
"field_order": [
"specification",
"value",
+ "non_numeric",
"column_break_3",
+ "min_value",
+ "max_value",
+ "formula_based_criteria",
"acceptance_formula"
],
"fields": [
{
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"print_width": "200px",
"reqd": 1,
- "width": "200px"
+ "width": "100px"
},
{
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
@@ -36,17 +42,45 @@
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
- "in_list_view": 1,
"label": "Acceptance Criteria Formula"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Maximum Value"
+ },
+ {
+ "default": "0",
+ "fieldname": "non_numeric",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Non-Numeric",
+ "width": "80px"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:33:42.421842",
+ "modified": "2021-01-07 21:32:49.866439",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 01edd99..527b0d3 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -2,6 +2,7 @@
// License: GNU General Public License v3. See license.txt
// eslint-disable-next-line
+frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on('Material Request', {
@@ -66,6 +67,12 @@
filters: {'company': doc.company}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload_post_render: function(frm) {
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 0a29fa0..72a3a5e 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -424,6 +424,7 @@
"basic_rate": 1.0
})
se_doc.get("items")[1].update({
+ "item_code": "_Test Item Home Desktop 100",
"qty": 3.0,
"transfer_qty": 3.0,
"s_warehouse": "_Test Warehouse 1 - _TC",
@@ -534,7 +535,7 @@
mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
uom="_Test UOM 1", conversion_factor=12)
-
+
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 218fb9e..57cc350 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -46,6 +46,8 @@
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -75,6 +77,7 @@
company: function(frm) {
frm.trigger("toggle_display_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
toggle_display_account_head: function(frm) {
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 03e3de1..f7565fd 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,55 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", {
+
+ setup: function(frm) {
+ frm.set_query("batch_no", function() {
+ return {
+ filters: {
+ "item": frm.doc.item_code
+ }
+ };
+ });
+
+ // Serial No based on item_code
+ frm.set_query("item_serial_no", function() {
+ let filters = {};
+ if (frm.doc.item_code) {
+ filters = {
+ 'item_code': frm.doc.item_code
+ };
+ }
+ return { filters: filters };
+ });
+
+ // item code based on GRN/DN
+ frm.set_query("item_code", function(doc) {
+ let doctype = doc.reference_type;
+
+ if (doc.reference_type !== "Job Card") {
+ doctype = (doc.reference_type == "Stock Entry") ?
+ "Stock Entry Detail" : doc.reference_type + " Item";
+ }
+
+ if (doc.reference_type && doc.reference_name) {
+ let filters = {
+ "from": doctype,
+ "inspection_type": doc.inspection_type
+ };
+
+ if (doc.reference_type == doctype)
+ filters["reference_name"] = doc.reference_name;
+ else
+ filters["parent"] = doc.reference_name;
+
+ return {
+ query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
+ filters: filters
+ };
+ }
+ });
+ },
+
refresh: function(frm) {
// Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
@@ -31,55 +80,5 @@
}
});
}
- }
-})
-
-// item code based on GRN/DN
-cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
- let doctype = doc.reference_type;
-
- if (doc.reference_type !== "Job Card") {
- doctype = (doc.reference_type == "Stock Entry") ?
- "Stock Entry Detail" : doc.reference_type + " Item";
- }
-
- if (doc.reference_type && doc.reference_name) {
- let filters = {
- "from": doctype,
- "inspection_type": doc.inspection_type
- };
-
- if (doc.reference_type == doctype)
- filters["reference_name"] = doc.reference_name;
- else
- filters["parent"] = doc.reference_name;
-
- return {
- query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
- filters: filters
- };
- }
-},
-
-// Serial No based on item_code
-cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
- var filters = {};
- if (doc.item_code) {
- filters = {
- 'item_code': doc.item_code
- }
- }
- return { filters: filters }
-}
-
-cur_frm.set_query("batch_no", function(doc) {
- return {
- filters: {
- "item": doc.item_code
- }
- }
-})
-
-cur_frm.add_fetch('item_code', 'item_name', 'item_name');
-cur_frm.add_fetch('item_code', 'description', 'description');
-
+ },
+});
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index f6d7619..edfe7e9 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -136,6 +136,7 @@
"width": "50%"
},
{
+ "fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -143,6 +144,7 @@
"read_only": 1
},
{
+ "fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
@@ -236,7 +238,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-19 17:06:05.409963",
+ "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index ae4eb9b..b3acbc5 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
@@ -16,7 +16,7 @@
self.get_item_specification_details()
if self.readings:
- self.set_status_based_on_acceptance_formula()
+ self.inspect_and_set_status()
def get_item_specification_details(self):
if not self.quality_inspection_template:
@@ -29,9 +29,7 @@
parameters = get_template_details(self.quality_inspection_template)
for d in parameters:
child = self.append('readings', {})
- child.specification = d.specification
- child.value = d.value
- child.acceptance_formula = d.acceptance_formula
+ child.update(d)
child.status = "Accepted"
def get_quality_inspection_template(self):
@@ -69,35 +67,98 @@
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
+ conditions = ""
+ if self.batch_no and self.docstatus == 1:
+ conditions += " and t1.batch_no = '%s'"%(self.batch_no)
+
+ if self.docstatus == 2: # if cancel, then remove qi link wherever same name
+ conditions += " and t1.quality_inspection = '%s'"%(self.name)
+
frappe.db.sql("""
- UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2
- SET t1.quality_inspection = %s, t2.modified = %s
- WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name
- """.format(parent_doc=self.reference_type, child_doc=doctype),
+ UPDATE
+ `tab{child_doc}` t1, `tab{parent_doc}` t2
+ SET
+ t1.quality_inspection = %s, t2.modified = %s
+ WHERE
+ t1.parent = %s
+ and t1.item_code = %s
+ and t1.parent = t2.name
+ {conditions}
+ """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(quality_inspection, self.modified, self.reference_name, self.item_code))
- def set_status_based_on_acceptance_formula(self):
+ def inspect_and_set_status(self):
for reading in self.readings:
- if not reading.acceptance_formula: continue
+ if not reading.manual_inspection: # dont auto set status if manual
+ if reading.formula_based_criteria:
+ self.set_status_based_on_acceptance_formula(reading)
+ else:
+ # if not formula based check acceptance values set
+ self.set_status_based_on_acceptance_values(reading)
- condition = reading.acceptance_formula
- data = {}
+ def set_status_based_on_acceptance_values(self, reading):
+ if cint(reading.non_numeric):
+ result = reading.get("reading_value") == reading.get("value")
+ else:
+ # numeric readings
+ result = self.min_max_criteria_passed(reading)
+
+ reading.status = "Accepted" if result else "Rejected"
+
+ def min_max_criteria_passed(self, reading):
+ """Determine whether all readings fall in the acceptable range."""
+ for i in range(1, 11):
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
+ if not result: return False
+ return True
+
+ def set_status_based_on_acceptance_formula(self, reading):
+ if not reading.acceptance_formula:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
+ title=_("Missing Formula"))
+
+ condition = reading.acceptance_formula
+ data = self.get_formula_evaluation_data(reading)
+
+ try:
+ result = frappe.safe_eval(condition, None, data)
+ reading.status = "Accepted" if result else "Rejected"
+ except NameError as e:
+ field = frappe.bold(e.args[0].split()[1])
+ frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
+ .format(reading.idx, field),
+ title=_("Invalid Formula"))
+ except Exception:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
+ title=_("Invalid Formula"))
+
+ def get_formula_evaluation_data(self, reading):
+ data = {}
+ if cint(reading.non_numeric):
+ data = {"reading_value": reading.get("reading_value")}
+ else:
+ # numeric readings
for i in range(1, 11):
field = "reading_" + str(i)
- data[field] = flt(reading.get(field)) or 0
+ data[field] = flt(reading.get(field))
+ data["mean"] = self.calculate_mean(reading)
- try:
- result = frappe.safe_eval(condition, None, data)
- reading.status = "Accepted" if result else "Rejected"
- except SyntaxError:
- frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
- title=_("Invalid Formula"))
- except NameError as e:
- field = frappe.bold(e.args[0].split()[1])
- frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
- .format(reading.idx, field),
- title=_("Invalid Formula"))
+ return data
+ def calculate_mean(self, reading):
+ """Calculate mean of all non-empty readings."""
+ from statistics import mean
+ readings_list = []
+
+ for i in range(1, 11):
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ readings_list.append(flt(reading_value))
+
+ actual_mean = mean(readings_list) if readings_list else 0
+ return actual_mean
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 2c40009..8c5a04b 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -44,24 +44,61 @@
qa.delete()
dn.delete()
+ def test_value_based_qi_readings(self):
+ # Test QI based on acceptance values (Non formula)
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [{
+ "specification": "Iron Content", # numeric reading
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ },
+ {
+ "specification": "Particle Inspection Needed", # non-numeric reading
+ "non_numeric": 1,
+ "value": "Yes",
+ "reading_value": "Yes"
+ }]
+
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
+ readings=readings, do_not_save=True)
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Accepted")
+
+ qa.delete()
+ dn.delete()
+
def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{
- "specification": "Iron Content",
+ "specification": "Iron Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
- "reading_1": 0.4
+ "reading_1": "0.4"
},
{
- "specification": "Calcium Content",
+ "specification": "Calcium Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
- "reading_1": 0.7
+ "reading_1": "0.7"
},
{
- "specification": "Mg Content",
- "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
- "reading_1": 0.5,
- "reading_2": 0.7,
+ "specification": "Mg Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "mean < 0.9",
+ "reading_1": "0.5",
+ "reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues
+ },
+ {
+ "specification": "Calcium Content", # non-numeric reading
+ "formula_based_criteria": 1,
+ "non_numeric": 1,
+ "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
+ "reading_value": "Grade B"
}]
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@@ -72,6 +109,7 @@
self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted")
+ self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete()
dn.delete()
@@ -86,11 +124,20 @@
qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1
qa.inspected_by = frappe.session.user
+ qa.status = args.status or "Accepted"
- readings = args.readings or {"specification": "Size", "status": args.status}
+ if not args.readings:
+ create_quality_inspection_parameter("Size")
+ readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+ else:
+ readings = args.readings
+
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list):
for entry in readings:
+ create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry)
else:
qa.append("readings", readings)
@@ -101,3 +148,11 @@
qa.submit()
return qa
+
+def create_quality_inspection_parameter(parameter):
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
new file mode 100644
index 0000000..47c7e11
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Quality Inspection Parameter', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
new file mode 100644
index 0000000..0b5a9b5
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
@@ -0,0 +1,86 @@
+{
+ "actions": [],
+ "autoname": "field:parameter",
+ "creation": "2020-12-28 17:06:00.254129",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "parameter",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "parameter",
+ "fieldtype": "Data",
+ "label": "Parameter",
+ "unique": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-28 18:06:54.897317",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter",
+ "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": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
new file mode 100644
index 0000000..8678422
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, 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 QualityInspectionParameter(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
new file mode 100644
index 0000000..cefdc08
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestQualityInspectionParameter(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index c1976dd..30ff1fe 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,21 +7,28 @@
"engine": "InnoDB",
"field_order": [
"specification",
- "value",
"status",
+ "value",
+ "non_numeric",
+ "manual_inspection",
"column_break_4",
+ "min_value",
+ "max_value",
+ "formula_based_criteria",
"acceptance_formula",
"section_break_3",
+ "reading_value",
+ "section_break_14",
"reading_1",
"reading_2",
"reading_3",
- "column_break_10",
"reading_4",
+ "column_break_10",
"reading_5",
"reading_6",
- "column_break_14",
"reading_7",
"reading_8",
+ "column_break_14",
"reading_9",
"reading_10"
],
@@ -29,19 +36,20 @@
{
"columns": 3,
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"reqd": 1
},
{
"columns": 2,
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value",
"fieldtype": "Data",
- "in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
@@ -67,7 +75,6 @@
"columns": 1,
"fieldname": "reading_3",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Reading 3",
"oldfieldname": "reading_3",
"oldfieldtype": "Data"
@@ -130,18 +137,21 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Accepted\nRejected"
+ "options": "\nAccepted\nRejected"
},
{
+ "depends_on": "non_numeric",
"fieldname": "section_break_3",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Value Based Inspection"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
"label": "Acceptance Criteria Formula"
@@ -153,12 +163,59 @@
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+ "description": "Applied on each reading.",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+ "description": "Applied on each reading.",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "label": "Maximum Value"
+ },
+ {
+ "depends_on": "non_numeric",
+ "fieldname": "reading_value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Reading Value"
+ },
+ {
+ "depends_on": "eval:!doc.non_numeric",
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break",
+ "label": "Numeric Inspection"
+ },
+ {
+ "default": "0",
+ "fieldname": "non_numeric",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Non-Numeric"
+ },
+ {
+ "default": "0",
+ "description": "Set the status manually.",
+ "fieldname": "manual_inspection",
+ "fieldtype": "Check",
+ "label": "Manual Inspection"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2021-01-07 22:16:53.978410",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index e284846..c5a7974 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -13,6 +13,7 @@
if not template: return []
return frappe.get_all('Item Quality Inspection Parameter',
- fields=["specification", "value", "acceptance_formula"],
+ fields=["specification", "value", "acceptance_formula",
+ "non_numeric", "formula_based_criteria", "min_value", "max_value"],
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx")
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 6db95c4..bd2fce8 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1,6 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.stock");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Stock Entry', {
setup: function(frm) {
@@ -97,6 +98,7 @@
});
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
setup_quality_inspection: function(frm) {
@@ -312,6 +314,8 @@
frm.set_value("letter_head", company_doc.default_letter_head);
}
frm.trigger("toggle_display_account_head");
+
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
},
@@ -524,7 +528,7 @@
})
);
}
-
+
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
@@ -679,7 +683,13 @@
});
refresh_field("items");
- if (!d.serial_no) {
+ let no_batch_serial_number_value = !d.serial_no;
+ if (d.has_batch_no && !d.has_serial_no) {
+ // check only batch_no for batched item
+ no_batch_serial_number_value = !d.batch_no;
+ }
+
+ if (no_batch_serial_number_value) {
erpnext.stock.select_batch_and_serial_no(frm, d);
}
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index ecdd396..9ced492 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1342,9 +1342,6 @@
frappe.MappingMismatchError)
elif self.purpose == "Material Transfer" and self.add_to_transit:
continue
- elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse):
- frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx),
- frappe.MappingMismatchError)
def validate_batch(self):
if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index e2121fc..ac4ed5e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -2,6 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.stock");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Stock Reconciliation", {
onload: function(frm) {
@@ -26,6 +27,12 @@
if (!frm.doc.expense_account) {
frm.trigger("set_expense_account");
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 08f7a83..bf45251 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -74,7 +74,9 @@
update_party_blanket_order(args, out)
- get_price_list_rate(args, item, out)
+ if not doc or cint(doc.get('is_return')) == 0:
+ # get price list rate only if the invoice is not a credit or debit note
+ get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args))
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
index 28410f3..dba2b14 100644
--- a/erpnext/support/desk_page/support/support.json
+++ b/erpnext/support/desk_page/support/support.json
@@ -28,7 +28,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
+ "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Analytics\",\n \"name\": \"Issue Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
- "modified": "2020-08-11 15:49:34.307341",
+ "modified": "2021-01-13 20:15:03.064256",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 62b39cc..02d10a4 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -214,7 +214,7 @@
def before_insert(self):
if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
- self.set_response_and_resolution_time()
+ self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
def set_response_and_resolution_time(self, priority=None, service_level_agreement=None):
service_level_agreement = get_active_service_level_agreement_for(priority=priority,
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index c962dc6..483bb15 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -135,15 +135,19 @@
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
-def make_issue(creation=None, customer=None, index=0):
+def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),
"customer": customer,
"raised_by": "test@example.com",
"description": "Service Level Agreement Issue",
+ "issue_type": issue_type,
+ "priority": priority,
"creation": creation,
- "service_level_agreement_creation": creation
+ "opening_date": creation,
+ "service_level_agreement_creation": creation,
+ "company": "_Test Company"
}).insert(ignore_permissions=True)
return issue
diff --git a/erpnext/support/report/issue_analytics/__init__.py b/erpnext/support/report/issue_analytics/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/__init__.py
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js
new file mode 100644
index 0000000..f87b2c2
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.js
@@ -0,0 +1,141 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Issue Analytics"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "based_on",
+ label: __("Based On"),
+ fieldtype: "Select",
+ options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "range",
+ label: __("Range"),
+ fieldtype: "Select",
+ options: [
+ { "value": "Weekly", "label": __("Weekly") },
+ { "value": "Monthly", "label": __("Monthly") },
+ { "value": "Quarterly", "label": __("Quarterly") },
+ { "value": "Yearly", "label": __("Yearly") }
+ ],
+ default: "Monthly",
+ reqd: 1
+ },
+ {
+ fieldname: "status",
+ label: __("Status"),
+ fieldtype: "Select",
+ options:[
+ {label: __('Open'), value: 'Open'},
+ {label: __('Replied'), value: 'Replied'},
+ {label: __('Resolved'), value: 'Resolved'},
+ {label: __('Closed'), value: 'Closed'}
+ ]
+ },
+ {
+ fieldname: "priority",
+ label: __("Issue Priority"),
+ fieldtype: "Link",
+ options: "Issue Priority"
+ },
+ {
+ fieldname: "customer",
+ label: __("Customer"),
+ fieldtype: "Link",
+ options: "Customer"
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project"
+ },
+ {
+ fieldname: "assigned_to",
+ label: __("Assigned To"),
+ fieldtype: "Link",
+ options: "User"
+ }
+ ],
+ after_datatable_render: function(datatable_obj) {
+ $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click();
+ },
+ get_datatable_options(options) {
+ return Object.assign(options, {
+ checkboxColumn: true,
+ events: {
+ onCheckRow: function(data) {
+ if (data && data.length) {
+ row_name = data[2].content;
+ row_values = data.slice(3).map(function(column) {
+ return column.content;
+ })
+ entry = {
+ 'name': row_name,
+ 'values': row_values
+ }
+
+ let raw_data = frappe.query_report.chart.data;
+ let new_datasets = raw_data.datasets;
+
+ var found = false;
+
+ for(var i=0; i < new_datasets.length; i++){
+ if (new_datasets[i].name == row_name){
+ found = true;
+ new_datasets.splice(i,1);
+ break;
+ }
+ }
+
+ if (!found){
+ new_datasets.push(entry);
+ }
+
+ let new_data = {
+ labels: raw_data.labels,
+ datasets: new_datasets
+ }
+
+ setTimeout(() => {
+ frappe.query_report.chart.update(new_data)
+ },500)
+
+
+ setTimeout(() => {
+ frappe.query_report.chart.draw(true);
+ }, 1000)
+
+ frappe.query_report.raw_chart_data = new_data;
+ }
+ },
+ }
+ });
+ }
+};
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.json b/erpnext/support/report/issue_analytics/issue_analytics.json
new file mode 100644
index 0000000..dd18498
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2020-10-09 19:52:10.227317",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-10-11 19:43:19.358625",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Issue Analytics",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Issue",
+ "report_name": "Issue Analytics",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py
new file mode 100644
index 0000000..0b62915
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.py
@@ -0,0 +1,222 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import iteritems
+from frappe import _, scrub
+from frappe.utils import getdate, flt, add_to_date, add_days
+from erpnext.accounts.utils import get_fiscal_year
+
+def execute(filters=None):
+ return IssueAnalytics(filters).run()
+
+class IssueAnalytics(object):
+ def __init__(self, filters=None):
+ """Issue Analytics Report"""
+ self.filters = frappe._dict(filters or {})
+ self.get_period_date_ranges()
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+
+ return self.columns, self.data, None, self.chart
+
+ def get_columns(self):
+ self.columns = []
+
+ if self.filters.based_on == 'Customer':
+ self.columns.append({
+ 'label': _('Customer'),
+ 'options': 'Customer',
+ 'fieldname': 'customer',
+ 'fieldtype': 'Link',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Assigned To':
+ self.columns.append({
+ 'label': _('User'),
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Type':
+ self.columns.append({
+ 'label': _('Issue Type'),
+ 'fieldname': 'issue_type',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Type',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Priority':
+ self.columns.append({
+ 'label': _('Issue Priority'),
+ 'fieldname': 'priority',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Priority',
+ 'width': 200
+ })
+
+ for end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ self.columns.append({
+ 'label': _(period),
+ 'fieldname': scrub(period),
+ 'fieldtype': 'Int',
+ 'width': 120
+ })
+
+ self.columns.append({
+ 'label': _('Total'),
+ 'fieldname': 'total',
+ 'fieldtype': 'Int',
+ 'width': 120
+ })
+
+ def get_data(self):
+ self.get_issues()
+ self.get_rows()
+
+ def get_period(self, date):
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+ if self.filters.range == 'Weekly':
+ period = 'Week ' + str(date.isocalendar()[1])
+ elif self.filters.range == 'Monthly':
+ period = str(months[date.month - 1])
+ elif self.filters.range == 'Quarterly':
+ period = 'Quarter ' + str(((date.month - 1) // 3) + 1)
+ else:
+ year = get_fiscal_year(date, self.filters.company)
+ period = str(year[0])
+
+ if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly':
+ period += ' ' + str(date.year)
+
+ return period
+
+ def get_period_date_ranges(self):
+ from dateutil.relativedelta import relativedelta, MO
+ from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
+
+ increment = {
+ 'Monthly': 1,
+ 'Quarterly': 3,
+ 'Half-Yearly': 6,
+ 'Yearly': 12
+ }.get(self.filters.range, 1)
+
+ if self.filters.range in ['Monthly', 'Quarterly']:
+ from_date = from_date.replace(day=1)
+ elif self.filters.range == 'Yearly':
+ from_date = get_fiscal_year(from_date)[1]
+ else:
+ from_date = from_date + relativedelta(from_date, weekday=MO(-1))
+
+ self.periodic_daterange = []
+ for dummy in range(1, 53):
+ if self.filters.range == 'Weekly':
+ period_end_date = add_days(from_date, 6)
+ else:
+ period_end_date = add_to_date(from_date, months=increment, days=-1)
+
+ if period_end_date > to_date:
+ period_end_date = to_date
+
+ self.periodic_daterange.append(period_end_date)
+
+ from_date = add_days(period_end_date, 1)
+ if period_end_date == to_date:
+ break
+
+ def get_issues(self):
+ filters = self.get_common_filters()
+ self.field_map = {
+ 'Customer': 'customer',
+ 'Issue Type': 'issue_type',
+ 'Issue Priority': 'priority',
+ 'Assigned To': '_assign'
+ }
+
+ self.entries = frappe.db.get_all('Issue',
+ fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'],
+ filters=filters,
+ debug=1
+ )
+
+ def get_common_filters(self):
+ filters = {}
+ filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+
+ if self.filters.get('assigned_to'):
+ filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+
+ for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ if self.filters.get(entry):
+ filters[entry] = self.filters.get(entry)
+
+ return filters
+
+ def get_rows(self):
+ self.data = []
+ self.get_periodic_data()
+
+ for entity, period_data in iteritems(self.issue_periodic_data):
+ if self.filters.based_on == 'Customer':
+ row = {'customer': entity}
+ elif self.filters.based_on == 'Assigned To':
+ row = {'user': entity}
+ elif self.filters.based_on == 'Issue Type':
+ row = {'issue_type': entity}
+ elif self.filters.based_on == 'Issue Priority':
+ row = {'priority': entity}
+
+ total = 0
+ for end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ amount = flt(period_data.get(period, 0.0))
+ row[scrub(period)] = amount
+ total += amount
+
+ row['total'] = total
+
+ self.data.append(row)
+
+ def get_periodic_data(self):
+ self.issue_periodic_data = frappe._dict()
+
+ for d in self.entries:
+ period = self.get_period(d.get('opening_date'))
+
+ if self.filters.based_on == 'Assigned To':
+ if d._assign:
+ for entry in json.loads(d._assign):
+ self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0)
+ self.issue_periodic_data[entry][period] += 1
+
+ else:
+ field = self.field_map.get(self.filters.based_on)
+ value = d.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0)
+ self.issue_periodic_data[value][period] += 1
+
+ def get_chart_data(self):
+ length = len(self.columns)
+ labels = [d.get('label') for d in self.columns[1:length-1]]
+ self.chart = {
+ 'data': {
+ 'labels': labels,
+ 'datasets': []
+ },
+ 'type': 'line'
+ }
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py
new file mode 100644
index 0000000..432906d
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py
@@ -0,0 +1,211 @@
+from __future__ import unicode_literals
+import unittest
+import frappe
+from frappe.utils import getdate, add_months
+from erpnext.support.report.issue_analytics.issue_analytics import execute
+from erpnext.support.doctype.issue.test_issue import make_issue, create_customer
+from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
+from frappe.desk.form.assign_to import add as add_assignment
+
+months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+class TestIssueAnalytics(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ frappe.db.sql("delete from `tabIssue` where company='_Test Company'")
+ frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
+
+ current_month_date = getdate()
+ last_month_date = add_months(current_month_date, -1)
+ self.current_month = str(months[current_month_date.month - 1]).lower() + '_' + str(current_month_date.year)
+ self.last_month = str(months[last_month_date.month - 1]).lower() + '_' + str(last_month_date.year)
+
+ def test_issue_analytics(self):
+ create_service_level_agreements_for_issues()
+ create_issue_types()
+ create_records()
+
+ self.compare_result_for_customer()
+ self.compare_result_for_issue_type()
+ self.compare_result_for_issue_priority()
+ self.compare_result_for_assignment()
+
+ def compare_result_for_customer(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Customer',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'customer': '__Test Customer 2',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'customer': '__Test Customer 1',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ },
+ {
+ 'customer': '__Test Customer',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_issue_type(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Issue Type',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'issue_type': 'Discomfort',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'issue_type': 'Service Request',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ },
+ {
+ 'issue_type': 'Bug',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_issue_priority(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Issue Priority',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'priority': 'Medium',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ },
+ {
+ 'priority': 'Low',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'priority': 'High',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_assignment(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Assigned To',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'user': 'test@example.com',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ },
+ {
+ 'user': 'test1@example.com',
+ self.last_month: 2.0,
+ self.current_month: 1.0,
+ 'total': 3.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+
+def create_issue_types():
+ for entry in ['Bug', 'Service Request', 'Discomfort']:
+ if not frappe.db.exists('Issue Type', entry):
+ frappe.get_doc({
+ 'doctype': 'Issue Type',
+ '__newname': entry
+ }).insert()
+
+
+def create_records():
+ create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory")
+ create_customer("__Test Customer 1", "_Test SLA Customer Group", "__Test SLA Territory")
+ create_customer("__Test Customer 2", "_Test SLA Customer Group", "__Test SLA Territory")
+
+ current_month_date = getdate()
+ last_month_date = add_months(current_month_date, -1)
+
+ issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug")
+ add_assignment({
+ "assign_to": ["test@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug")
+ add_assignment({
+ "assign_to": ["test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request")
+ add_assignment({
+ "assign_to": ["test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort")
+ add_assignment({
+ "assign_to": ["test@example.com", "test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/__init__.py b/erpnext/support/report/issue_summary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/support/report/issue_summary/__init__.py
diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js
new file mode 100644
index 0000000..684482a
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.js
@@ -0,0 +1,73 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Issue Summary"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "based_on",
+ label: __("Based On"),
+ fieldtype: "Select",
+ options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "status",
+ label: __("Status"),
+ fieldtype: "Select",
+ options:[
+ {label: __('Open'), value: 'Open'},
+ {label: __('Replied'), value: 'Replied'},
+ {label: __('Resolved'), value: 'Resolved'},
+ {label: __('Closed'), value: 'Closed'}
+ ]
+ },
+ {
+ fieldname: "priority",
+ label: __("Issue Priority"),
+ fieldtype: "Link",
+ options: "Issue Priority"
+ },
+ {
+ fieldname: "customer",
+ label: __("Customer"),
+ fieldtype: "Link",
+ options: "Customer"
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project"
+ },
+ {
+ fieldname: "assigned_to",
+ label: __("Assigned To"),
+ fieldtype: "Link",
+ options: "User"
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.json b/erpnext/support/report/issue_summary/issue_summary.json
new file mode 100644
index 0000000..b8a580c
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-10-12 01:01:55.181777",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-10-12 14:54:55.655920",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Issue Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Issue",
+ "report_name": "Issue Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
new file mode 100644
index 0000000..3d73531
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -0,0 +1,353 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import iteritems
+from frappe import _, scrub
+from frappe.utils import flt
+
+def execute(filters=None):
+ return IssueSummary(filters).run()
+
+class IssueSummary(object):
+ def __init__(self, filters=None):
+ self.filters = frappe._dict(filters or {})
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+ self.get_report_summary()
+
+ return self.columns, self.data, None, self.chart, self.report_summary
+
+ def get_columns(self):
+ self.columns = []
+
+ if self.filters.based_on == 'Customer':
+ self.columns.append({
+ 'label': _('Customer'),
+ 'options': 'Customer',
+ 'fieldname': 'customer',
+ 'fieldtype': 'Link',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Assigned To':
+ self.columns.append({
+ 'label': _('User'),
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Type':
+ self.columns.append({
+ 'label': _('Issue Type'),
+ 'fieldname': 'issue_type',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Type',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Priority':
+ self.columns.append({
+ 'label': _('Issue Priority'),
+ 'fieldname': 'priority',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Priority',
+ 'width': 200
+ })
+
+ self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
+ for status in self.statuses:
+ self.columns.append({
+ 'label': _(status),
+ 'fieldname': scrub(status),
+ 'fieldtype': 'Int',
+ 'width': 80
+ })
+
+ self.columns.append({
+ 'label': _('Total Issues'),
+ 'fieldname': 'total_issues',
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.sla_status_map = {
+ 'SLA Failed': 'failed',
+ 'SLA Fulfilled': 'fulfilled',
+ 'SLA Ongoing': 'ongoing'
+ }
+
+ for label, fieldname in self.sla_status_map.items():
+ self.columns.append({
+ 'label': _(label),
+ 'fieldname': fieldname,
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time',
+ 'Avg Resolution Time', 'Avg User Resolution Time']
+
+ for metric in self.metrics:
+ self.columns.append({
+ 'label': _(metric),
+ 'fieldname': scrub(metric),
+ 'fieldtype': 'Duration',
+ 'width': 170
+ })
+
+ def get_data(self):
+ self.get_issues()
+ self.get_rows()
+
+ def get_issues(self):
+ filters = self.get_common_filters()
+ self.field_map = {
+ 'Customer': 'customer',
+ 'Issue Type': 'issue_type',
+ 'Issue Priority': 'priority',
+ 'Assigned To': '_assign'
+ }
+
+ self.entries = frappe.db.get_all('Issue',
+ fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time',
+ 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'],
+ filters=filters
+ )
+
+ def get_common_filters(self):
+ filters = {}
+ filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+
+ if self.filters.get('assigned_to'):
+ filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+
+ for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ if self.filters.get(entry):
+ filters[entry] = self.filters.get(entry)
+
+ return filters
+
+ def get_rows(self):
+ self.data = []
+ self.get_summary_data()
+
+ for entity, data in iteritems(self.issue_summary_data):
+ if self.filters.based_on == 'Customer':
+ row = {'customer': entity}
+ elif self.filters.based_on == 'Assigned To':
+ row = {'user': entity}
+ elif self.filters.based_on == 'Issue Type':
+ row = {'issue_type': entity}
+ elif self.filters.based_on == 'Issue Priority':
+ row = {'priority': entity}
+
+ for status in self.statuses:
+ count = flt(data.get(status, 0.0))
+ row[scrub(status)] = count
+
+ row['total_issues'] = data.get('total_issues', 0.0)
+
+ for sla_status in self.sla_status_map.values():
+ value = flt(data.get(sla_status), 0.0)
+ row[sla_status] = value
+
+ for metric in self.metrics:
+ value = flt(data.get(scrub(metric)), 0.0)
+ row[scrub(metric)] = value
+
+ self.data.append(row)
+
+ def get_summary_data(self):
+ self.issue_summary_data = frappe._dict()
+
+ for d in self.entries:
+ status = d.status
+ agreement_status = scrub(d.agreement_status)
+
+ if self.filters.based_on == 'Assigned To':
+ if d._assign:
+ for entry in json.loads(d._assign):
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[entry][status] += 1
+ self.issue_summary_data[entry][agreement_status] += 1
+ self.issue_summary_data[entry]['total_issues'] += 1
+
+ else:
+ field = self.field_map.get(self.filters.based_on)
+ value = d.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[value][status] += 1
+ self.issue_summary_data[value][agreement_status] += 1
+ self.issue_summary_data[value]['total_issues'] += 1
+
+ self.get_metrics_data()
+
+ def get_metrics_data(self):
+ issues = []
+
+ metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time',
+ 'avg_resolution_time', 'avg_user_resolution_time']
+
+ for entry in self.entries:
+ issues.append(entry.name)
+
+ field = self.field_map.get(self.filters.based_on)
+
+ if issues:
+ if self.filters.based_on == 'Assigned To':
+ assignment_map = frappe._dict()
+ for d in self.entries:
+ if d._assign:
+ for entry in json.loads(d._assign):
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0
+ self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0
+ self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0
+
+ if not assignment_map.get(entry):
+ assignment_map[entry] = 0
+ assignment_map[entry] += 1
+
+ for entry in assignment_map:
+ for metric in metrics_list:
+ self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry))
+
+ else:
+ data = frappe.db.sql("""
+ SELECT
+ {0}, AVG(first_response_time) as avg_frt,
+ AVG(avg_response_time) as avg_resp_time,
+ AVG(total_hold_time) as avg_hold_time,
+ AVG(resolution_time) as avg_resolution_time,
+ AVG(user_resolution_time) as avg_user_resolution_time
+ FROM `tabIssue`
+ WHERE
+ name IN %(issues)s
+ GROUP BY {0}
+ """.format(field), {'issues': issues}, as_dict=1)
+
+ for entry in data:
+ value = entry.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0
+ self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0
+ self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0
+ self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0
+ self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
+
+ def get_chart_data(self):
+ if not self.data:
+ return None
+
+ labels = []
+ open_issues = []
+ replied_issues = []
+ resolved_issues = []
+ closed_issues = []
+
+ entity = self.filters.based_on
+ entity_field = self.field_map.get(entity)
+ if entity == 'Assigned To':
+ entity_field = 'user'
+
+ for entry in self.data:
+ labels.append(entry.get(entity_field))
+ open_issues.append(entry.get('open'))
+ replied_issues.append(entry.get('replied'))
+ resolved_issues.append(entry.get('resolved'))
+ closed_issues.append(entry.get('closed'))
+
+ self.chart = {
+ 'data': {
+ 'labels': labels[:30],
+ 'datasets': [
+ {
+ 'name': 'Open',
+ 'values': open_issues[:30]
+ },
+ {
+ 'name': 'Replied',
+ 'values': replied_issues[:30]
+ },
+ {
+ 'name': 'Resolved',
+ 'values': resolved_issues[:30]
+ },
+ {
+ 'name': 'Closed',
+ 'values': closed_issues[:30]
+ }
+ ]
+ },
+ 'type': 'bar',
+ 'barOptions': {
+ 'stacked': True
+ }
+ }
+
+ def get_report_summary(self):
+ if not self.data:
+ return None
+
+ open_issues = 0
+ replied = 0
+ resolved = 0
+ closed = 0
+
+ for entry in self.data:
+ open_issues += entry.get('open')
+ replied += entry.get('replied')
+ resolved += entry.get('resolved')
+ closed += entry.get('closed')
+
+ self.report_summary = [
+ {
+ 'value': open_issues,
+ 'indicator': 'Red',
+ 'label': _('Open'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': replied,
+ 'indicator': 'Grey',
+ 'label': _('Replied'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': resolved,
+ 'indicator': 'Green',
+ 'label': _('Resolved'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': closed,
+ 'indicator': 'Green',
+ 'label': _('Closed'),
+ 'datatype': 'Int',
+ }
+ ]
+
diff --git a/erpnext/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html
index f92e72e..c562db3 100644
--- a/erpnext/templates/generators/job_opening.html
+++ b/erpnext/templates/generators/job_opening.html
@@ -13,10 +13,21 @@
{%- if description -%}
<div>{{ description }}</div>
{% endif %}
+
+{%- if publish_salary_range -%}
+<div><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}</div>
+{% endif %}
+
<p style='margin-top: 30px'>
- <a class='btn btn-primary'
+ {%- if job_application_route -%}
+ <a class='btn btn-primary'
+ href='/{{job_application_route}}?new=1&job_title={{ doc.name }}'>
+ {{ _("Apply Now") }}</a>
+ {% else %}
+ <a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
+ {% endif %}
</p>
{% endblock %}