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
def get_last_membership():
def get_last_membership(member):
'''Returns last membership if exists'''
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]
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
if last_membership:
return last_membership[0]

View File

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

View File

@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension):
return all_dimensions
@frappe.whitelist()
def get_dimension_filters():
def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type
FROM `tabAccounting Dimension`
@ -214,6 +214,18 @@ def get_dimension_filters():
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1)
if with_cost_center_and_project:
dimension_filters.extend([
{
'fieldname': 'cost_center',
'document_type': 'Cost Center'
},
{
'fieldname': 'project',
'document_type': 'Project'
}
])
default_dimensions_map = {}
for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {})

View File

@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d
class TestAccountingDimension(unittest.TestCase):
def setUp(self):
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
dimension = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 0
dimension1.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
create_dimension()
def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1)
@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self):
disable_dimension()
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")

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
// For license information, please see license.txt
frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', {
onload: function(frm) {
@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm);
}
},
if (frm.doc.plaid_access_token) {
frm.add_custom_button(__('Refresh Plaid Link'), () => {
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
});
}
}
});
@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) {
frm.doc.name).options = options;
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
// For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', {
onload: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("project", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", "accounts", function() {
return {
filters: {
@ -26,16 +11,18 @@ frappe.ui.form.on('Budget', {
report_type: "Profit and Loss",
is_group: 0
}
}
})
};
});
frm.set_query("monthly_distribution", function() {
return {
filters: {
fiscal_year: frm.doc.fiscal_year
}
}
})
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {

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.utils import get_account_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
from six import iteritems
exclude_from_linked_with = True
class GLEntry(Document):
@ -39,6 +41,7 @@ class GLEntry(Document):
if not from_repost:
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj)
@ -76,11 +79,9 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname):
@ -93,6 +94,25 @@ class GLEntry(Document):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account))
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in iteritems(dimension_filter_map):
dimension = key[0]
account = key[1]
if self.account == account:
if value['is_mandatory'] and not self.get(dimension):
frappe.throw(_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
if value['allow_or_restrict'] == 'Allow':
if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
else:
if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
def check_pl_account(self):
if self.is_opening=='Yes' and \

View File

@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", {
}
}
});
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
voucher_type: function(frm){
@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
this.load_defaults();
this.setup_queries();
this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
onload_post_render: function() {
@ -222,15 +225,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
return erpnext.journal_entry.account_query(me.frm);
});
me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) {
return {
filters: {
company: me.frm.doc.company,
is_group: 0
}
};
});
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
@ -406,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
}
}
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
// For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
@ -46,20 +48,17 @@ 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"));
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) {
frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules."));
}
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
});

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.page.set_indicator(__('In Progress'), 'orange');
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
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) {

View File

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

View File

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

View File

@ -57,6 +57,8 @@ frappe.ui.form.on('POS Profile', {
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@ -67,6 +69,7 @@ frappe.ui.form.on('POS Profile', {
company: function(frm) {
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
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() {
this._super();
@ -41,6 +46,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh: function(doc) {
@ -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) {

View File

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

View File

@ -1861,23 +1861,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self):
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.naming_series = 'INV-2020-.#####'
si.items = []
@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual(
value_details['TotInvVal'],
value_details['AssVal'] + value_details['CgstVal']
+ value_details['SgstVal'] + value_details['IgstVal']
calculated_invoice_value = \
value_details['AssVal'] + value_details['CgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal'] \
+ 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.assertTrue(einvoice['EwbDtls'])

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", {
@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", {
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) {
set_schedule_date(frm);
if (!frm.doc.transaction_date){
@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
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, {})
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', '')
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", "")
})
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
meta = frappe.get_meta(doctype)
if meta.is_tree:
query_filters.append(['is_group', '=', 0])
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
if txt:
query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
query_selector = 'in'
else:
query_selector = 'not in'
if len(dimension_filters['allowed_dimensions']) == 1:
dimensions = tuple(dimension_filters['allowed_dimensions'] * 2)
else:
dimensions = tuple(dimension_filters['allowed_dimensions'])
query_filters.append(['name', query_selector, dimensions])
output = frappe.get_all(doctype, filters=query_filters)
result = [d.name for d in output]
return [(d,) for d in set(result)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

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

View File

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

View File

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

View File

@ -29,14 +29,11 @@ class PlaidConnector():
response = self.client.Item.public_token.exchange(public_token)
access_token = response["access_token"]
return access_token
def get_link_token(self):
def get_token_request(self, update_mode=False):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
token_request = {
args = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes,
@ -45,6 +42,20 @@ 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:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:

View File

@ -12,9 +12,25 @@ frappe.ui.form.on('Plaid Settings', {
refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => {
frm.add_custom_button(__('Link a new bank account'), () => {
new erpnext.integrations.plaidLink(frm);
});
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
freeze: true,
callback: () => {
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
frappe.msgprint({
title: __("Sync Started"),
message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
alert: 1
});
}
});
}).addClass("btn-primary");
}
}
});
@ -30,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink {
this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
this.token = await this.get_link_token();
this.init_plaid();
}
async get_link_token() {
const token = await this.frm.call("get_link_token").then(resp => resp.message);
if (!token) {
frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
}
return token;
}
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
@ -78,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink {
}
onScriptError(error) {
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error);
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
console.log(error);
}
plaid_success(token, response) {
@ -107,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink {
});
}, __("Select a company"), __("Continue"));
}
};
};

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)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None
@ -228,13 +227,23 @@ def new_bank_transaction(transaction):
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
enqueue_synchronization()
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)
@frappe.whitelist()
def enqueue_synchronization():
plaid_accounts = frappe.get_all("Bank Account",
filters={"integration_id": ["!=", ""]},
fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)
@frappe.whitelist()
def get_link_token_for_update(access_token):
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)

