Merge branch 'develop' into patient-history-enhancements

This commit is contained in:
Rucha Mahabal 2021-01-22 09:04:37 +05:30 committed by GitHub
commit 7201498801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 3177 additions and 998 deletions

View File

@ -132,16 +132,10 @@ def allow_regional(fn):
return caller return caller
def get_last_membership(): def get_last_membership(member):
'''Returns last membership if exists''' '''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) dict(member=member, paid=1), order_by='to_date desc', limit=1)
return last_membership and last_membership[0] if last_membership:
return last_membership[0]
def is_member():
'''Returns true if the user is still a member'''
last_membership = get_last_membership()
if last_membership and getdate(last_membership.to_date) > getdate():
return True
return False

View File

@ -2,7 +2,6 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension', { frappe.ui.form.on('Accounting Dimension', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query('document_type', () => { frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list; let invalid_doctypes = frappe.model.core_doctypes_list;

View File

@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension):
return all_dimensions return all_dimensions
@frappe.whitelist() @frappe.whitelist()
def get_dimension_filters(): def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql(""" dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type SELECT label, fieldname, document_type
FROM `tabAccounting Dimension` FROM `tabAccounting Dimension`
@ -214,6 +214,18 @@ def get_dimension_filters():
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1) 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 = {} default_dimensions_map = {}
for dimension in default_dimensions: for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {}) default_dimensions_map.setdefault(dimension.company, {})

View File

@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d
class TestAccountingDimension(unittest.TestCase): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.set_user("Administrator") create_dimension()
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()
def test_dimension_against_sales_invoice(self): def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self): def tearDown(self):
disable_dimension() 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(): def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1 = frappe.get_doc("Accounting Dimension", "Department")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', { frappe.ui.form.on('Bank', {
onload: function(frm) { onload: function(frm) {
@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
frm.set_df_property('address_and_contact', 'hidden', 0); frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm); 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);
});
}
}
}); });
@ -41,3 +47,78 @@ let add_fields_to_mapping_table = function (frm) {
frm.fields_dict.bank_transaction_mapping.grid.refresh(); frm.fields_dict.bank_transaction_mapping.grid.refresh();
}; };
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' });
}
};

View File

@ -1,24 +1,9 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', { frappe.ui.form.on('Budget', {
onload: function(frm) { 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() { frm.set_query("account", "accounts", function() {
return { return {
filters: { filters: {
@ -26,16 +11,18 @@ frappe.ui.form.on('Budget', {
report_type: "Profit and Loss", report_type: "Profit and Loss",
is_group: 0 is_group: 0
} }
} };
}) });
frm.set_query("monthly_distribution", function() { frm.set_query("monthly_distribution", function() {
return { return {
filters: { filters: {
fiscal_year: frm.doc.fiscal_year fiscal_year: frm.doc.fiscal_year
} }
} };
}) });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -11,8 +11,10 @@ from frappe.model.meta import get_field_precision
from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled 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_account_currency
from erpnext.accounts.utils import get_fiscal_year 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.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 exclude_from_linked_with = True
class GLEntry(Document): class GLEntry(Document):
@ -39,6 +41,7 @@ class GLEntry(Document):
if not from_repost: if not from_repost:
self.validate_account_details(adv_adj) self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs() self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_frozen_account(self.account, adv_adj) validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj) validate_balance_type(self.account, adv_adj)
@ -76,11 +79,9 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self): def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type") account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts(): for dimension in get_checks_for_pl_and_bs_accounts():
if account_type == "Profit and Loss" \ if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname): if not self.get(dimension.fieldname):
@ -93,6 +94,25 @@ class GLEntry(Document):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.") frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account)) .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): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \

View File

@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", {
} }
} }
}); });
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
voucher_type: function(frm){ voucher_type: function(frm){
@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
this.load_defaults(); this.load_defaults();
this.setup_queries(); this.setup_queries();
this.setup_balance_formatter(); this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
onload_post_render: function() { onload_post_render: function() {
@ -222,15 +225,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
return erpnext.journal_entry.account_query(me.frm); 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) { me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -406,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
} }
} }
cur_frm.cscript.update_totals(doc); cur_frm.cscript.update_totals(doc);
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts');
}, },
}); });

View File

@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Loyalty Program', { frappe.ui.form.on('Loyalty Program', {
setup: function(frm) { setup: function(frm) {
var help_content = var help_content =
@ -46,20 +48,17 @@ frappe.ui.form.on('Loyalty Program', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
};
});
frm.set_value("company", frappe.defaults.get_user_default("Company")); frm.set_value("company", frappe.defaults.get_user_default("Company"));
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { 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.")); 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);
} }
}); });

View File

@ -36,6 +36,8 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange'); frm.page.set_indicator(__('In Progress'), 'orange');
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@ -100,6 +102,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
} }
}) })
} }
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
invoice_type: function(frm) { invoice_type: function(frm) {

View File

@ -1,6 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %} {% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
@ -8,6 +9,8 @@ frappe.ui.form.on('Payment Entry', {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); 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); 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) { setup: function(frm) {
@ -88,15 +91,6 @@ frappe.ui.form.on('Payment Entry', {
} }
}); });
frm.set_query("cost_center", "deductions", function() {
return {
filters: {
"is_group": 0,
"company": frm.doc.company
}
}
});
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") { if (frm.doc.party_type=="Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@ -167,6 +161,7 @@ frappe.ui.form.on('Payment Entry', {
company: function(frm) { company: function(frm) {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
contact_person: function(frm) { contact_person: function(frm) {

View File

@ -8,7 +8,7 @@ from frappe import _
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
get_dimension_filters) get_dimensions)
class PeriodClosingVoucher(AccountsController): class PeriodClosingVoucher(AccountsController):
def validate(self): def validate(self):
@ -58,7 +58,7 @@ class PeriodClosingVoucher(AccountsController):
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension)) 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) pl_accounts = self.get_pl_balances(dimension_fields)

View File

@ -57,6 +57,8 @@ frappe.ui.form.on('POS Profile', {
} }
}; };
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@ -67,6 +69,7 @@ frappe.ui.form.on('POS Profile', {
company: function(frm) { company: function(frm) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
toggle_display_account_head: function(frm) { toggle_display_account_head: function(frm) {

View File

@ -26,6 +26,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}; };
}); });
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
this._super(); this._super();
@ -41,6 +46,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) { if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier'); this.frm.trigger('supplier');
} }
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc) { refresh: function(doc) {
@ -511,15 +518,6 @@ frappe.ui.form.on("Purchase Invoice", {
} }
} }
} }
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
}, },
onload: function(frm) { onload: function(frm) {

View File

@ -5,14 +5,17 @@
cur_frm.pformat.print_heading = 'Invoice'; cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({
setup: function(doc) { setup: function(doc) {
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
this._super(doc); this._super(doc);
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
var me = this; var me = this;
this._super(); this._super();
@ -33,6 +36,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.refresh_fields(); me.frm.refresh_fields();
} }
erpnext.queries.setup_warehouse_query(this.frm); erpnext.queries.setup_warehouse_query(this.frm);
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc, dt, dn) { refresh: function(doc, dt, dn) {
@ -571,15 +575,6 @@ frappe.ui.form.on('Sales Invoice', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("unrealized_profit_loss_account", function() { frm.set_query("unrealized_profit_loss_account", function() {
return { return {
filters: { filters: {

View File

@ -1861,23 +1861,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self): def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
si.items = [] si.items = []
@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(value_details['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual( calculated_invoice_value = \
value_details['TotInvVal'], value_details['AssVal'] + value_details['CgstVal'] \
value_details['AssVal'] + value_details['CgstVal'] + value_details['SgstVal'] + value_details['IgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal']
+ value_details['OthChrg'] - value_details['Discount'] + value_details['OthChrg'] - value_details['Discount']
)
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls']) self.assertTrue(einvoice['EwbDtls'])

View File

@ -1,16 +1,18 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on('Shipping Rule', { frappe.provide('erpnext.accounts.dimensions');
refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
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() { frm.set_query("account", function() {
return { return {
filters: { filters: {

View File

@ -2,6 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.asset"); frappe.provide("erpnext.asset");
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset', { frappe.ui.form.on('Asset', {
onload: function(frm) { onload: function(frm) {
@ -32,13 +33,11 @@ frappe.ui.form.on('Asset', {
}; };
}); });
frm.set_query("cost_center", function() { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
return { },
"filters": {
"company": frm.doc.company, company: function(frm) {
} erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
};
});
}, },
setup: function(frm) { setup: function(frm) {

View File

@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset Value Adjustment', { frappe.ui.form.on('Asset Value Adjustment', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('company', 'cost_center', 'cost_center'); frm.add_fetch('company', 'cost_center', 'cost_center');
@ -13,11 +15,19 @@ frappe.ui.form.on('Asset Value Adjustment', {
} }
}); });
}, },
onload: function(frm) { onload: function(frm) {
if(frm.is_new() && frm.doc.asset) { if(frm.is_new() && frm.doc.asset) {
frm.trigger("set_current_asset_value"); 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) { asset: function(frm) {
frm.trigger("set_current_asset_value"); frm.trigger("set_current_asset_value");
}, },

View File

@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %}; {% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", { frappe.ui.form.on("Purchase Order", {
@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", {
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
set_schedule_date(frm); set_schedule_date(frm);
if (!frm.doc.transaction_date){ if (!frm.doc.transaction_date){
@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
} }
}); });

View File

@ -336,7 +336,7 @@ class BuyingController(StockController):
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0) consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '') consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '') consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty transferred_qty = raw_material.qty

View File

@ -493,6 +493,41 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
'company': filters.get("company", "") '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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Schedule', { frappe.ui.form.on('Fee Schedule', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account'); frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account');
@ -8,6 +9,10 @@ frappe.ui.form.on('Fee Schedule', {
frm.add_fetch('fee_structure', 'cost_center', 'cost_center'); frm.add_fetch('fee_structure', 'cost_center', 'cost_center');
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
frm.set_query('receivable_account', function(doc) { frm.set_query('receivable_account', function(doc) {
return { return {
@ -50,6 +55,8 @@ frappe.ui.form.on('Fee Schedule', {
} }
} }
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -1,6 +1,8 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Structure', { frappe.ui.form.on('Fee Structure', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('company', 'default_receivable_account', 'receivable_account'); frm.add_fetch('company', 'default_receivable_account', 'receivable_account');
@ -8,6 +10,10 @@ frappe.ui.form.on('Fee Structure', {
frm.add_fetch('company', 'cost_center', 'cost_center'); frm.add_fetch('company', 'cost_center', 'cost_center');
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
frm.set_query('academic_term', function() { frm.set_query('academic_term', function() {
return { return {
@ -35,6 +41,8 @@ frappe.ui.form.on('Fee Structure', {
} }
}; };
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Fees", { frappe.ui.form.on("Fees", {
setup: function(frm) { setup: function(frm) {
@ -9,6 +10,10 @@ frappe.ui.form.on("Fees", {
frm.add_fetch("fee_structure", "cost_center", "cost_center"); frm.add_fetch("fee_structure", "cost_center", "cost_center");
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
frm.set_query("academic_term", function() { frm.set_query("academic_term", function() {
return{ return{
@ -45,6 +50,8 @@ frappe.ui.form.on("Fees", {
if (!frm.doc.posting_date) { if (!frm.doc.posting_date) {
frm.doc.posting_date = frappe.datetime.get_today(); frm.doc.posting_date = frappe.datetime.get_today();
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -30,13 +30,10 @@ class PlaidConnector():
access_token = response["access_token"] access_token = response["access_token"]
return 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"] 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_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) # 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", "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes, "country_codes": country_codes,
@ -45,6 +42,20 @@ class PlaidConnector():
} }
} }
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: try:
response = self.client.LinkToken.create(token_request) response = self.client.LinkToken.create(token_request)
except InvalidRequestError: except InvalidRequestError:

View File

@ -12,9 +12,25 @@ frappe.ui.form.on('Plaid Settings', {
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.enabled) { 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); 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 @@ erpnext.integrations.plaidLink = class plaidLink {
this.product = ["auth", "transactions"]; this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename; 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(); 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() { init_plaid() {
const me = this; const me = this;
me.loadScript(me.plaidUrl) me.loadScript(me.plaidUrl)
@ -78,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptError(error) { onScriptError(error) {
frappe.msgprint("There was an issue connecting to Plaid's authentication server"); frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
frappe.msgprint(error); console.log(error);
} }
plaid_success(token, response) { plaid_success(token, response) {

View File

@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True) 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") access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id account_id = related_bank[0].integration_id
else: else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token") access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None account_id = None
@ -228,9 +227,14 @@ def new_bank_transaction(transaction):
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1: if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) enqueue_synchronization()
@frappe.whitelist()
def enqueue_synchronization():
plaid_accounts = frappe.get_all("Bank Account",
filters={"integration_id": ["!=", ""]},
fields=["name", "bank"])
for plaid_account in plaid_accounts: for plaid_account in plaid_accounts:
frappe.enqueue( frappe.enqueue(
@ -238,3 +242,8 @@ def automatic_synchronization():
bank=plaid_account.bank, bank=plaid_account.bank,
bank_account=plaid_account.name 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)

View File

@ -6,3 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass
class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass
class InvalidCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass
class PartyDisabled(frappe.ValidationError):pass class PartyDisabled(frappe.ValidationError):pass
class InvalidAccountDimensionError(frappe.ValidationError): pass
class MandatoryAccountDimensionError(frappe.ValidationError): pass

View File

@ -346,7 +346,8 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.setup.doctype.email_digest.email_digest.send",

View File

@ -2,11 +2,21 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.hr"); frappe.provide("erpnext.hr");
frappe.provide("erpnext.accounts.dimensions");
erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ frappe.ui.form.on('Expense Claim', {
expense_type: function(doc, cdt, cdn) { 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]; var d = locals[cdt][cdn];
if(!doc.company) { if (!frm.doc.company) {
d.expense_type = ""; d.expense_type = "";
frappe.msgprint(__("Please set the Company")); frappe.msgprint(__("Please set the Company"));
this.frm.refresh_fields(); this.frm.refresh_fields();
@ -20,7 +30,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: { args: {
"expense_claim_type": d.expense_type, "expense_claim_type": d.expense_type,
"company": doc.company "company": frm.doc.company
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
@ -32,8 +42,6 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({
} }
}); });
$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm}));
cur_frm.add_fetch('employee', 'company', 'company'); cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee','employee_name','employee_name'); cur_frm.add_fetch('employee','employee_name','employee_name');
cur_frm.add_fetch('expense_type','description','description'); cur_frm.add_fetch('expense_type','description','description');
@ -167,15 +175,6 @@ frappe.ui.form.on("Expense Claim", {
}; };
}); });
frm.set_query("cost_center", "expenses", function() {
return {
filters: {
"company": frm.doc.company,
"is_group": 0
}
};
});
frm.set_query("payable_account", function() { frm.set_query("payable_account", function() {
return { return {
filters: { filters: {

View File

@ -11,15 +11,24 @@
"field_order": [ "field_order": [
"applicant_name", "applicant_name",
"email_id", "email_id",
"phone_number",
"country",
"status", "status",
"column_break_3", "column_break_3",
"job_title", "job_title",
"source", "source",
"source_name", "source_name",
"applicant_rating",
"section_break_6", "section_break_6",
"notes", "notes",
"cover_letter", "cover_letter",
"resume_attachment" "resume_attachment",
"resume_link",
"section_break_16",
"currency",
"column_break_18",
"lower_range",
"upper_range"
], ],
"fields": [ "fields": [
{ {
@ -91,12 +100,65 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Notes", "label": "Notes",
"read_only": 1 "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", "icon": "fa fa-user",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-01-13 16:19:39.113330", "modified": "2020-09-18 12:39:02.557563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Applicant", "name": "Job Applicant",

View File

@ -1,456 +1,188 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:route", "autoname": "field:route",
"beta": 0,
"creation": "2013-01-15 16:13:36", "creation": "2013-01-15 16:13:36",
"custom": 0,
"description": "Description of a Job Opening", "description": "Description of a Job Opening",
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 0,
"engine": "InnoDB", "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": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "job_title", "fieldname": "job_title",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Job Title", "label": "Job Title",
"length": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "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", "label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company", "options": "Company",
"permlevel": 0, "reqd": 1
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"length": 0, "options": "Open\nClosed"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "designation", "fieldname": "designation",
"fieldtype": "Link", "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", "label": "Designation",
"length": 0,
"no_copy": 0,
"options": "Designation", "options": "Designation",
"permlevel": 0, "reqd": 1
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "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", "label": "Department",
"length": 0, "options": "Department"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "staffing_plan", "fieldname": "staffing_plan",
"fieldtype": "Link", "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", "label": "Staffing Plan",
"length": 0,
"no_copy": 0,
"options": "Staffing Plan", "options": "Staffing Plan",
"permlevel": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "staffing_plan", "depends_on": "staffing_plan",
"fieldname": "planned_vacancies", "fieldname": "planned_vacancies",
"fieldtype": "Int", "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", "label": "Planned number of Positions",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "publish", "fieldname": "publish",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Publish on website"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "publish", "depends_on": "publish",
"fieldname": "route", "fieldname": "route",
"fieldtype": "Data", "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", "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,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Job profile, qualifications required etc.", "description": "Job profile, qualifications required etc.",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "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_list_view": 1,
"in_standard_filter": 0, "label": "Description"
"label": "Description", },
"length": 0, {
"no_copy": 0, "fieldname": "column_break_12",
"permlevel": 0, "fieldtype": "Column Break"
"print_hide": 0, },
"print_hide_if_no_value": 0, {
"read_only": 0, "fieldname": "section_break_14",
"remember_last_selected_value": 0, "fieldtype": "Section Break"
"report_hide": 0, },
"reqd": 0, {
"search_index": 0, "collapsible": 1,
"set_only_once": 0, "fieldname": "section_break_16",
"translatable": 0, "fieldtype": "Section Break"
"unique": 0 },
{
"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", "icon": "fa fa-bookmark",
"idx": 1, "idx": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2020-09-18 11:23:29.488923",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-20 15:38:44.705823",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Opening", "name": "Job Opening",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 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, "read": 1,
"report": 0, "role": "Guest"
"role": "Guest",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "ASC"
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -43,9 +43,8 @@ class JobOpening(WebsiteGenerator):
current_count = designation_counts['employee_count'] + designation_counts['job_openings'] current_count = designation_counts['employee_count'] + designation_counts['job_openings']
if self.planned_vacancies <= current_count: if self.planned_vacancies <= current_count:
frappe.throw(_("Job Openings for designation {0} already open \ frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
or hiring completed as per Staffing Plan {1}" self.designation, self.staffing_plan))
.format(self.designation, self.staffing_plan)))
def get_context(self, context): def get_context(self, context):
context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
@ -56,7 +55,8 @@ def get_list_context(context):
context.get_list = get_job_openings 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): 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 = filters or {}
filters.update({ filters.update({

View File

@ -1,9 +1,18 @@
<div class="my-5"> <div class="my-5">
<h3>{{ doc.job_title }}</h3> <h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p> <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> <div>
<a class="btn btn-primary" {%- if doc.job_application_route -%}
href="/job_application?new=1&job_title={{ doc.name }}"> <a class='btn btn-primary'
href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a> {{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</div> </div>
</div> </div>

View File

@ -8,6 +8,8 @@
"allow_print": 0, "allow_print": 0,
"amount": 0.0, "amount": 0.0,
"amount_based_on_field": 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", "creation": "2016-09-10 02:53:16.598314",
"doc_type": "Job Applicant", "doc_type": "Job Applicant",
"docstatus": 0, "docstatus": 0,
@ -17,13 +19,16 @@
"is_standard": 1, "is_standard": 1,
"login_required": 0, "login_required": 0,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2016-12-20 00:21:44.081622", "modified": "2020-10-07 19:27:17.143355",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "job-application", "name": "job-application",
"owner": "Administrator", "owner": "Administrator",
"published": 1, "published": 1,
"route": "job_application", "route": "job_application",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 1, "show_sidebar": 1,
"sidebar_items": [], "sidebar_items": [],
"success_message": "Thank you for applying.", "success_message": "Thank you for applying.",
@ -31,6 +36,7 @@
"title": "Job Application", "title": "Job Application",
"web_form_fields": [ "web_form_fields": [
{ {
"allow_read_on_all_link_options": 0,
"fieldname": "job_title", "fieldname": "job_title",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@ -39,9 +45,11 @@
"max_value": 0, "max_value": 0,
"options": "", "options": "",
"read_only": 1, "read_only": 1,
"reqd": 0 "reqd": 0,
"show_in_filter": 0
}, },
{ {
"allow_read_on_all_link_options": 0,
"fieldname": "applicant_name", "fieldname": "applicant_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@ -49,9 +57,11 @@
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"read_only": 0, "read_only": 0,
"reqd": 1 "reqd": 1,
"show_in_filter": 0
}, },
{ {
"allow_read_on_all_link_options": 0,
"fieldname": "email_id", "fieldname": "email_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@ -60,9 +70,37 @@
"max_value": 0, "max_value": 0,
"options": "Email", "options": "Email",
"read_only": 0, "read_only": 0,
"reqd": 1 "reqd": 1,
"show_in_filter": 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
},
{
"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", "fieldname": "cover_letter",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 0, "hidden": 0,
@ -70,17 +108,93 @@
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"read_only": 0, "read_only": 0,
"reqd": 0 "reqd": 0,
"show_in_filter": 0
}, },
{ {
"fieldname": "resume_attachment", "allow_read_on_all_link_options": 0,
"fieldtype": "Attach", "fieldname": "resume_link",
"fieldtype": "Data",
"hidden": 0, "hidden": 0,
"label": "Resume Attachment", "label": "Resume Link",
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"read_only": 0, "read_only": 0,
"reqd": 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
} }
] ]
} }

View File

@ -23,7 +23,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Reports", "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", "category": "Modules",
@ -38,7 +38,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Loan", "label": "Loan",
"modified": "2020-10-17 12:59:50.336085", "modified": "2021-01-17 07:21:22.092184",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@ -202,7 +202,9 @@ def request_loan_closure(loan, posting_date=None):
# checking greater than 0 as there may be some minor precision error # checking greater than 0 as there may be some minor precision error
if pending_amount < write_off_limit: 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') frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else: else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))

View File

@ -321,7 +321,7 @@ class TestLoan(unittest.TestCase):
self.assertEquals(sum(pledged_qty.values()), 0) self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) 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.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0) self.assertEqual(amounts['interest_amount'], 0)
@ -473,7 +473,7 @@ class TestLoan(unittest.TestCase):
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 5)) 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): def test_partial_unaccrued_interest_payment(self):
pledge = [{ pledge = [{

View File

@ -22,6 +22,7 @@
"paid_principal_amount", "paid_principal_amount",
"column_break_14", "column_break_14",
"interest_amount", "interest_amount",
"total_pending_interest_amount",
"paid_interest_amount", "paid_interest_amount",
"penalty_amount", "penalty_amount",
"section_break_15", "section_break_15",
@ -172,13 +173,19 @@
"hidden": 1, "hidden": 1,
"label": "Last Accrual Date", "label": "Last Accrual Date",
"read_only": 1 "read_only": 1
},
{
"fieldname": "total_pending_interest_amount",
"fieldtype": "Currency",
"label": "Total Pending Interest Amount",
"options": "Company:company:default_currency"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-07 05:49:25.448875", "modified": "2021-01-10 00:15:21.544140",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Interest Accrual", "name": "Loan Interest Accrual",

View File

@ -100,6 +100,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days payable_interest = interest_per_day * no_of_days
pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure')
args = frappe._dict({ args = frappe._dict({
'loan': loan.name, 'loan': loan.name,
'applicant_type': loan.applicant_type, 'applicant_type': loan.applicant_type,
@ -108,7 +110,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
'loan_account': loan.loan_account, 'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount, 'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest, '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, 'process_loan_interest': process_loan_interest,
'posting_date': posting_date, 'posting_date': posting_date,
'accrual_type': accrual_type 'accrual_type': accrual_type
@ -202,6 +205,7 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) 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.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.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest

View File

@ -37,10 +37,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application) create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate())) posting_date=get_first_day(nowdate()))
loan.submit() loan.submit()
first_date = '2019-10-01' first_date = '2019-10-01'
@ -50,11 +48,46 @@ class TestLoanInterestAccrual(unittest.TestCase):
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100) / (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) 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)) 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)

View File

@ -377,7 +377,7 @@ def get_amounts(amounts, against_loan, posting_date):
amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = unaccrued_interest amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date

View File

@ -7,6 +7,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"loan_security", "loan_security",
"loan_security_name",
"loan_security_type", "loan_security_type",
"column_break_2", "column_break_2",
"uom", "uom",
@ -79,10 +80,18 @@
"label": "Loan Security Type", "label": "Loan Security Type",
"options": "Loan Security Type", "options": "Loan Security Type",
"read_only": 1 "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": [], "links": [],
"modified": "2020-06-11 03:41:33.900340", "modified": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Price", "name": "Loan Security Price",

View File

@ -144,17 +144,17 @@
}, },
{ {
"allow_on_submit": 1, "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", "fieldname": "write_off_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Write Off Amount ", "label": "Auto Write Off Amount ",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-26 07:13:55.029811", "modified": "2021-01-17 06:51:26.082879",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Type", "name": "Loan Type",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"loan_security", "loan_security",
"loan_security_name",
"loan_security_type", "loan_security_type",
"loan_security_code", "loan_security_code",
"uom", "uom",
@ -85,11 +86,18 @@
"label": "Post Haircut Amount", "label": "Post Haircut Amount",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "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, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-05 10:07:15.424937", "modified": "2021-01-17 07:41:12.452514",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Pledge", "name": "Pledge",

View File

@ -30,7 +30,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-02-01 08:14:05.845161", "modified": "2021-01-17 03:59:14.494557",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Process Loan Security Shortfall", "name": "Process Loan Security Shortfall",
@ -45,7 +45,9 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"select": 1,
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
@ -57,7 +59,9 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Loan Manager", "role": "Loan Manager",
"select": 1,
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

View File

@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"loan_security", "loan_security",
"loan_security_name",
"qty", "qty",
"loan_security_price", "loan_security_price",
"amount", "amount",
@ -56,12 +57,19 @@
"label": "Post Haircut Amount", "label": "Post Haircut Amount",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-05 10:07:37.542344", "modified": "2021-01-17 07:29:01.671722",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Proposed Pledge", "name": "Proposed Pledge",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"loan_security", "loan_security",
"loan_security_name",
"loan_security_type", "loan_security_type",
"loan_security_code", "loan_security_code",
"haircut", "haircut",
@ -61,12 +62,19 @@
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Haircut", "label": "Haircut",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-05 10:07:28.106961", "modified": "2021-01-17 07:36:20.212342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Unpledge", "name": "Unpledge",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@
"membership_expiry_date", "membership_expiry_date",
"column_break_5", "column_break_5",
"membership_type", "membership_type",
"email",
"email_id", "email_id",
"image", "image",
"customer_section", "customer_section",
@ -64,13 +63,6 @@
"options": "Membership Type", "options": "Membership Type",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User"
},
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
@ -178,7 +170,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-09-16 23:44:13.596948", "modified": "2020-11-09 12:12:10.174647",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Member", "name": "Member",

View File

@ -18,8 +18,6 @@ class Member(Document):
def validate(self): def validate(self):
if self.email:
self.validate_email_type(self.email)
if self.email_id: if self.email_id:
self.validate_email_type(self.email_id) self.validate_email_type(self.email_id)
@ -57,14 +55,16 @@ class Member(Document):
def make_customer_and_link(self): def make_customer_and_link(self):
if self.customer: if self.customer:
frappe.msgprint(_("A customer is already linked to this Member")) frappe.msgprint(_("A customer is already linked to this Member"))
cust = create_customer(frappe._dict({
customer = create_customer(frappe._dict({
'fullname': self.member_name, 'fullname': self.member_name,
'email': self.email_id or self.email, 'email': self.email_id,
'phone': None 'phone': None
})) }))
self.customer = cust self.customer = customer
self.save() self.save()
frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
def get_or_create_member(user_details): def get_or_create_member(user_details):

View File

@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_details_section', 'hidden', false); if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
}) })
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.__islocal)
return;
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", { frm.call({
save: true doc: frm.doc,
}).then(() => { method: "generate_invoice",
args: {save: true},
freeze: true,
freeze_message: __("Creating Membership Invoice"),
callback: function(r) {
if (r.invoice)
frm.reload_doc(); frm.reload_doc();
}
}); });
}); });
@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', {
}, },
onload: function(frm) { onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount'); frm.add_fetch("membership_type", "amount", "amount");
} }
}); });

View File

@ -7,6 +7,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"member", "member",
"member_name",
"membership_type", "membership_type",
"column_break_3", "column_break_3",
"membership_status", "membership_status",
@ -46,6 +47,8 @@
{ {
"fieldname": "membership_status", "fieldname": "membership_status",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Membership Status", "label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled" "options": "New\nCurrent\nExpired\nPending\nCancelled"
}, },
@ -122,11 +125,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Invoice", "label": "Invoice",
"options": "Sales Invoice" "options": "Sales Invoice"
},
{
"fetch_from": "member.member_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-09-19 14:28:11.532696", "modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership", "name": "Membership",
@ -158,7 +168,9 @@
} }
], ],
"restrict_to_domain": "Non Profit", "restrict_to_domain": "Non Profit",
"search_fields": "member, member_name",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "member_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -14,17 +14,26 @@ from erpnext.non_profit.doctype.member.member import create_member
from frappe import _ from frappe import _
import erpnext import erpnext
class Membership(Document): class Membership(Document):
def validate(self): def validate(self):
if not self.member or not frappe.db.exists("Member", self.member): if not self.member or not frappe.db.exists("Member", self.member):
member_name = frappe.get_value('Member', dict(email=frappe.session.user)) # for web forms
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
if user_type == "Website User":
self.create_member_from_website_user()
else:
frappe.throw(_("Please select a Member"))
self.validate_membership_period()
def create_member_from_website_user(self):
member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
if not member_name: if not member_name:
user = frappe.get_doc('User', frappe.session.user) user = frappe.get_doc("User", frappe.session.user)
member = frappe.get_doc(dict( member = frappe.get_doc(dict(
doctype='Member', doctype="Member",
email=frappe.session.user, email_id=frappe.session.user,
membership_type=self.membership_type, membership_type=self.membership_type,
member_name=user.get_fullname() member_name=user.get_fullname()
)).insert(ignore_permissions=True) )).insert(ignore_permissions=True)
@ -33,14 +42,15 @@ class Membership(Document):
if self.get("__islocal"): if self.get("__islocal"):
self.member = member_name self.member = member_name
def validate_membership_period(self):
# get last membership (if active) # get last membership (if active)
last_membership = erpnext.get_last_membership() last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership # if person applied for offline membership
if last_membership and not frappe.session.user == "Administrator": if last_membership and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew # if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
frappe.throw(_('You can only renew if your membership expires within 30 days')) frappe.throw(_("You can only renew if your membership expires within 30 days"))
self.from_date = add_days(last_membership.to_date, 1) self.from_date = add_days(last_membership.to_date, 1)
elif frappe.session.user == "Administrator": elif frappe.session.user == "Administrator":
@ -54,11 +64,16 @@ class Membership(Document):
self.to_date = add_months(self.from_date, 1) self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None): def on_payment_authorized(self, status_changed_to=None):
if status_changed_to in ("Completed", "Authorized"): if status_changed_to not in ("Completed", "Authorized"):
return
self.load_from_db() self.load_from_db()
self.db_set('paid', 1) self.db_set("paid", 1)
settings = frappe.get_doc("Membership Settings")
if settings.enable_invoicing and settings.create_for_web_forms:
self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
def generate_invoice(self, save=True):
def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount): if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@ -66,34 +81,64 @@ class Membership(Document):
frappe.throw(_("An invoice is already linked to this document")) frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member) member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
if not member.customer: if not member.customer:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
if not settings.debit_account: plan = frappe.get_doc("Membership Type", self.membership_type)
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings")) settings = frappe.get_doc("Membership Settings")
self.validate_membership_type_and_settings(plan, settings)
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
invoice = make_invoice(self, member, plan, settings) invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name self.invoice = invoice.name
if with_payment_entry:
self.make_payment_entry(settings, invoice)
if save: if save:
self.save() self.save()
return invoice return invoice
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in {0}").format(settings_link))
if not plan.linked_item:
frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
if not settings.payment_account:
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
get_link_to_form("Membership Type", self.membership_type)))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
pe.paid_to = settings.payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
pe.save(ignore_permissions=True)
pe.submit()
def send_acknowlement(self): def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Membership Settings")
if not settings.send_email: if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings")) frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
get_link_to_form("Membership Settings", "Membership Settings")))
member = frappe.get_doc("Member", self.member) member = frappe.get_doc("Member", self.member)
if not member.email_id:
frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type) plan = frappe.get_doc("Membership Type", self.membership_type)
email = member.email_id if member.email_id else member.email email = member.email_id
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice: if self.invoice and settings.send_invoice:
@ -112,48 +157,56 @@ class Membership(Document):
} }
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
else: else:
frappe.sendmail(**email_args) frappe.sendmail(**email_args)
def generate_and_send_invoice(self): def generate_and_send_invoice(self):
invoice = self.generate_invoice(False) self.generate_invoice(save=False)
self.send_acknowlement() self.send_acknowlement()
def make_invoice(membership, member, plan, settings): def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({ invoice = frappe.get_doc({
'doctype': 'Sales Invoice', "doctype": "Sales Invoice",
'customer': member.customer, "customer": member.customer,
'debit_to': settings.debit_account, "debit_to": settings.debit_account,
'currency': membership.currency, "currency": membership.currency,
'is_pos': 0, "company": settings.company,
'items': [ "is_pos": 0,
"items": [
{ {
'item_code': plan.linked_item, "item_code": plan.linked_item,
'rate': membership.amount, "rate": membership.amount,
'qty': 1 "qty": 1
} }
] ]
}) })
invoice.set_missing_values()
invoice.insert(ignore_permissions=True) invoice.insert(ignore_permissions=True)
invoice.submit() invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
return invoice return invoice
def get_member_based_on_subscription(subscription_id, email): def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={ members = frappe.get_all("Member", filters={
'subscription_id': subscription_id, "subscription_id": subscription_id,
'email_id': email "email_id": email
}, order_by="creation desc") }, order_by="creation desc")
try: try:
return frappe.get_doc("Member", members[0]['name']) return frappe.get_doc("Member", members[0]["name"])
except: except:
return None return None
def verify_signature(data): def verify_signature(data):
signature = frappe.request.headers.get('X-Razorpay-Signature') if frappe.flags.in_test:
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Membership Settings")
key = settings.get_webhook_secret() key = settings.get_webhook_secret()
@ -162,6 +215,7 @@ def verify_signature(data):
controller.verify_signature(data, signature, key) controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs): def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True) data = frappe.request.get_data(as_text=True)
@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e: except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error") log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log) notify_failure(log)
return { 'status': 'Failed', 'reason': e} return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
data = frappe._dict(data) data = frappe._dict(data)
subscription = data.payload.get("subscription", {}).get('entity', {}) subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription) subscription = frappe._dict(subscription)
payment = data.payload.get("payment", {}).get('entity', {}) payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment) payment = frappe._dict(payment)
try: try:
@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member = get_member_based_on_subscription(subscription.id, payment.email) member = get_member_based_on_subscription(subscription.id, payment.email)
if not member: if not member:
member = create_member(frappe._dict({ member = create_member(frappe._dict({
'fullname': payment.email, "fullname": payment.email,
'email': payment.email, "email": payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id) "plan_id": get_plan_from_razorpay_id(subscription.plan_id)
})) }))
member.subscription_id = subscription.id member.subscription_id = subscription.id
member.customer_id = payment.customer_id member.customer_id = payment.customer_id
if subscription.notes and type(subscription.notes) == dict: if subscription.notes and type(subscription.notes) == dict:
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes) member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str: elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes) member.add_comment("Comment", subscription.notes)
@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log) notify_failure(log)
return { 'status': 'Failed', 'reason': e} return { "status": "Failed", "reason": e}
return { 'status': 'Success' } return { "status": "Success" }
def notify_failure(log): def notify_failure(log):
try: try:
content = """Dear System Manager, content = """
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason.
Please check the following error log linked below
Error Log: {0} Error Log: {0}
Regards, Administrator
""".format(get_link_to_form("Error Log", log.name))
Regards,
Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except: except:
pass pass
def get_plan_from_razorpay_id(plan_id): def get_plan_from_razorpay_id(plan_id):
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
try: try:
return plan[0]['name'] return plan[0]["name"]
except: except:
return None return None
def set_expired_status():
frappe.db.sql("""
UPDATE
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))

View File

@ -0,0 +1,15 @@
frappe.listview_settings['Membership'] = {
get_indicator: function(doc) {
if (doc.membership_status == 'New') {
return [__('New'), 'blue', 'membership_status,=,New'];
} else if (doc.membership_status === 'Current') {
return [__('Current'), 'green', 'membership_status,=,Current'];
} else if (doc.membership_status === 'Pending') {
return [__('Pending'), 'yellow', 'membership_status,=,Pending'];
} else if (doc.membership_status === 'Expired') {
return [__('Expired'), 'grey', 'membership_status,=,Expired'];
} else {
return [__('Cancelled'), 'red', 'membership_status,=,Cancelled'];
}
}
};

View File

@ -2,8 +2,110 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
pass def setUp(self):
# Get default company
company = frappe.get_doc("Company", erpnext.get_default_company())
# update membership settings
settings = frappe.get_doc("Membership Settings")
# Enable razorpay
settings.enable_razorpay = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.enable_invoicing = 1
settings.make_payment_entry = 1
settings.company = company.name
settings.payment_account = company.default_cash_account
settings.debit_account = company.default_receivable_account
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
# make test member
self.member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
def test_auto_generate_invoice_and_payment_entry(self):
entry = make_membership(self.member)
# Naive test to see if at all invoice was generated and attached to member
# In any case if details were missing, the invoicing would throw an error
invoice = entry.generate_invoice(save=True)
self.assertEqual(invoice.name, entry.invoice)
def test_renew_within_30_days(self):
# create a membership for two months
# Should work fine
make_membership(self.member, { "from_date": nowdate() })
make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
from frappe.utils.user import add_role
add_role("test@example.com", "Non Profit Manager")
frappe.set_user("test@example.com")
# create next membership with expiry not within 30 days
self.assertRaises(frappe.ValidationError, make_membership, self.member, {
"from_date": add_months(nowdate(), 2),
})
frappe.set_user("Administrator")
# create the same membership but as administrator
make_membership(self.member, {
"from_date": add_months(nowdate(), 2),
"to_date": add_months(nowdate(), 3),
})
def set_config(key, value):
frappe.db.set_value("Membership Settings", None, key, value)
def make_membership(member, payload={}):
data = {
"doctype": "Membership",
"member": member,
"membership_status": "Current",
"membership_type": "_rzpy_test_milythm",
"currency": "INR",
"paid": 1,
"from_date": nowdate(),
"amount": 100
}
data.update(payload)
membership = frappe.get_doc(data)
membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
return membership
def create_item(item_code):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
item.item_name = item_code
item.stock_uom = "Nos"
item.description = item_code
item.item_group = "All Item Groups"
item.is_stock_item = 0
item.save()
else:
item = frappe.get_doc("Item", item_code)
return item

View File

@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", {
}); });
} }
frm.set_query('inv_print_format', function(doc) { frm.set_query("inv_print_format", function() {
return { return {
filters: { filters: {
"doc_type": "Sales Invoice" "doc_type": "Sales Invoice"
@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", {
}; };
}); });
frm.set_query('membership_print_format', function(doc) { frm.set_query("membership_print_format", function() {
return { return {
filters: { filters: {
"doc_type": "Membership" "doc_type": "Membership"
@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", {
}; };
}); });
frm.set_query('debit_account', function(doc) { frm.set_query("debit_account", function() {
return { return {
filters: { filters: {
'account_type': 'Receivable', "account_type": "Receivable",
'is_group': 0, "is_group": 0,
'company': frm.doc.company "company": frm.doc.company
}
};
});
frm.set_query("payment_account", function () {
var account_types = ["Bank", "Cash"];
return {
filters: {
"account_type": ["in", account_types],
"is_group": 0,
"company": frm.doc.company
} }
}; };
}); });

View File

@ -11,9 +11,12 @@
"billing_frequency", "billing_frequency",
"webhook_secret", "webhook_secret",
"column_break_6", "column_break_6",
"enable_auto_invoicing", "enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company", "company",
"debit_account", "debit_account",
"payment_account",
"column_break_9", "column_break_9",
"send_email", "send_email",
"send_invoice", "send_invoice",
@ -58,14 +61,7 @@
"label": "Invoicing" "label": "Invoicing"
}, },
{ {
"default": "0", "depends_on": "eval:doc.enable_invoicing",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "debit_account", "fieldname": "debit_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Debit Account", "label": "Debit Account",
@ -77,7 +73,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:doc.enable_auto_invoicing", "depends_on": "eval:doc.enable_invoicing",
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
@ -86,7 +82,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice", "fieldname": "send_invoice",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Send Invoice with Email" "label": "Send Invoice with Email"
@ -119,11 +115,43 @@
"label": "Email Template", "label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email", "mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template" "options": "Email Template"
},
{
"default": "0",
"fieldname": "enable_invoicing",
"fieldtype": "Check",
"label": "Enable Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "make_payment_entry",
"fieldtype": "Check",
"label": "Make Payment Entry"
},
{
"depends_on": "eval:doc.make_payment_entry",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment To",
"mandatory_depends_on": "eval:doc.make_payment_entry",
"options": "Account"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "create_for_web_forms",
"fieldtype": "Check",
"label": "Auto Create Invoice for Web Forms"
} }
], ],
"index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-08-05 17:26:37.287395", "modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership Settings", "name": "Membership Settings",

View File

@ -3,12 +3,20 @@
frappe.ui.form.on('Membership Type', { frappe.ui.form.on('Membership Type', {
refresh: function (frm) { refresh: function (frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
}); });
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false); if (val) frm.set_df_property('linked_item', 'hidden', false);
}); });
frm.set_query('linked_item', () => {
return {
filters: {
is_stock_item: 0
}
};
});
} }
}); });

View File

@ -5,9 +5,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
import frappe import frappe
from frappe import _
class MembershipType(Document): class MembershipType(Document):
pass def validate(self):
if self.linked_item:
is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
if is_stock_item:
frappe.throw(_("The Linked Item should be a service item"))
def get_membership_type(razorpay_id): def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})

View File

@ -736,8 +736,9 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_reason_for_resignation_in_employee
erpnext.patches.v13_0.update_custom_fields_for_shopify
execute:frappe.delete_doc("Report", "Quoted Item Comparison") execute:frappe.delete_doc("Report", "Quoted Item Comparison")
erpnext.patches.v13_0.update_member_email_address
erpnext.patches.v13_0.update_custom_fields_for_shopify
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy 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.add_po_to_global_search

View File

@ -0,0 +1,23 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
"""add value to email_id column from email"""
if frappe.db.has_column("Member", "email"):
# Get all members
for member in frappe.db.get_all("Member", pluck="name"):
# Check if email_id already exists
if not frappe.db.get_value("Member", member, "email_id"):
# fetch email id from the user linked field email
email = frappe.db.get_value("Member", member, "email")
# Set the value for it
frappe.db.set_value("Member", member, "email_id", email)
if frappe.db.exists("DocType", "Membership Settings"):
rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing")

View File

@ -3,6 +3,8 @@
var in_progress = false; var in_progress = false;
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payroll Entry', { frappe.ui.form.on('Payroll Entry', {
onload: function (frm) { onload: function (frm) {
if (!frm.doc.posting_date) { if (!frm.doc.posting_date) {
@ -10,6 +12,7 @@ frappe.ui.form.on('Payroll Entry', {
} }
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); 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.department_filters(frm);
frm.events.payroll_payable_account_filters(frm); frm.events.payroll_payable_account_filters(frm);
}, },
@ -129,21 +132,6 @@ frappe.ui.form.on('Payroll Entry', {
"company": frm.doc.company "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 @@ frappe.ui.form.on('Payroll Entry', {
company: function (frm) { company: function (frm) {
frm.events.clear_employee_table(frm); frm.events.clear_employee_table(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
currency: function (frm) { currency: function (frm) {

View File

@ -9,6 +9,7 @@
"abbr", "abbr",
"column_break_3", "column_break_3",
"amount", "amount",
"year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"statistical_component", "statistical_component",
@ -226,11 +227,19 @@
{ {
"fieldname": "column_break_24", "fieldname": "column_break_24",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
"fieldname": "year_to_date",
"fieldtype": "Currency",
"label": "Year To Date",
"options": "currency",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-25 13:12:41.081106", "modified": "2021-01-14 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@ -138,11 +138,11 @@ frappe.ui.form.on("Salary Slip", {
}, },
change_grid_labels: function(frm) { change_grid_labels: function(frm) {
frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit",
"tax_on_additional_salary"], frm.doc.currency, "earnings"); "tax_on_additional_salary"];
frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", frm.set_currency_labels(fields, frm.doc.currency, "earnings");
"tax_on_additional_salary"], frm.doc.currency, "deductions"); frm.set_currency_labels(fields, frm.doc.currency, "deductions");
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -584,6 +584,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
"fieldname": "year_to_date", "fieldname": "year_to_date",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Year To Date", "label": "Year To Date",
@ -591,6 +592,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.",
"fieldname": "month_to_date", "fieldname": "month_to_date",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Month To Date", "label": "Month To Date",
@ -616,7 +618,7 @@
"idx": 9, "idx": 9,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-21 23:43:44.959840", "modified": "2021-01-14 13:37:38.180920",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Slip", "name": "Salary Slip",

View File

@ -52,6 +52,7 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay() self.calculate_net_pay()
self.compute_year_to_date() self.compute_year_to_date()
self.compute_month_to_date() self.compute_month_to_date()
self.compute_component_wise_year_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@ -1138,16 +1139,7 @@ class SalarySlip(TransactionBase):
def compute_year_to_date(self): def compute_year_to_date(self):
year_to_date = 0 year_to_date = 0
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) period_start_date, period_end_date = self.get_year_to_date_period()
if payroll_period:
period_start_date = payroll_period.start_date
period_end_date = payroll_period.end_date
else:
# get dates based on fiscal year if no payroll period exists
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
period_start_date = fiscal_year.year_start_date
period_end_date = fiscal_year.year_end_date
salary_slip_sum = frappe.get_list('Salary Slip', salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'], fields = ['sum(net_pay) as sum'],
@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase):
month_to_date += self.net_pay month_to_date += self.net_pay
self.month_to_date = month_to_date self.month_to_date = month_to_date
def compute_component_wise_year_to_date(self):
period_start_date, period_end_date = self.get_year_to_date_period()
for key in ('earnings', 'deductions'):
for component in self.get(key):
year_to_date = 0
component_sum = frappe.db.sql("""
SELECT sum(detail.amount) as sum
FROM `tabSalary Detail` as detail
INNER JOIN `tabSalary Slip` as salary_slip
ON detail.parent = salary_slip.name
WHERE
salary_slip.employee_name = %(employee_name)s
AND detail.salary_component = %(component)s
AND salary_slip.start_date >= %(period_start_date)s
AND salary_slip.end_date < %(period_end_date)s
AND salary_slip.name != %(docname)s
AND salary_slip.docstatus = 1""",
{'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
'period_end_date': period_end_date, 'docname': self.name}
)
year_to_date = flt(component_sum[0][0]) if component_sum else 0.0
year_to_date += component.amount
component.year_to_date = year_to_date
def get_year_to_date_period(self):
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
if payroll_period:
period_start_date = payroll_period.start_date
period_end_date = payroll_period.end_date
else:
# get dates based on fiscal year if no payroll period exists
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
period_start_date = fiscal_year.year_start_date
period_end_date = fiscal_year.year_end_date
return period_start_date, period_end_date
def unlink_ref_doc_from_salary_slip(ref_no): def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no)) where journal_entry=%s and docstatus < 2""", (ref_no))

View File

@ -321,6 +321,38 @@ class TestSalarySlip(unittest.TestCase):
year_to_date += flt(slip.net_pay) year_to_date += flt(slip.net_pay)
self.assertEqual(slip.year_to_date, year_to_date) self.assertEqual(slip.year_to_date, year_to_date)
def test_component_wise_year_to_date_computation(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
applicant = make_employee("test_ytd@salary.com", company="_Test Company")
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
company="_Test Company")
salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
# clear salary slip for this employee
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False, num=3)
salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name":
"test_ytd@salary.com"}, order_by = "posting_date")
year_to_date = dict()
for slip in salary_slips:
doc = frappe.get_doc("Salary Slip", slip.name)
for entry in doc.get("earnings"):
if not year_to_date.get(entry.salary_component):
year_to_date[entry.salary_component] = 0
year_to_date[entry.salary_component] += entry.amount
self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date)
def test_tax_for_payroll_period(self): def test_tax_for_payroll_period(self):
data = {} data = {}
# test the impact of tax exemption declaration, tax exemption proof submission # test the impact of tax exemption declaration, tax exemption proof submission
@ -714,10 +746,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption =
else: else:
return income_tax_slab_name return income_tax_slab_name
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12):
deducted_dates = [] deducted_dates = []
i = 0 i = 0
while i < 12: while i < num:
slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee,
"salary_structure": salary_structure, "frequency": "Monthly"}) "salary_structure": salary_structure, "frequency": "Monthly"})
if i == 0: if i == 0:

View File

@ -70,6 +70,9 @@ frappe.ui.form.on('Salary Structure', {
}); });
}, },
company: function(frm) {
frm.trigger('set_earning_deduction_component');
},
currency: function(frm) { currency: function(frm) {
calculate_totals(frm.doc); calculate_totals(frm.doc);
@ -117,6 +120,7 @@ frappe.ui.form.on('Salary Structure', {
fields_read_only.forEach(function(field) { fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
}); });
frm.trigger('set_earning_deduction_component');
}, },
assign_to_employees:function (frm) { assign_to_employees:function (frm) {

View File

@ -216,8 +216,13 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len,
return frappe.db.sql(""" return frappe.db.sql("""
select t1.salary_component select t1.salary_component
from `tabSalary Component` t1, `tabSalary Component Account` t2 from `tabSalary Component` t1, `tabSalary Component Account` t2
where t1.salary_component = t2.parent where (t1.name = t2.parent
and t1.type = %s and t1.type = %(type)s
and t2.company = %s and t2.company = %(company)s)
or (t1.type = %(type)s
and t1.statistical_component = 1)
order by salary_component order by salary_component
""", (filters['type'], filters['company']) ) """,{
"type": filters['type'],
"company": filters['company']
})

View File

View File

@ -0,0 +1,25 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-01-14 09:56:42.393623",
"custom_format": 0,
"default_print_language": "en",
"disabled": 0,
"doc_type": "Salary Slip",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \" <h3 style=\\\"text-align: right;\\\"><span style=\\\"line-height: 1.42857;\\\">{{doc.name}}</span></h3>\\n<div>\\n <hr style=\\\"text-align: center;\\\">\\n</div> \"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"employee\", \"print_hide\": 0, \"label\": \"Employee\"}, {\"fieldname\": \"company\", \"print_hide\": 0, \"label\": \"Company\"}, {\"fieldname\": \"employee_name\", \"print_hide\": 0, \"label\": \"Employee Name\"}, {\"fieldname\": \"department\", \"print_hide\": 0, \"label\": \"Department\"}, {\"fieldname\": \"designation\", \"print_hide\": 0, \"label\": \"Designation\"}, {\"fieldname\": \"branch\", \"print_hide\": 0, \"label\": \"Branch\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"start_date\", \"print_hide\": 0, \"label\": \"Start Date\"}, {\"fieldname\": \"end_date\", \"print_hide\": 0, \"label\": \"End Date\"}, {\"fieldname\": \"total_working_days\", \"print_hide\": 0, \"label\": \"Working Days\"}, {\"fieldname\": \"leave_without_pay\", \"print_hide\": 0, \"label\": \"Leave Without Pay\"}, {\"fieldname\": \"payment_days\", \"print_hide\": 0, \"label\": \"Payment Days\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"earnings\", \"print_hide\": 0, \"label\": \"Earnings\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"deductions\", \"print_hide\": 0, \"label\": \"Deductions\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depends_on_payment_days\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"gross_pay\", \"print_hide\": 0, \"label\": \"Gross Pay\"}, {\"fieldname\": \"total_deduction\", \"print_hide\": 0, \"label\": \"Total Deduction\"}, {\"fieldname\": \"net_pay\", \"print_hide\": 0, \"label\": \"Net Pay\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"total_in_words\", \"print_hide\": 0, \"label\": \"Total in words\"}, {\"fieldtype\": \"Section Break\", \"label\": \"net pay info\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"year_to_date\", \"print_hide\": 0, \"label\": \"Year To Date\"}, {\"fieldname\": \"month_to_date\", \"print_hide\": 0, \"label\": \"Month To Date\"}]",
"idx": 0,
"line_breaks": 0,
"modified": "2021-01-14 10:03:45.283725",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip with Year to Date",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@ -31,15 +31,6 @@ frappe.ui.form.on(cur_frm.doctype, {
} }
} }
}); });
frm.set_query("cost_center", "taxes", function(doc) {
return {
filters: {
'company': doc.company,
"is_group": 0
}
}
});
} }
}, },
validate: function(frm) { validate: function(frm) {

View File

@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide('erpnext.accounts.dimensions');
erpnext.TransactionController = erpnext.taxes_and_totals.extend({ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
setup: function() { setup: function() {
this._super(); this._super();
@ -106,6 +108,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!item.warehouse && frm.doc.set_warehouse) { if(!item.warehouse && frm.doc.set_warehouse) {
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 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}; };
}); });
} }
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")) { if (this.frm.fields_dict["items"].grid.get_field("expense_account")) {
this.frm.set_query("expense_account", "items", function(doc) { this.frm.set_query("expense_account", "items", function(doc) {

View File

@ -115,7 +115,26 @@ $.extend(erpnext.queries, {
["Warehouse", "is_group", "=",0] ["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
}
};
} }
}); });

View File

@ -194,7 +194,11 @@ $.extend(erpnext.utils, {
add_dimensions: function(report_name, index) { add_dimensions: function(report_name, index) {
let filters = frappe.query_reports[report_name].filters; let filters = frappe.query_reports[report_name].filters;
erpnext.dimension_filters.forEach((dimension) => { 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']); let found = filters.some(el => el.fieldname === dimension['fieldname']);
if (!found) { if (!found) {
@ -206,6 +210,8 @@ $.extend(erpnext.utils, {
}); });
} }
}); });
}
});
}, },
make_subscription: function(doctype, docname) { make_subscription: function(doctype, docname) {

Some files were not shown because too many files have changed in this diff Show More