Merge branch 'develop' into service-units-in-appointment

This commit is contained in:
Rucha Mahabal
2021-01-22 09:09:29 +05:30
committed by GitHub
33 changed files with 663 additions and 192 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_details_section', 'hidden', false); if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
}) })
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.__islocal)
return;
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", { frm.call({
save: true doc: frm.doc,
}).then(() => { method: "generate_invoice",
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");
} }
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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