View File

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

View File

@ -346,7 +346,8 @@ scheduler_events = {
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_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": [
"erpnext.setup.doctype.email_digest.email_digest.send",

View File

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

View File

@ -11,15 +11,24 @@
"field_order": [
"applicant_name",
"email_id",
"phone_number",
"country",
"status",
"column_break_3",
"job_title",
"source",
"source_name",
"applicant_rating",
"section_break_6",
"notes",
"cover_letter",
"resume_attachment"
"resume_attachment",
"resume_link",
"section_break_16",
"currency",
"column_break_18",
"lower_range",
"upper_range"
],
"fields": [
{
@ -91,12 +100,65 @@
"fieldtype": "Data",
"label": "Notes",
"read_only": 1
},
{
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number",
"options": "Phone"
},
{
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country"
},
{
"fieldname": "resume_link",
"fieldtype": "Data",
"label": "Resume Link"
},
{
"fieldname": "applicant_rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Applicant Rating"
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Salary Expectation"
},
{
"fieldname": "lower_range",
"fieldtype": "Currency",
"label": "Lower Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "upper_range",
"fieldtype": "Currency",
"label": "Upper Range",
"options": "currency",
"precision": "0"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-01-13 16:19:39.113330",
"modified": "2020-09-18 12:39:02.557563",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",

View File

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

View File

@ -43,9 +43,8 @@ class JobOpening(WebsiteGenerator):
current_count = designation_counts['employee_count'] + designation_counts['job_openings']
if self.planned_vacancies <= current_count:
frappe.throw(_("Job Openings for designation {0} already open \
or hiring completed as per Staffing Plan {1}"
.format(self.designation, self.staffing_plan)))
frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
self.designation, self.staffing_plan))
def get_context(self, context):
context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
@ -56,7 +55,8 @@ def get_list_context(context):
context.get_list = get_job_openings
def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
fields = ['name', 'status', 'job_title', 'description']
fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range',
'lower_range', 'upper_range', 'currency', 'job_application_route']
filters = filters or {}
filters.update({

View File

@ -1,9 +1,18 @@
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
{%- if doc.publish_salary_range -%}
<p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
{% endif %}
<div>
<a class="btn btn-primary"
href="/job_application?new=1&job_title={{ doc.name }}">
{%- if doc.job_application_route -%}
<a class='btn btn-primary'
href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</div>
</div>

View File

@ -1,86 +1,200 @@
{
"accept_payment": 0,
"allow_comments": 1,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"creation": "2016-09-10 02:53:16.598314",
"doc_type": "Job Applicant",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"introduction_text": "",
"is_standard": 1,
"login_required": 0,
"max_attachment_size": 0,
"modified": "2016-12-20 00:21:44.081622",
"modified_by": "Administrator",
"module": "HR",
"name": "job-application",
"owner": "Administrator",
"published": 1,
"route": "job_application",
"show_sidebar": 1,
"sidebar_items": [],
"success_message": "Thank you for applying.",
"success_url": "/jobs",
"title": "Job Application",
"accept_payment": 0,
"allow_comments": 1,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n",
"creation": "2016-09-10 02:53:16.598314",
"doc_type": "Job Applicant",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"introduction_text": "",
"is_standard": 1,
"login_required": 0,
"max_attachment_size": 0,
"modified": "2020-10-07 19:27:17.143355",
"modified_by": "Administrator",
"module": "HR",
"name": "job-application",
"owner": "Administrator",
"published": 1,
"route": "job_application",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 1,
"sidebar_items": [],
"success_message": "Thank you for applying.",
"success_url": "/jobs",
"title": "Job Application",
"web_form_fields": [
{
"fieldname": "job_title",
"fieldtype": "Data",
"hidden": 0,
"label": "Job Opening",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 1,
"reqd": 0
},
"allow_read_on_all_link_options": 0,
"fieldname": "job_title",
"fieldtype": "Data",
"hidden": 0,
"label": "Job Opening",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 1,
"reqd": 0,
"show_in_filter": 0
},
{
"fieldname": "applicant_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Applicant Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1
},
"allow_read_on_all_link_options": 0,
"fieldname": "applicant_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Applicant Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"fieldname": "email_id",
"fieldtype": "Data",
"hidden": 0,
"label": "Email Address",
"max_length": 0,
"max_value": 0,
"options": "Email",
"read_only": 0,
"reqd": 1
},
"allow_read_on_all_link_options": 0,
"fieldname": "email_id",
"fieldtype": "Data",
"hidden": 0,
"label": "Email Address",
"max_length": 0,
"max_value": 0,
"options": "Email",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"fieldname": "cover_letter",
"fieldtype": "Text",
"hidden": 0,
"label": "Cover Letter",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
},
"allow_read_on_all_link_options": 0,
"fieldname": "phone_number",
"fieldtype": "Data",
"hidden": 0,
"label": "Phone Number",
"max_length": 0,
"max_value": 0,
"options": "Phone",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"fieldname": "resume_attachment",
"fieldtype": "Attach",
"hidden": 0,
"label": "Resume Attachment",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0
"allow_read_on_all_link_options": 0,
"fieldname": "country",
"fieldtype": "Link",
"hidden": 0,
"label": "Country of Residence",
"max_length": 0,
"max_value": 0,
"options": "Country",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "cover_letter",
"fieldtype": "Text",
"hidden": 0,
"label": "Cover Letter",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "resume_link",
"fieldtype": "Data",
"hidden": 0,
"label": "Resume Link",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Section Break",
"hidden": 0,
"label": "Expected Salary Range per month",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 0,
"label": "Currency",
"max_length": 0,
"max_value": 0,
"options": "Currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "lower_range",
"fieldtype": "Currency",
"hidden": 0,
"label": "Lower Range",
"max_length": 0,
"max_value": 0,
"options": "currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "upper_range",
"fieldtype": "Currency",
"hidden": 0,
"label": "Upper Range",
"max_length": 0,
"max_value": 0,
"options": "currency",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Interest Accrual\",\n \"is_query_report\": true,\n \"label\": \"Loan Interest Report\",\n \"name\": \"Loan Interest Report\",\n \"route\": \"#query-report/Loan Interest Report\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Exposure\",\n \"name\": \"Loan Security Exposure\",\n \"route\": \"#query-report/Loan Security Exposure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Applicant-Wise Loan Security Exposure\",\n \"name\": \"Applicant-Wise Loan Security Exposure\",\n \"route\": \"#query-report/Applicant-Wise Loan Security Exposure\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@ -38,7 +38,7 @@
"idx": 0,
"is_standard": 1,
"label": "Loan",
"modified": "2020-10-17 12:59:50.336085",
"modified": "2021-01-17 07:21:22.092184",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

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
if pending_amount < write_off_limit:
# update status as loan closure requested
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@ -336,13 +338,13 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a
return unpledge_request
def validate_employee_currency_with_company_currency(applicant, company):
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
frappe.throw(_("Please select Company"))
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
frappe.throw(_("Please select Company"))
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))

