mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 12:19:12 +00:00
Merge branch 'develop' into service-units-in-appointment
This commit is contained in:
@@ -132,16 +132,10 @@ def allow_regional(fn):
|
|||||||
|
|
||||||
return caller
|
return caller
|
||||||
|
|
||||||
def get_last_membership():
|
def get_last_membership(member):
|
||||||
'''Returns last membership if exists'''
|
'''Returns last membership if exists'''
|
||||||
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
|
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
|
||||||
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1)
|
dict(member=member, paid=1), order_by='to_date desc', limit=1)
|
||||||
|
|
||||||
return last_membership and last_membership[0]
|
if last_membership:
|
||||||
|
return last_membership[0]
|
||||||
def is_member():
|
|
||||||
'''Returns true if the user is still a member'''
|
|
||||||
last_membership = get_last_membership()
|
|
||||||
if last_membership and getdate(last_membership.to_date) > getdate():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
frappe.provide('erpnext.integrations');
|
||||||
|
|
||||||
frappe.ui.form.on('Bank', {
|
frappe.ui.form.on('Bank', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
@@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
|
|||||||
frm.set_df_property('address_and_contact', 'hidden', 0);
|
frm.set_df_property('address_and_contact', 'hidden', 0);
|
||||||
frappe.contacts.render_address_and_contact(frm);
|
frappe.contacts.render_address_and_contact(frm);
|
||||||
}
|
}
|
||||||
},
|
if (frm.doc.plaid_access_token) {
|
||||||
|
frm.add_custom_button(__('Refresh Plaid Link'), () => {
|
||||||
|
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) {
|
|||||||
frm.doc.name).options = options;
|
frm.doc.name).options = options;
|
||||||
|
|
||||||
frm.fields_dict.bank_transaction_mapping.grid.refresh();
|
frm.fields_dict.bank_transaction_mapping.grid.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||||
|
constructor(access_token) {
|
||||||
|
this.access_token = access_token;
|
||||||
|
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
|
||||||
|
this.init_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init_config() {
|
||||||
|
this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
|
||||||
|
this.token = await this.get_link_token_for_update();
|
||||||
|
this.init_plaid();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_link_token_for_update() {
|
||||||
|
const token = frappe.xcall(
|
||||||
|
'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
|
||||||
|
{ access_token: this.access_token }
|
||||||
|
)
|
||||||
|
if (!token) {
|
||||||
|
frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
init_plaid() {
|
||||||
|
const me = this;
|
||||||
|
me.loadScript(me.plaidUrl)
|
||||||
|
.then(() => {
|
||||||
|
me.onScriptLoaded(me);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (me.linkHandler) {
|
||||||
|
me.linkHandler.open();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
me.onScriptError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadScript(src) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (document.querySelector("script[src='" + src + "']")) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = document.createElement('script');
|
||||||
|
el.type = 'text/javascript';
|
||||||
|
el.async = true;
|
||||||
|
el.src = src;
|
||||||
|
el.addEventListener('load', resolve);
|
||||||
|
el.addEventListener('error', reject);
|
||||||
|
el.addEventListener('abort', reject);
|
||||||
|
document.head.appendChild(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptLoaded(me) {
|
||||||
|
me.linkHandler = Plaid.create({
|
||||||
|
env: me.plaid_env,
|
||||||
|
token: me.token,
|
||||||
|
onSuccess: me.plaid_success
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptError(error) {
|
||||||
|
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
plaid_success(token, response) {
|
||||||
|
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1861,23 +1861,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
def test_einvoice_json(self):
|
def test_einvoice_json(self):
|
||||||
from erpnext.regional.india.e_invoice.utils import make_einvoice
|
from erpnext.regional.india.e_invoice.utils import make_einvoice
|
||||||
|
|
||||||
customer_gstin = '27AACCM7806M1Z3'
|
|
||||||
customer_gstin_dtls = {
|
|
||||||
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
|
|
||||||
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
|
|
||||||
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
|
|
||||||
}
|
|
||||||
company_gstin = '27AAECE4835E1ZR'
|
|
||||||
company_gstin_dtls = {
|
|
||||||
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
|
|
||||||
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
|
|
||||||
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
|
|
||||||
}
|
|
||||||
# set cache gstin details to avoid fetching details which will require connection to GSP servers
|
|
||||||
frappe.local.gstin_cache = {}
|
|
||||||
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
|
|
||||||
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
|
|
||||||
|
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
si.items = []
|
si.items = []
|
||||||
@@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
|
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
|
||||||
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
|
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
|
||||||
|
|
||||||
self.assertEqual(
|
calculated_invoice_value = \
|
||||||
value_details['TotInvVal'],
|
value_details['AssVal'] + value_details['CgstVal'] \
|
||||||
value_details['AssVal'] + value_details['CgstVal']
|
+ value_details['SgstVal'] + value_details['IgstVal'] \
|
||||||
+ value_details['SgstVal'] + value_details['IgstVal']
|
|
||||||
+ value_details['OthChrg'] - value_details['Discount']
|
+ value_details['OthChrg'] - value_details['Discount']
|
||||||
)
|
|
||||||
|
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
|
||||||
|
|
||||||
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
|
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
|
||||||
self.assertTrue(einvoice['EwbDtls'])
|
self.assertTrue(einvoice['EwbDtls'])
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ class BuyingController(StockController):
|
|||||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||||
|
|
||||||
consumed_qty = raw_material_data.get('qty', 0)
|
consumed_qty = raw_material_data.get('qty', 0)
|
||||||
consumed_serial_nos = raw_material_data.get('serial_nos', '')
|
consumed_serial_nos = raw_material_data.get('serial_no', '')
|
||||||
consumed_batch_nos = raw_material_data.get('batch_nos', '')
|
consumed_batch_nos = raw_material_data.get('batch_nos', '')
|
||||||
|
|
||||||
transferred_qty = raw_material.qty
|
transferred_qty = raw_material.qty
|
||||||
|
|||||||
@@ -29,14 +29,11 @@ class PlaidConnector():
|
|||||||
response = self.client.Item.public_token.exchange(public_token)
|
response = self.client.Item.public_token.exchange(public_token)
|
||||||
access_token = response["access_token"]
|
access_token = response["access_token"]
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
def get_link_token(self):
|
def get_token_request(self, update_mode=False):
|
||||||
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
|
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
|
||||||
token_request = {
|
args = {
|
||||||
"client_name": self.client_name,
|
"client_name": self.client_name,
|
||||||
"client_id": self.settings.plaid_client_id,
|
|
||||||
"secret": self.settings.plaid_secret,
|
|
||||||
"products": self.products,
|
|
||||||
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
|
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
|
||||||
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
|
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
|
||||||
"country_codes": country_codes,
|
"country_codes": country_codes,
|
||||||
@@ -45,6 +42,20 @@ class PlaidConnector():
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if update_mode:
|
||||||
|
args["access_token"] = self.access_token
|
||||||
|
else:
|
||||||
|
args.update({
|
||||||
|
"client_id": self.settings.plaid_client_id,
|
||||||
|
"secret": self.settings.plaid_secret,
|
||||||
|
"products": self.products,
|
||||||
|
})
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def get_link_token(self, update_mode=False):
|
||||||
|
token_request = self.get_token_request(update_mode)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.LinkToken.create(token_request)
|
response = self.client.LinkToken.create(token_request)
|
||||||
except InvalidRequestError:
|
except InvalidRequestError:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', {
|
|||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.enabled) {
|
if (frm.doc.enabled) {
|
||||||
frm.add_custom_button('Link a new bank account', () => {
|
frm.add_custom_button(__('Link a new bank account'), () => {
|
||||||
new erpnext.integrations.plaidLink(frm);
|
new erpnext.integrations.plaidLink(frm);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
this.product = ["auth", "transactions"];
|
this.product = ["auth", "transactions"];
|
||||||
this.plaid_env = this.frm.doc.plaid_env;
|
this.plaid_env = this.frm.doc.plaid_env;
|
||||||
this.client_name = frappe.boot.sitename;
|
this.client_name = frappe.boot.sitename;
|
||||||
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
|
this.token = await this.get_link_token();
|
||||||
this.init_plaid();
|
this.init_plaid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_link_token() {
|
||||||
|
const token = await this.frm.call("get_link_token").then(resp => resp.message);
|
||||||
|
if (!token) {
|
||||||
|
frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
init_plaid() {
|
init_plaid() {
|
||||||
const me = this;
|
const me = this;
|
||||||
me.loadScript(me.plaidUrl)
|
me.loadScript(me.plaidUrl)
|
||||||
@@ -94,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScriptError(error) {
|
onScriptError(error) {
|
||||||
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
|
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
|
||||||
frappe.msgprint(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
plaid_success(token, response) {
|
plaid_success(token, response) {
|
||||||
@@ -123,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
});
|
});
|
||||||
}, __("Select a company"), __("Continue"));
|
}, __("Select a company"), __("Continue"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -230,7 +230,6 @@ def automatic_synchronization():
|
|||||||
if settings.enabled == 1 and settings.automatic_sync == 1:
|
if settings.enabled == 1 and settings.automatic_sync == 1:
|
||||||
enqueue_synchronization()
|
enqueue_synchronization()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enqueue_synchronization():
|
def enqueue_synchronization():
|
||||||
plaid_accounts = frappe.get_all("Bank Account",
|
plaid_accounts = frappe.get_all("Bank Account",
|
||||||
@@ -243,3 +242,8 @@ def enqueue_synchronization():
|
|||||||
bank=plaid_account.bank,
|
bank=plaid_account.bank,
|
||||||
bank_account=plaid_account.name
|
bank_account=plaid_account.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_link_token_for_update(access_token):
|
||||||
|
plaid = PlaidConnector(access_token)
|
||||||
|
return plaid.get_link_token(update_mode=True)
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ scheduler_events = {
|
|||||||
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
|
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
|
||||||
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
|
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
|
||||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email"
|
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
|
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"membership_expiry_date",
|
"membership_expiry_date",
|
||||||
"column_break_5",
|
"column_break_5",
|
||||||
"membership_type",
|
"membership_type",
|
||||||
"email",
|
|
||||||
"email_id",
|
"email_id",
|
||||||
"image",
|
"image",
|
||||||
"customer_section",
|
"customer_section",
|
||||||
@@ -64,13 +63,6 @@
|
|||||||
"options": "Membership Type",
|
"options": "Membership Type",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "email",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "User",
|
|
||||||
"options": "User"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
@@ -178,7 +170,7 @@
|
|||||||
],
|
],
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-16 23:44:13.596948",
|
"modified": "2020-11-09 12:12:10.174647",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Non Profit",
|
"module": "Non Profit",
|
||||||
"name": "Member",
|
"name": "Member",
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ class Member(Document):
|
|||||||
|
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.email:
|
|
||||||
self.validate_email_type(self.email)
|
|
||||||
if self.email_id:
|
if self.email_id:
|
||||||
self.validate_email_type(self.email_id)
|
self.validate_email_type(self.email_id)
|
||||||
|
|
||||||
@@ -57,14 +55,16 @@ class Member(Document):
|
|||||||
def make_customer_and_link(self):
|
def make_customer_and_link(self):
|
||||||
if self.customer:
|
if self.customer:
|
||||||
frappe.msgprint(_("A customer is already linked to this Member"))
|
frappe.msgprint(_("A customer is already linked to this Member"))
|
||||||
cust = create_customer(frappe._dict({
|
|
||||||
|
customer = create_customer(frappe._dict({
|
||||||
'fullname': self.member_name,
|
'fullname': self.member_name,
|
||||||
'email': self.email_id or self.email,
|
'email': self.email_id,
|
||||||
'phone': None
|
'phone': None
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.customer = cust
|
self.customer = customer
|
||||||
self.save()
|
self.save()
|
||||||
|
frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_member(user_details):
|
def get_or_create_member(user_details):
|
||||||
|
|||||||
@@ -4,16 +4,25 @@
|
|||||||
frappe.ui.form.on('Membership', {
|
frappe.ui.form.on('Membership', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
|
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
|
||||||
if (val) frm.set_df_property('razorpay_details_section', 'hidden', false);
|
if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
if (frm.doc.__islocal)
|
||||||
|
return;
|
||||||
|
|
||||||
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
|
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
|
||||||
frm.call("generate_invoice", {
|
frm.call({
|
||||||
save: true
|
doc: frm.doc,
|
||||||
}).then(() => {
|
method: "generate_invoice",
|
||||||
frm.reload_doc();
|
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) {
|
onload: function(frm) {
|
||||||
frm.add_fetch('membership_type', 'amount', 'amount');
|
frm.add_fetch("membership_type", "amount", "amount");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"member",
|
"member",
|
||||||
|
"member_name",
|
||||||
"membership_type",
|
"membership_type",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"membership_status",
|
"membership_status",
|
||||||
@@ -46,6 +47,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "membership_status",
|
"fieldname": "membership_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Membership Status",
|
"label": "Membership Status",
|
||||||
"options": "New\nCurrent\nExpired\nPending\nCancelled"
|
"options": "New\nCurrent\nExpired\nPending\nCancelled"
|
||||||
},
|
},
|
||||||
@@ -122,11 +125,18 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Invoice",
|
"label": "Invoice",
|
||||||
"options": "Sales Invoice"
|
"options": "Sales Invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "member.member_name",
|
||||||
|
"fieldname": "member_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Member Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-19 14:28:11.532696",
|
"modified": "2021-01-21 16:31:20.032656",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Non Profit",
|
"module": "Non Profit",
|
||||||
"name": "Membership",
|
"name": "Membership",
|
||||||
@@ -158,7 +168,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"restrict_to_domain": "Non Profit",
|
"restrict_to_domain": "Non Profit",
|
||||||
|
"search_fields": "member, member_name",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"title_field": "member_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
import erpnext
|
import erpnext
|
||||||
|
|
||||||
|
|
||||||
class Membership(Document):
|
class Membership(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if not self.member or not frappe.db.exists("Member", self.member):
|
if not self.member or not frappe.db.exists("Member", self.member):
|
||||||
member_name = frappe.get_value('Member', dict(email=frappe.session.user))
|
# for web forms
|
||||||
|
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
|
||||||
|
if user_type == "Website User":
|
||||||
|
self.create_member_from_website_user()
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Please select a Member"))
|
||||||
|
|
||||||
if not member_name:
|
self.validate_membership_period()
|
||||||
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
|
|
||||||
|
|
||||||
if self.get("__islocal"):
|
def create_member_from_website_user(self):
|
||||||
self.member = member_name
|
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)
|
# get last membership (if active)
|
||||||
last_membership = erpnext.get_last_membership()
|
last_membership = erpnext.get_last_membership(self.member)
|
||||||
|
|
||||||
# if person applied for offline membership
|
# if person applied for offline membership
|
||||||
if last_membership and not frappe.session.user == "Administrator":
|
if last_membership and not frappe.session.user == "Administrator":
|
||||||
# if last membership does not expire in 30 days, then do not allow to renew
|
# if last membership does not expire in 30 days, then do not allow to renew
|
||||||
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
|
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
|
||||||
frappe.throw(_('You can only renew if your membership expires within 30 days'))
|
frappe.throw(_("You can only renew if your membership expires within 30 days"))
|
||||||
|
|
||||||
self.from_date = add_days(last_membership.to_date, 1)
|
self.from_date = add_days(last_membership.to_date, 1)
|
||||||
elif frappe.session.user == "Administrator":
|
elif frappe.session.user == "Administrator":
|
||||||
@@ -54,11 +64,16 @@ class Membership(Document):
|
|||||||
self.to_date = add_months(self.from_date, 1)
|
self.to_date = add_months(self.from_date, 1)
|
||||||
|
|
||||||
def on_payment_authorized(self, status_changed_to=None):
|
def on_payment_authorized(self, status_changed_to=None):
|
||||||
if status_changed_to in ("Completed", "Authorized"):
|
if status_changed_to not in ("Completed", "Authorized"):
|
||||||
self.load_from_db()
|
return
|
||||||
self.db_set('paid', 1)
|
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):
|
if not (self.paid or self.currency or self.amount):
|
||||||
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
|
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
|
||||||
|
|
||||||
@@ -66,34 +81,64 @@ class Membership(Document):
|
|||||||
frappe.throw(_("An invoice is already linked to this document"))
|
frappe.throw(_("An invoice is already linked to this document"))
|
||||||
|
|
||||||
member = frappe.get_doc("Member", self.member)
|
member = frappe.get_doc("Member", self.member)
|
||||||
plan = frappe.get_doc("Membership Type", self.membership_type)
|
|
||||||
settings = frappe.get_doc("Membership Settings")
|
|
||||||
|
|
||||||
if not member.customer:
|
if not member.customer:
|
||||||
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
|
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
|
||||||
|
|
||||||
if not settings.debit_account:
|
plan = frappe.get_doc("Membership Type", self.membership_type)
|
||||||
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
|
settings = frappe.get_doc("Membership Settings")
|
||||||
|
self.validate_membership_type_and_settings(plan, settings)
|
||||||
if not settings.company:
|
|
||||||
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
|
|
||||||
|
|
||||||
invoice = make_invoice(self, member, plan, settings)
|
invoice = make_invoice(self, member, plan, settings)
|
||||||
self.invoice = invoice.name
|
self.invoice = invoice.name
|
||||||
|
|
||||||
|
if with_payment_entry:
|
||||||
|
self.make_payment_entry(settings, invoice)
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
def validate_membership_type_and_settings(self, plan, settings):
|
||||||
|
settings_link = get_link_to_form("Membership Type", self.membership_type)
|
||||||
|
|
||||||
|
if not settings.debit_account:
|
||||||
|
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
|
||||||
|
|
||||||
|
if not settings.company:
|
||||||
|
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in {0}").format(settings_link))
|
||||||
|
|
||||||
|
if not plan.linked_item:
|
||||||
|
frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
|
||||||
|
get_link_to_form("Membership Type", self.membership_type)))
|
||||||
|
|
||||||
|
def make_payment_entry(self, settings, invoice):
|
||||||
|
if not settings.payment_account:
|
||||||
|
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
|
||||||
|
get_link_to_form("Membership Type", self.membership_type)))
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
frappe.flags.ignore_account_permission = True
|
||||||
|
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
|
||||||
|
frappe.flags.ignore_account_permission=False
|
||||||
|
pe.paid_to = settings.payment_account
|
||||||
|
pe.reference_no = self.name
|
||||||
|
pe.reference_date = getdate()
|
||||||
|
pe.save(ignore_permissions=True)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
def send_acknowlement(self):
|
def send_acknowlement(self):
|
||||||
settings = frappe.get_doc("Membership Settings")
|
settings = frappe.get_doc("Membership Settings")
|
||||||
if not settings.send_email:
|
if not settings.send_email:
|
||||||
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings"))
|
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
|
||||||
|
get_link_to_form("Membership Settings", "Membership Settings")))
|
||||||
|
|
||||||
member = frappe.get_doc("Member", self.member)
|
member = frappe.get_doc("Member", self.member)
|
||||||
|
if not member.email_id:
|
||||||
|
frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
|
||||||
|
|
||||||
plan = frappe.get_doc("Membership Type", self.membership_type)
|
plan = frappe.get_doc("Membership Type", self.membership_type)
|
||||||
email = member.email_id if member.email_id else member.email
|
email = member.email_id
|
||||||
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
|
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
|
||||||
|
|
||||||
if self.invoice and settings.send_invoice:
|
if self.invoice and settings.send_invoice:
|
||||||
@@ -112,48 +157,56 @@ class Membership(Document):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
|
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||||
else:
|
else:
|
||||||
frappe.sendmail(**email_args)
|
frappe.sendmail(**email_args)
|
||||||
|
|
||||||
def generate_and_send_invoice(self):
|
def generate_and_send_invoice(self):
|
||||||
invoice = self.generate_invoice(False)
|
self.generate_invoice(save=False)
|
||||||
self.send_acknowlement()
|
self.send_acknowlement()
|
||||||
|
|
||||||
|
|
||||||
def make_invoice(membership, member, plan, settings):
|
def make_invoice(membership, member, plan, settings):
|
||||||
invoice = frappe.get_doc({
|
invoice = frappe.get_doc({
|
||||||
'doctype': 'Sales Invoice',
|
"doctype": "Sales Invoice",
|
||||||
'customer': member.customer,
|
"customer": member.customer,
|
||||||
'debit_to': settings.debit_account,
|
"debit_to": settings.debit_account,
|
||||||
'currency': membership.currency,
|
"currency": membership.currency,
|
||||||
'is_pos': 0,
|
"company": settings.company,
|
||||||
'items': [
|
"is_pos": 0,
|
||||||
|
"items": [
|
||||||
{
|
{
|
||||||
'item_code': plan.linked_item,
|
"item_code": plan.linked_item,
|
||||||
'rate': membership.amount,
|
"rate": membership.amount,
|
||||||
'qty': 1
|
"qty": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
invoice.set_missing_values()
|
||||||
invoice.insert(ignore_permissions=True)
|
invoice.insert(ignore_permissions=True)
|
||||||
invoice.submit()
|
invoice.submit()
|
||||||
|
|
||||||
|
frappe.msgprint(_("Sales Invoice created successfully"))
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
def get_member_based_on_subscription(subscription_id, email):
|
def get_member_based_on_subscription(subscription_id, email):
|
||||||
members = frappe.get_all("Member", filters={
|
members = frappe.get_all("Member", filters={
|
||||||
'subscription_id': subscription_id,
|
"subscription_id": subscription_id,
|
||||||
'email_id': email
|
"email_id": email
|
||||||
}, order_by="creation desc")
|
}, order_by="creation desc")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return frappe.get_doc("Member", members[0]['name'])
|
return frappe.get_doc("Member", members[0]["name"])
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def verify_signature(data):
|
def verify_signature(data):
|
||||||
signature = frappe.request.headers.get('X-Razorpay-Signature')
|
if frappe.flags.in_test:
|
||||||
|
return True
|
||||||
|
signature = frappe.request.headers.get("X-Razorpay-Signature")
|
||||||
|
|
||||||
settings = frappe.get_doc("Membership Settings")
|
settings = frappe.get_doc("Membership Settings")
|
||||||
key = settings.get_webhook_secret()
|
key = settings.get_webhook_secret()
|
||||||
@@ -162,6 +215,7 @@ def verify_signature(data):
|
|||||||
|
|
||||||
controller.verify_signature(data, signature, key)
|
controller.verify_signature(data, signature, key)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def trigger_razorpay_subscription(*args, **kwargs):
|
def trigger_razorpay_subscription(*args, **kwargs):
|
||||||
data = frappe.request.get_data(as_text=True)
|
data = frappe.request.get_data(as_text=True)
|
||||||
@@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log = frappe.log_error(e, "Webhook Verification Error")
|
log = frappe.log_error(e, "Webhook Verification Error")
|
||||||
notify_failure(log)
|
notify_failure(log)
|
||||||
return { 'status': 'Failed', 'reason': e}
|
return { "status": "Failed", "reason": e}
|
||||||
|
|
||||||
if isinstance(data, six.string_types):
|
if isinstance(data, six.string_types):
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
data = frappe._dict(data)
|
data = frappe._dict(data)
|
||||||
|
|
||||||
subscription = data.payload.get("subscription", {}).get('entity', {})
|
subscription = data.payload.get("subscription", {}).get("entity", {})
|
||||||
subscription = frappe._dict(subscription)
|
subscription = frappe._dict(subscription)
|
||||||
|
|
||||||
payment = data.payload.get("payment", {}).get('entity', {})
|
payment = data.payload.get("payment", {}).get("entity", {})
|
||||||
payment = frappe._dict(payment)
|
payment = frappe._dict(payment)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
|||||||
member = get_member_based_on_subscription(subscription.id, payment.email)
|
member = get_member_based_on_subscription(subscription.id, payment.email)
|
||||||
if not member:
|
if not member:
|
||||||
member = create_member(frappe._dict({
|
member = create_member(frappe._dict({
|
||||||
'fullname': payment.email,
|
"fullname": payment.email,
|
||||||
'email': payment.email,
|
"email": payment.email,
|
||||||
'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
|
"plan_id": get_plan_from_razorpay_id(subscription.plan_id)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
member.subscription_id = subscription.id
|
member.subscription_id = subscription.id
|
||||||
member.customer_id = payment.customer_id
|
member.customer_id = payment.customer_id
|
||||||
if subscription.notes and type(subscription.notes) == dict:
|
if subscription.notes and type(subscription.notes) == dict:
|
||||||
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items())
|
notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
|
||||||
member.add_comment("Comment", notes)
|
member.add_comment("Comment", notes)
|
||||||
elif subscription.notes and type(subscription.notes) == str:
|
elif subscription.notes and type(subscription.notes) == str:
|
||||||
member.add_comment("Comment", subscription.notes)
|
member.add_comment("Comment", subscription.notes)
|
||||||
@@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
|||||||
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
|
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
|
||||||
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
|
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
|
||||||
notify_failure(log)
|
notify_failure(log)
|
||||||
return { 'status': 'Failed', 'reason': e}
|
return { "status": "Failed", "reason": e}
|
||||||
|
|
||||||
return { 'status': 'Success' }
|
return { "status": "Success" }
|
||||||
|
|
||||||
|
|
||||||
def notify_failure(log):
|
def notify_failure(log):
|
||||||
try:
|
try:
|
||||||
content = """Dear System Manager,
|
content = """
|
||||||
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
|
Dear System Manager,
|
||||||
|
Razorpay webhook for creating renewing membership subscription failed due to some reason.
|
||||||
|
Please check the following error log linked below
|
||||||
|
Error Log: {0}
|
||||||
|
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)
|
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_plan_from_razorpay_id(plan_id):
|
def get_plan_from_razorpay_id(plan_id):
|
||||||
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc")
|
plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return plan[0]['name']
|
return plan[0]["name"]
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_expired_status():
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE
|
||||||
|
`tabMembership` SET `status` = 'Expired'
|
||||||
|
WHERE
|
||||||
|
`status` not in ('Cancelled') AND `to_date` < %s
|
||||||
|
""", (nowdate()))
|
||||||
15
erpnext/non_profit/doctype/membership/membership_list.js
Normal file
15
erpnext/non_profit/doctype/membership/membership_list.js
Normal 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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,8 +2,110 @@
|
|||||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import frappe
|
||||||
|
import erpnext
|
||||||
|
from erpnext.non_profit.doctype.member.member import create_member
|
||||||
|
from frappe.utils import nowdate, add_months
|
||||||
|
|
||||||
class TestMembership(unittest.TestCase):
|
class TestMembership(unittest.TestCase):
|
||||||
pass
|
def setUp(self):
|
||||||
|
# Get default company
|
||||||
|
company = frappe.get_doc("Company", erpnext.get_default_company())
|
||||||
|
|
||||||
|
# update membership settings
|
||||||
|
settings = frappe.get_doc("Membership Settings")
|
||||||
|
# Enable razorpay
|
||||||
|
settings.enable_razorpay = 1
|
||||||
|
settings.billing_cycle = "Monthly"
|
||||||
|
settings.billing_frequency = 24
|
||||||
|
# Enable invoicing
|
||||||
|
settings.enable_invoicing = 1
|
||||||
|
settings.make_payment_entry = 1
|
||||||
|
settings.company = company.name
|
||||||
|
settings.payment_account = company.default_cash_account
|
||||||
|
settings.debit_account = company.default_receivable_account
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
# make test plan
|
||||||
|
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
|
||||||
|
plan = frappe.new_doc("Membership Type")
|
||||||
|
plan.membership_type = "_rzpy_test_milythm"
|
||||||
|
plan.amount = 100
|
||||||
|
plan.razorpay_plan_id = "_rzpy_test_milythm"
|
||||||
|
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
|
||||||
|
plan.insert()
|
||||||
|
else:
|
||||||
|
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
|
||||||
|
|
||||||
|
# make test member
|
||||||
|
self.member_doc = create_member(frappe._dict({
|
||||||
|
'fullname': "_Test_Member",
|
||||||
|
'email': "_test_member_erpnext@example.com",
|
||||||
|
'plan_id': plan.name
|
||||||
|
}))
|
||||||
|
self.member_doc.make_customer_and_link()
|
||||||
|
self.member = self.member_doc.name
|
||||||
|
|
||||||
|
def test_auto_generate_invoice_and_payment_entry(self):
|
||||||
|
entry = make_membership(self.member)
|
||||||
|
|
||||||
|
# Naive test to see if at all invoice was generated and attached to member
|
||||||
|
# In any case if details were missing, the invoicing would throw an error
|
||||||
|
invoice = entry.generate_invoice(save=True)
|
||||||
|
self.assertEqual(invoice.name, entry.invoice)
|
||||||
|
|
||||||
|
def test_renew_within_30_days(self):
|
||||||
|
# create a membership for two months
|
||||||
|
# Should work fine
|
||||||
|
make_membership(self.member, { "from_date": nowdate() })
|
||||||
|
make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
|
||||||
|
|
||||||
|
from frappe.utils.user import add_role
|
||||||
|
add_role("test@example.com", "Non Profit Manager")
|
||||||
|
frappe.set_user("test@example.com")
|
||||||
|
|
||||||
|
# create next membership with expiry not within 30 days
|
||||||
|
self.assertRaises(frappe.ValidationError, make_membership, self.member, {
|
||||||
|
"from_date": add_months(nowdate(), 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
# create the same membership but as administrator
|
||||||
|
make_membership(self.member, {
|
||||||
|
"from_date": add_months(nowdate(), 2),
|
||||||
|
"to_date": add_months(nowdate(), 3),
|
||||||
|
})
|
||||||
|
|
||||||
|
def set_config(key, value):
|
||||||
|
frappe.db.set_value("Membership Settings", None, key, value)
|
||||||
|
|
||||||
|
def make_membership(member, payload={}):
|
||||||
|
data = {
|
||||||
|
"doctype": "Membership",
|
||||||
|
"member": member,
|
||||||
|
"membership_status": "Current",
|
||||||
|
"membership_type": "_rzpy_test_milythm",
|
||||||
|
"currency": "INR",
|
||||||
|
"paid": 1,
|
||||||
|
"from_date": nowdate(),
|
||||||
|
"amount": 100
|
||||||
|
}
|
||||||
|
data.update(payload)
|
||||||
|
membership = frappe.get_doc(data)
|
||||||
|
membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||||
|
return membership
|
||||||
|
|
||||||
|
def create_item(item_code):
|
||||||
|
if not frappe.db.exists("Item", item_code):
|
||||||
|
item = frappe.new_doc("Item")
|
||||||
|
item.item_code = item_code
|
||||||
|
item.item_name = item_code
|
||||||
|
item.stock_uom = "Nos"
|
||||||
|
item.description = item_code
|
||||||
|
item.item_group = "All Item Groups"
|
||||||
|
item.is_stock_item = 0
|
||||||
|
item.save()
|
||||||
|
else:
|
||||||
|
item = frappe.get_doc("Item", item_code)
|
||||||
|
return item
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.set_query('inv_print_format', function(doc) {
|
frm.set_query("inv_print_format", function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"doc_type": "Sales Invoice"
|
"doc_type": "Sales Invoice"
|
||||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query('membership_print_format', function(doc) {
|
frm.set_query("membership_print_format", function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"doc_type": "Membership"
|
"doc_type": "Membership"
|
||||||
@@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query('debit_account', function(doc) {
|
frm.set_query("debit_account", function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'account_type': 'Receivable',
|
"account_type": "Receivable",
|
||||||
'is_group': 0,
|
"is_group": 0,
|
||||||
'company': frm.doc.company
|
"company": frm.doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("payment_account", function () {
|
||||||
|
var account_types = ["Bank", "Cash"];
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"account_type": ["in", account_types],
|
||||||
|
"is_group": 0,
|
||||||
|
"company": frm.doc.company
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,9 +11,12 @@
|
|||||||
"billing_frequency",
|
"billing_frequency",
|
||||||
"webhook_secret",
|
"webhook_secret",
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
"enable_auto_invoicing",
|
"enable_invoicing",
|
||||||
|
"create_for_web_forms",
|
||||||
|
"make_payment_entry",
|
||||||
"company",
|
"company",
|
||||||
"debit_account",
|
"debit_account",
|
||||||
|
"payment_account",
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"send_email",
|
"send_email",
|
||||||
"send_invoice",
|
"send_invoice",
|
||||||
@@ -58,14 +61,7 @@
|
|||||||
"label": "Invoicing"
|
"label": "Invoicing"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"depends_on": "eval:doc.enable_invoicing",
|
||||||
"fieldname": "enable_auto_invoicing",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Auto Invoicing",
|
|
||||||
"mandatory_depends_on": "eval:doc.send_invoice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:doc.enable_auto_invoicing",
|
|
||||||
"fieldname": "debit_account",
|
"fieldname": "debit_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Debit Account",
|
"label": "Debit Account",
|
||||||
@@ -77,7 +73,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.enable_auto_invoicing",
|
"depends_on": "eval:doc.enable_invoicing",
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
@@ -86,7 +82,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
|
"depends_on": "eval:doc.enable_invoicing && doc.send_email",
|
||||||
"fieldname": "send_invoice",
|
"fieldname": "send_invoice",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Send Invoice with Email"
|
"label": "Send Invoice with Email"
|
||||||
@@ -119,11 +115,43 @@
|
|||||||
"label": "Email Template",
|
"label": "Email Template",
|
||||||
"mandatory_depends_on": "eval:doc.send_email",
|
"mandatory_depends_on": "eval:doc.send_email",
|
||||||
"options": "Email Template"
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_invoicing",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Invoicing",
|
||||||
|
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.enable_invoicing",
|
||||||
|
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
|
||||||
|
"fieldname": "make_payment_entry",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Make Payment Entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.make_payment_entry",
|
||||||
|
"fieldname": "payment_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Payment To",
|
||||||
|
"mandatory_depends_on": "eval:doc.make_payment_entry",
|
||||||
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.enable_invoicing",
|
||||||
|
"description": "Automatically create an invoice when payment is authorized from a web form entry",
|
||||||
|
"fieldname": "create_for_web_forms",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Auto Create Invoice for Web Forms"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-05 17:26:37.287395",
|
"modified": "2021-01-21 19:57:53.213286",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Non Profit",
|
"module": "Non Profit",
|
||||||
"name": "Membership Settings",
|
"name": "Membership Settings",
|
||||||
|
|||||||
@@ -2,13 +2,21 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Membership Type', {
|
frappe.ui.form.on('Membership Type', {
|
||||||
refresh: function(frm) {
|
refresh: function (frm) {
|
||||||
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
|
frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
|
||||||
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
|
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => {
|
frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
|
||||||
if (val) frm.set_df_property('linked_item', 'hidden', false);
|
if (val) frm.set_df_property('linked_item', 'hidden', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query('linked_item', () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_stock_item: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,9 +5,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
class MembershipType(Document):
|
class MembershipType(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
if self.linked_item:
|
||||||
|
is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
|
||||||
|
if is_stock_item:
|
||||||
|
frappe.throw(_("The Linked Item should be a service item"))
|
||||||
|
|
||||||
def get_membership_type(razorpay_id):
|
def get_membership_type(razorpay_id):
|
||||||
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
|
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
|
||||||
@@ -736,8 +736,9 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
|
|||||||
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
|
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
|
||||||
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||||
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
||||||
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
|
||||||
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
||||||
|
erpnext.patches.v13_0.update_member_email_address
|
||||||
|
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
||||||
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
||||||
erpnext.patches.v13_0.add_po_to_global_search
|
erpnext.patches.v13_0.add_po_to_global_search
|
||||||
|
|||||||
23
erpnext/patches/v13_0/update_member_email_address.py
Normal file
23
erpnext/patches/v13_0/update_member_email_address.py
Normal 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")
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"abbr",
|
"abbr",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"amount",
|
"amount",
|
||||||
|
"year_to_date",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"additional_salary",
|
"additional_salary",
|
||||||
"statistical_component",
|
"statistical_component",
|
||||||
@@ -226,11 +227,19 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_24",
|
"fieldname": "column_break_24",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
|
||||||
|
"fieldname": "year_to_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Year To Date",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-25 13:12:41.081106",
|
"modified": "2021-01-14 13:39:15.847158",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Detail",
|
"name": "Salary Detail",
|
||||||
|
|||||||
@@ -138,11 +138,11 @@ frappe.ui.form.on("Salary Slip", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
change_grid_labels: function(frm) {
|
change_grid_labels: function(frm) {
|
||||||
frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit",
|
let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit",
|
||||||
"tax_on_additional_salary"], frm.doc.currency, "earnings");
|
"tax_on_additional_salary"];
|
||||||
|
|
||||||
frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit",
|
frm.set_currency_labels(fields, frm.doc.currency, "earnings");
|
||||||
"tax_on_additional_salary"], frm.doc.currency, "deductions");
|
frm.set_currency_labels(fields, frm.doc.currency, "deductions");
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
|||||||
@@ -584,6 +584,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
|
||||||
"fieldname": "year_to_date",
|
"fieldname": "year_to_date",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Year To Date",
|
"label": "Year To Date",
|
||||||
@@ -591,6 +592,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.",
|
||||||
"fieldname": "month_to_date",
|
"fieldname": "month_to_date",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Month To Date",
|
"label": "Month To Date",
|
||||||
@@ -616,7 +618,7 @@
|
|||||||
"idx": 9,
|
"idx": 9,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-21 23:43:44.959840",
|
"modified": "2021-01-14 13:37:38.180920",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Slip",
|
"name": "Salary Slip",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class SalarySlip(TransactionBase):
|
|||||||
self.calculate_net_pay()
|
self.calculate_net_pay()
|
||||||
self.compute_year_to_date()
|
self.compute_year_to_date()
|
||||||
self.compute_month_to_date()
|
self.compute_month_to_date()
|
||||||
|
self.compute_component_wise_year_to_date()
|
||||||
|
|
||||||
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
|
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
|
||||||
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
|
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
|
||||||
@@ -1138,16 +1139,7 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
def compute_year_to_date(self):
|
def compute_year_to_date(self):
|
||||||
year_to_date = 0
|
year_to_date = 0
|
||||||
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
|
period_start_date, period_end_date = self.get_year_to_date_period()
|
||||||
|
|
||||||
if payroll_period:
|
|
||||||
period_start_date = payroll_period.start_date
|
|
||||||
period_end_date = payroll_period.end_date
|
|
||||||
else:
|
|
||||||
# get dates based on fiscal year if no payroll period exists
|
|
||||||
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
|
|
||||||
period_start_date = fiscal_year.year_start_date
|
|
||||||
period_end_date = fiscal_year.year_end_date
|
|
||||||
|
|
||||||
salary_slip_sum = frappe.get_list('Salary Slip',
|
salary_slip_sum = frappe.get_list('Salary Slip',
|
||||||
fields = ['sum(net_pay) as sum'],
|
fields = ['sum(net_pay) as sum'],
|
||||||
@@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase):
|
|||||||
month_to_date += self.net_pay
|
month_to_date += self.net_pay
|
||||||
self.month_to_date = month_to_date
|
self.month_to_date = month_to_date
|
||||||
|
|
||||||
|
def compute_component_wise_year_to_date(self):
|
||||||
|
period_start_date, period_end_date = self.get_year_to_date_period()
|
||||||
|
|
||||||
|
for key in ('earnings', 'deductions'):
|
||||||
|
for component in self.get(key):
|
||||||
|
year_to_date = 0
|
||||||
|
component_sum = frappe.db.sql("""
|
||||||
|
SELECT sum(detail.amount) as sum
|
||||||
|
FROM `tabSalary Detail` as detail
|
||||||
|
INNER JOIN `tabSalary Slip` as salary_slip
|
||||||
|
ON detail.parent = salary_slip.name
|
||||||
|
WHERE
|
||||||
|
salary_slip.employee_name = %(employee_name)s
|
||||||
|
AND detail.salary_component = %(component)s
|
||||||
|
AND salary_slip.start_date >= %(period_start_date)s
|
||||||
|
AND salary_slip.end_date < %(period_end_date)s
|
||||||
|
AND salary_slip.name != %(docname)s
|
||||||
|
AND salary_slip.docstatus = 1""",
|
||||||
|
{'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
|
||||||
|
'period_end_date': period_end_date, 'docname': self.name}
|
||||||
|
)
|
||||||
|
|
||||||
|
year_to_date = flt(component_sum[0][0]) if component_sum else 0.0
|
||||||
|
year_to_date += component.amount
|
||||||
|
component.year_to_date = year_to_date
|
||||||
|
|
||||||
|
def get_year_to_date_period(self):
|
||||||
|
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
|
||||||
|
|
||||||
|
if payroll_period:
|
||||||
|
period_start_date = payroll_period.start_date
|
||||||
|
period_end_date = payroll_period.end_date
|
||||||
|
else:
|
||||||
|
# get dates based on fiscal year if no payroll period exists
|
||||||
|
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
|
||||||
|
period_start_date = fiscal_year.year_start_date
|
||||||
|
period_end_date = fiscal_year.year_end_date
|
||||||
|
|
||||||
|
return period_start_date, period_end_date
|
||||||
|
|
||||||
|
|
||||||
def unlink_ref_doc_from_salary_slip(ref_no):
|
def unlink_ref_doc_from_salary_slip(ref_no):
|
||||||
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
|
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||||
where journal_entry=%s and docstatus < 2""", (ref_no))
|
where journal_entry=%s and docstatus < 2""", (ref_no))
|
||||||
|
|||||||
@@ -321,6 +321,38 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
year_to_date += flt(slip.net_pay)
|
year_to_date += flt(slip.net_pay)
|
||||||
self.assertEqual(slip.year_to_date, year_to_date)
|
self.assertEqual(slip.year_to_date, year_to_date)
|
||||||
|
|
||||||
|
def test_component_wise_year_to_date_computation(self):
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
applicant = make_employee("test_ytd@salary.com", company="_Test Company")
|
||||||
|
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||||
|
|
||||||
|
create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
|
||||||
|
company="_Test Company")
|
||||||
|
|
||||||
|
salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
|
||||||
|
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
|
||||||
|
|
||||||
|
# clear salary slip for this employee
|
||||||
|
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
|
||||||
|
|
||||||
|
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
|
||||||
|
payroll_period, deduct_random=False, num=3)
|
||||||
|
|
||||||
|
salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name":
|
||||||
|
"test_ytd@salary.com"}, order_by = "posting_date")
|
||||||
|
|
||||||
|
year_to_date = dict()
|
||||||
|
for slip in salary_slips:
|
||||||
|
doc = frappe.get_doc("Salary Slip", slip.name)
|
||||||
|
for entry in doc.get("earnings"):
|
||||||
|
if not year_to_date.get(entry.salary_component):
|
||||||
|
year_to_date[entry.salary_component] = 0
|
||||||
|
|
||||||
|
year_to_date[entry.salary_component] += entry.amount
|
||||||
|
self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date)
|
||||||
|
|
||||||
def test_tax_for_payroll_period(self):
|
def test_tax_for_payroll_period(self):
|
||||||
data = {}
|
data = {}
|
||||||
# test the impact of tax exemption declaration, tax exemption proof submission
|
# test the impact of tax exemption declaration, tax exemption proof submission
|
||||||
@@ -714,10 +746,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption =
|
|||||||
else:
|
else:
|
||||||
return income_tax_slab_name
|
return income_tax_slab_name
|
||||||
|
|
||||||
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
|
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12):
|
||||||
deducted_dates = []
|
deducted_dates = []
|
||||||
i = 0
|
i = 0
|
||||||
while i < 12:
|
while i < num:
|
||||||
slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee,
|
slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee,
|
||||||
"salary_structure": salary_structure, "frequency": "Monthly"})
|
"salary_structure": salary_structure, "frequency": "Monthly"})
|
||||||
if i == 0:
|
if i == 0:
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
company: function(frm) {
|
||||||
|
frm.trigger('set_earning_deduction_component');
|
||||||
|
},
|
||||||
|
|
||||||
currency: function(frm) {
|
currency: function(frm) {
|
||||||
calculate_totals(frm.doc);
|
calculate_totals(frm.doc);
|
||||||
@@ -117,6 +120,7 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
fields_read_only.forEach(function(field) {
|
fields_read_only.forEach(function(field) {
|
||||||
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
|
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
|
||||||
});
|
});
|
||||||
|
frm.trigger('set_earning_deduction_component');
|
||||||
},
|
},
|
||||||
|
|
||||||
assign_to_employees:function (frm) {
|
assign_to_employees:function (frm) {
|
||||||
|
|||||||
@@ -216,8 +216,13 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len,
|
|||||||
return frappe.db.sql("""
|
return frappe.db.sql("""
|
||||||
select t1.salary_component
|
select t1.salary_component
|
||||||
from `tabSalary Component` t1, `tabSalary Component Account` t2
|
from `tabSalary Component` t1, `tabSalary Component Account` t2
|
||||||
where t1.salary_component = t2.parent
|
where (t1.name = t2.parent
|
||||||
and t1.type = %s
|
and t1.type = %(type)s
|
||||||
and t2.company = %s
|
and t2.company = %(company)s)
|
||||||
|
or (t1.type = %(type)s
|
||||||
|
and t1.statistical_component = 1)
|
||||||
order by salary_component
|
order by salary_component
|
||||||
""", (filters['type'], filters['company']) )
|
""",{
|
||||||
|
"type": filters['type'],
|
||||||
|
"company": filters['company']
|
||||||
|
})
|
||||||
|
|||||||
0
erpnext/payroll/print_format/__init__.py
Normal file
0
erpnext/payroll/print_format/__init__.py
Normal 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"
|
||||||
|
}
|
||||||
@@ -161,9 +161,9 @@ def get_item_list(invoice):
|
|||||||
|
|
||||||
item.qty = abs(item.qty)
|
item.qty = abs(item.qty)
|
||||||
item.discount_amount = abs(item.discount_amount * item.qty)
|
item.discount_amount = abs(item.discount_amount * item.qty)
|
||||||
item.unit_rate = abs(item.base_amount / item.qty)
|
item.unit_rate = abs(item.base_net_amount / item.qty)
|
||||||
item.gross_amount = abs(item.base_amount)
|
item.gross_amount = abs(item.base_net_amount)
|
||||||
item.taxable_value = abs(item.base_amount)
|
item.taxable_value = abs(item.base_net_amount)
|
||||||
|
|
||||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
||||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||||
@@ -198,7 +198,7 @@ def update_item_taxes(invoice, item):
|
|||||||
if t.account_head in gst_accounts_list:
|
if t.account_head in gst_accounts_list:
|
||||||
item_tax_rate = item_tax_detail[0]
|
item_tax_rate = item_tax_detail[0]
|
||||||
# item tax amount excluding discount amount
|
# item tax amount excluding discount amount
|
||||||
item_tax_amount = (item_tax_rate / 100) * item.base_amount
|
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
|
||||||
|
|
||||||
if t.account_head in gst_accounts.cess_account:
|
if t.account_head in gst_accounts.cess_account:
|
||||||
item_tax_amount_after_discount = item_tax_detail[1]
|
item_tax_amount_after_discount = item_tax_detail[1]
|
||||||
@@ -217,8 +217,14 @@ def update_item_taxes(invoice, item):
|
|||||||
|
|
||||||
def get_invoice_value_details(invoice):
|
def get_invoice_value_details(invoice):
|
||||||
invoice_value_details = frappe._dict(dict())
|
invoice_value_details = frappe._dict(dict())
|
||||||
invoice_value_details.base_total = abs(invoice.base_total)
|
|
||||||
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
|
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||||
|
invoice_value_details.base_total = abs(invoice.base_total)
|
||||||
|
else:
|
||||||
|
invoice_value_details.base_total = abs(invoice.base_net_total)
|
||||||
|
|
||||||
|
# since tax already considers discount amount
|
||||||
|
invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount
|
||||||
invoice_value_details.round_off = invoice.base_rounding_adjustment
|
invoice_value_details.round_off = invoice.base_rounding_adjustment
|
||||||
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
|
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
|
||||||
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
|
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
|
||||||
@@ -244,9 +250,9 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
|||||||
|
|
||||||
for tax_type in ['igst', 'cgst', 'sgst']:
|
for tax_type in ['igst', 'cgst', 'sgst']:
|
||||||
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
||||||
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount)
|
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
|
||||||
else:
|
else:
|
||||||
invoice_value_details.total_other_charges += abs(t.base_tax_amount)
|
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
|
||||||
|
|
||||||
return invoice_value_details
|
return invoice_value_details
|
||||||
|
|
||||||
@@ -473,7 +479,7 @@ class GSPConnector():
|
|||||||
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
|
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
|
||||||
"response": json.dumps(res, indent=4) if res else None
|
"response": json.dumps(res, indent=4) if res else None
|
||||||
})
|
})
|
||||||
request_log.insert(ignore_permissions=True)
|
request_log.save(ignore_permissions=True)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
def fetch_auth_token(self):
|
def fetch_auth_token(self):
|
||||||
@@ -486,7 +492,8 @@ class GSPConnector():
|
|||||||
res = self.make_request('post', self.authenticate_url, headers)
|
res = self.make_request('post', self.authenticate_url, headers)
|
||||||
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
|
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
|
||||||
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
|
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
|
||||||
self.e_invoice_settings.save()
|
self.e_invoice_settings.save(ignore_permissions=True)
|
||||||
|
self.e_invoice_settings.reload()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log_error(res)
|
self.log_error(res)
|
||||||
@@ -757,7 +764,7 @@ class GSPConnector():
|
|||||||
'label': _('IRN Generated')
|
'label': _('IRN Generated')
|
||||||
}
|
}
|
||||||
self.update_invoice()
|
self.update_invoice()
|
||||||
|
|
||||||
def attach_qrcode_image(self):
|
def attach_qrcode_image(self):
|
||||||
qrcode = self.invoice.signed_qr_code
|
qrcode = self.invoice.signed_qr_code
|
||||||
doctype = self.invoice.doctype
|
doctype = self.invoice.doctype
|
||||||
@@ -768,7 +775,7 @@ class GSPConnector():
|
|||||||
'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
|
'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
|
||||||
'attached_to_doctype': doctype,
|
'attached_to_doctype': doctype,
|
||||||
'attached_to_name': docname,
|
'attached_to_name': docname,
|
||||||
'content': 'qrcode',
|
'content': str(base64.b64encode(os.urandom(64))),
|
||||||
'is_private': 1
|
'is_private': 1
|
||||||
})
|
})
|
||||||
_file.insert()
|
_file.insert()
|
||||||
|
|||||||
@@ -233,7 +233,8 @@ def get_stock_ledger_entries(filters):
|
|||||||
from `tabItem` {item_conditions}) item
|
from `tabItem` {item_conditions}) item
|
||||||
where item_code = item.name and
|
where item_code = item.name and
|
||||||
company = %(company)s and
|
company = %(company)s and
|
||||||
posting_date <= %(to_date)s
|
posting_date <= %(to_date)s and
|
||||||
|
is_cancelled != 1
|
||||||
{sle_conditions}
|
{sle_conditions}
|
||||||
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
|
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
|
||||||
.format(item_conditions=get_item_conditions(filters),
|
.format(item_conditions=get_item_conditions(filters),
|
||||||
|
|||||||
Reference in New Issue
Block a user