View File

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

View File

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

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

View File

@ -37,10 +37,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = '2019-10-01'
@ -50,11 +48,46 @@ class TestLoanInterestAccrual(unittest.TestCase):
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
def test_accumulated_amounts(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0))
next_start_date = '2019-10-31'
next_end_date = '2019-11-29'
no_of_days = date_diff(next_end_date, next_start_date) + 1
process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date)
new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0)
loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name,
'process_loan_interest_accrual': process})
self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount)

View File

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

View File

@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"column_break_2",
"uom",
@ -79,10 +80,18 @@
"label": "Loan Security Type",
"options": "Loan Security Type",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-06-11 03:41:33.900340",
"modified": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Price",

View File

@ -144,17 +144,17 @@
},
{
"allow_on_submit": 1,
"description": "Pending amount that will be automatically ignored on loan closure request ",
"description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount ",
"label": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-26 07:13:55.029811",
"modified": "2021-01-17 06:51:26.082879",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"loan_security_code",
"uom",
@ -85,11 +86,18 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:15.424937",
"modified": "2021-01-17 07:41:12.452514",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",

View File

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

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"qty",
"loan_security_price",
"amount",
@ -56,12 +57,19 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:37.542344",
"modified": "2021-01-17 07:29:01.671722",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"loan_security_code",
"haircut",
@ -61,12 +62,19 @@
"fieldtype": "Percent",
"label": "Haircut",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:28.106961",
"modified": "2021-01-17 07:36:20.212342",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",

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

View File

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

View File

@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
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) {
if (frm.doc.__islocal)
return;
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", {
save: true
}).then(() => {
frm.reload_doc();
frm.call({
doc: frm.doc,
method: "generate_invoice",
args: {save: true},
freeze: true,
freeze_message: __("Creating Membership Invoice"),
callback: function(r) {
if (r.invoice)
frm.reload_doc();
}
});
});
@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', {
},
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",
"field_order": [
"member",
"member_name",
"membership_type",
"column_break_3",
"membership_status",
@ -46,6 +47,8 @@
{
"fieldname": "membership_status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled"
},
@ -122,11 +125,18 @@
"fieldtype": "Link",
"label": "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,
"links": [],
"modified": "2020-09-19 14:28:11.532696",
"modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
@ -158,7 +168,9 @@
}
],
"restrict_to_domain": "Non Profit",
"search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "member_name",
"track_changes": 1
}
}

View File

@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member
from frappe import _
import erpnext
class Membership(Document):
def validate(self):
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"))
if not member_name:
user = frappe.get_doc('User', frappe.session.user)
member = frappe.get_doc(dict(
doctype='Member',
email=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
self.validate_membership_period()
if self.get("__islocal"):
self.member = member_name
def create_member_from_website_user(self):
member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
if not member_name:
user = frappe.get_doc("User", frappe.session.user)
member = frappe.get_doc(dict(
doctype="Member",
email_id=frappe.session.user,
membership_type=self.membership_type,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
member_name = member.name
if self.get("__islocal"):
self.member = member_name
def validate_membership_period(self):
# 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 last_membership and not frappe.session.user == "Administrator":
# 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()) :
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)
elif frappe.session.user == "Administrator":
@ -54,11 +64,16 @@ class Membership(Document):
self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None):
if status_changed_to in ("Completed", "Authorized"):
self.load_from_db()
self.db_set('paid', 1)
if status_changed_to not in ("Completed", "Authorized"):
return
self.load_from_db()
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):
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"))
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:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
if with_payment_entry:
self.make_payment_entry(settings, invoice)
if save:
self.save()
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):
settings = frappe.get_doc("Membership Settings")
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)
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)
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)]
if self.invoice and settings.send_invoice:
@ -112,48 +157,56 @@ class Membership(Document):
}
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:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
invoice = self.generate_invoice(False)
self.generate_invoice(save=False)
self.send_acknowlement()
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
'doctype': 'Sales Invoice',
'customer': member.customer,
'debit_to': settings.debit_account,
'currency': membership.currency,
'is_pos': 0,
'items': [
"doctype": "Sales Invoice",
"customer": member.customer,
"debit_to": settings.debit_account,
"currency": membership.currency,
"company": settings.company,
"is_pos": 0,
"items": [
{
'item_code': plan.linked_item,
'rate': membership.amount,
'qty': 1
"item_code": plan.linked_item,
"rate": membership.amount,
"qty": 1
}
]
})
invoice.set_missing_values()
invoice.insert(ignore_permissions=True)
invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
return invoice
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
'subscription_id': subscription_id,
'email_id': email
"subscription_id": subscription_id,
"email_id": email
}, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]['name'])
return frappe.get_doc("Member", members[0]["name"])
except:
return None
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")
key = settings.get_webhook_secret()
@ -162,6 +215,7 @@ def verify_signature(data):
controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log)
return { 'status': 'Failed', 'reason': e}
return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
subscription = data.payload.get("subscription", {}).get('entity', {})
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
payment = data.payload.get("payment", {}).get('entity', {})
payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment)
try:
@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member = get_member_based_on_subscription(subscription.id, payment.email)
if not member:
member = create_member(frappe._dict({
'fullname': payment.email,
'email': payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
"fullname": payment.email,
"email": payment.email,
"plan_id": get_plan_from_razorpay_id(subscription.plan_id)
}))
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
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)
elif subscription.notes and type(subscription.notes) == str:
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)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
return { 'status': 'Failed', 'reason': e}
return { "status": "Failed", "reason": e}
return { 'status': 'Success' }
return { "status": "Success" }
def notify_failure(log):
try:
content = """Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
content = """
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}
Regards, Administrator
""".format(get_link_to_form("Error Log", log.name))
Error Log: {0}
Regards,
Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except:
pass
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:
return plan[0]['name']
return plan[0]["name"]
except:
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
# See license.txt
from __future__ import unicode_literals
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):
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 {
filters: {
"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 {
filters: {
"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 {
filters: {
'account_type': 'Receivable',
'is_group': 0,
'company': frm.doc.company
"account_type": "Receivable",
"is_group": 0,
"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",
"webhook_secret",
"column_break_6",
"enable_auto_invoicing",
"enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company",
"debit_account",
"payment_account",
"column_break_9",
"send_email",
"send_invoice",
@ -58,14 +61,7 @@
"label": "Invoicing"
},
{
"default": "0",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
@ -77,7 +73,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@ -86,7 +82,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
"depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
@ -119,11 +115,43 @@
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"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,
"links": [],
"modified": "2020-08-05 17:26:37.287395",
"modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",

View File

@ -2,13 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership Type', {
refresh: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
refresh: function (frm) {
frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
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);
});
frm.set_query('linked_item', () => {
return {
filters: {
is_stock_item: 0
}
};
});
}
});

View File

@ -5,9 +5,14 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
from frappe import _
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):
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.v13_0.updates_for_multi_currency_payroll
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")
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.create_leave_policy_assignment_based_on_employee_current_leave_policy
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;
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payroll Entry', {
onload: function (frm) {
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);
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.events.department_filters(frm);
frm.events.payroll_payable_account_filters(frm);
},
@ -129,21 +132,6 @@ frappe.ui.form.on('Payroll Entry', {
"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) {
frm.events.clear_employee_table(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
currency: function (frm) {

View File

@ -9,6 +9,7 @@
"abbr",
"column_break_3",
"amount",
"year_to_date",
"section_break_5",
"additional_salary",
"statistical_component",
@ -226,11 +227,19 @@
{
"fieldname": "column_break_24",
"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,
"links": [],
"modified": "2020-11-25 13:12:41.081106",
"modified": "2021-01-14 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",

View File

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

View File

@ -584,6 +584,7 @@
"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",
"fieldtype": "Currency",
"label": "Year To Date",
@ -591,6 +592,7 @@
"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",
"fieldtype": "Currency",
"label": "Month To Date",
@ -616,7 +618,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
"modified": "2020-12-21 23:43:44.959840",
"modified": "2021-01-14 13:37:38.180920",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",

View File

@ -52,6 +52,7 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay()
self.compute_year_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"):
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):
year_to_date = 0
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
period_start_date, period_end_date = self.get_year_to_date_period()
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase):
month_to_date += self.net_pay
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):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
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)
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):
data = {}
# 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:
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 = []
i = 0
while i < 12:
while i < num:
slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee,
"salary_structure": salary_structure, "frequency": "Monthly"})
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) {
calculate_totals(frm.doc);
@ -117,6 +120,7 @@ frappe.ui.form.on('Salary Structure', {
fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
});
frm.trigger('set_earning_deduction_component');
},
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("""
select t1.salary_component
from `tabSalary Component` t1, `tabSalary Component Account` t2
where t1.salary_component = t2.parent
and t1.type = %s
and t2.company = %s
where (t1.name = t2.parent
and t1.type = %(type)s
and t2.company = %(company)s)
or (t1.type = %(type)s
and t1.statistical_component = 1)
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) {

View File

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

View File

@ -115,7 +115,26 @@ $.extend(erpnext.queries, {
["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,15 +194,21 @@ $.extend(erpnext.utils, {
add_dimensions: function(report_name, index) {
let filters = frappe.query_reports[report_name].filters;
erpnext.dimension_filters.forEach((dimension) => {
let found = filters.some(el => el.fieldname === dimension['fieldname']);
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
callback: function(r) {
let accounting_dimensions = r.message[0];
accounting_dimensions.forEach((dimension) => {
let found = filters.some(el => el.fieldname === dimension['fieldname']);
if (!found) {
filters.splice(index, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
if (!found) {
filters.splice(index, 0, {
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
}
});
}
});

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