Merge branch 'develop' into item-wise-purchase-registry-item-name-error

This commit is contained in:
Marica 2021-01-22 12:35:35 +05:30 committed by GitHub
commit 5631d014a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 500 additions and 219 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

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

@ -8,12 +8,12 @@
"is_mandatory": 0, "is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-14 17:38:27.496696", "modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create Opportunity", "name": "Create Opportunity",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Opportunity", "reference_document": "Opportunity",
"show_full_form": 0, "show_full_form": 1,
"title": "Create Opportunity", "title": "Create Opportunity",
"validate_action": 1 "validate_action": 1
} }

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",
args: {save: true},
freeze: true,
freeze_message: __("Creating Membership Invoice"),
callback: function(r) {
if (r.invoice)
frm.reload_doc(); frm.reload_doc();
}
}); });
}); });
@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', {
}, },
onload: function(frm) { onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount'); frm.add_fetch("membership_type", "amount", "amount");
} }
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,11 +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())
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total) invoice_value_details.base_total = abs(invoice.base_total)
else: else:
invoice_value_details.base_total = abs(invoice.base_net_total) invoice_value_details.base_total = abs(invoice.base_net_total)
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
# 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)
@ -247,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

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,32 @@
# 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
from __future__ import unicode_literals
import frappe
import json import json
from frappe import _, _dict
from frappe.utils import nowdate
from frappe.utils.data import fmt_money
from erpnext.accounts.utils import get_fiscal_year
from PyPDF2 import PdfFileWriter from PyPDF2 import PdfFileWriter
import frappe
from erpnext.accounts.utils import get_fiscal_year
from frappe import _
from frappe.utils import cstr, nowdate
from frappe.utils.data import fmt_money
from frappe.utils.jinja import render_template
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
from frappe.utils.print_format import read_multi_pdf from frappe.utils.print_format import read_multi_pdf
from frappe.utils.jinja import render_template
IRS_1099_FORMS_FILE_EXTENSION = ".pdf"
def execute(filters=None): def execute(filters=None):
filters = filters if isinstance(filters, _dict) else _dict(filters) filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters)
if not filters: if not filters:
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
filters.setdefault('company', frappe.db.get_default("company")) filters.setdefault('company', frappe.db.get_default("company"))
region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) region = frappe.db.get_value("Company",
filters={"name": filters.company},
fieldname=["country"])
if region != 'United States': if region != 'United States':
return [], [] return [], []
@ -34,20 +39,23 @@ def execute(filters=None):
s.tax_id as "tax_id", s.tax_id as "tax_id",
SUM(gl.debit_in_account_currency) AS "payments" SUM(gl.debit_in_account_currency) AS "payments"
FROM FROM
`tabGL Entry` gl INNER JOIN `tabSupplier` s `tabGL Entry` gl
INNER JOIN `tabSupplier` s
WHERE WHERE
s.name = gl.party s.name = gl.party
AND s.irs_1099 = 1 AND s.irs_1099 = 1
AND gl.fiscal_year = %(fiscal_year)s AND gl.fiscal_year = %(fiscal_year)s
AND gl.party_type = "Supplier" AND gl.party_type = "Supplier"
GROUP BY GROUP BY
gl.party gl.party
ORDER BY ORDER BY
gl.party DESC""", {"fiscal_year": filters.fiscal_year, gl.party DESC
""", {
"fiscal_year": filters.fiscal_year,
"supplier_group": filters.supplier_group, "supplier_group": filters.supplier_group,
"company": filters.company}, as_dict=True) "company": filters.company
}, as_dict=True)
return columns, data return columns, data
@ -74,7 +82,6 @@ def get_columns():
"width": 120 "width": 120
}, },
{ {
"fieldname": "payments", "fieldname": "payments",
"label": _("Total Payments"), "label": _("Total Payments"),
"fieldtype": "Currency", "fieldtype": "Currency",
@ -88,23 +95,32 @@ def irs_1099_print(filters):
if not filters: if not filters:
frappe._dict({ frappe._dict({
"company": frappe.db.get_default("Company"), "company": frappe.db.get_default("Company"),
"fiscal_year": frappe.db.get_default("fiscal_year")}) "fiscal_year": frappe.db.get_default("Fiscal Year")
})
else: else:
filters = frappe._dict(json.loads(filters)) filters = frappe._dict(json.loads(filters))
fiscal_year_doc = get_fiscal_year(fiscal_year=filters.fiscal_year, as_dict=True)
fiscal_year = cstr(fiscal_year_doc.year_start_date.year)
company_address = get_payer_address_html(filters.company) company_address = get_payer_address_html(filters.company)
company_tin = frappe.db.get_value("Company", filters.company, "tax_id") company_tin = frappe.db.get_value("Company", filters.company, "tax_id")
columns, data = execute(filters) columns, data = execute(filters)
template = frappe.get_doc("Print Format", "IRS 1099 Form").html template = frappe.get_doc("Print Format", "IRS 1099 Form").html
output = PdfFileWriter() output = PdfFileWriter()
for row in data: for row in data:
row["fiscal_year"] = fiscal_year
row["company"] = filters.company row["company"] = filters.company
row["company_tin"] = company_tin row["company_tin"] = company_tin
row["payer_street_address"] = company_address row["payer_street_address"] = company_address
row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html("Supplier", row.supplier) row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html(
"Supplier", row.supplier)
row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") row["payments"] = fmt_money(row["payments"], precision=0, currency="USD")
frappe._dict(row)
pdf = get_pdf(render_template(template, row), output=output if output else None) pdf = get_pdf(render_template(template, row), output=output if output else None)
frappe.local.response.filename = filters.fiscal_year + " " + filters.company + " IRS 1099 Forms"
frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}"
frappe.local.response.filecontent = read_multi_pdf(output) frappe.local.response.filecontent = read_multi_pdf(output)
frappe.local.response.type = "download" frappe.local.response.type = "download"
@ -121,35 +137,44 @@ def get_payer_address_html(company):
address_type="Postal" DESC, address_type="Billing" DESC address_type="Postal" DESC, address_type="Billing" DESC
LIMIT 1 LIMIT 1
""", {"company": company}, as_dict=True) """, {"company": company}, as_dict=True)
address_display = ""
if address_list: if address_list:
company_address = address_list[0]["name"] company_address = address_list[0]["name"]
return frappe.get_doc("Address", company_address).get_display() address_display = frappe.get_doc("Address", company_address).get_display()
else:
return "" return address_display
def get_street_address_html(party_type, party): def get_street_address_html(party_type, party):
address_list = frappe.db.sql(""" address_list = frappe.db.sql("""
SELECT SELECT
link.parent link.parent
FROM `tabDynamic Link` link, `tabAddress` address FROM
WHERE link.parenttype = "Address" `tabDynamic Link` link,
`tabAddress` address
WHERE
link.parenttype = "Address"
AND link.link_name = %(party)s AND link.link_name = %(party)s
ORDER BY address.address_type="Postal" DESC, ORDER BY
address.address_type="Postal" DESC,
address.address_type="Billing" DESC address.address_type="Billing" DESC
LIMIT 1 LIMIT 1
""", {"party": party}, as_dict=True) """, {"party": party}, as_dict=True)
street_address = city_state = ""
if address_list: if address_list:
supplier_address = address_list[0]["parent"] supplier_address = address_list[0]["parent"]
doc = frappe.get_doc("Address", supplier_address) doc = frappe.get_doc("Address", supplier_address)
if doc.address_line2: if doc.address_line2:
street = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n" street_address = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n"
else: else:
street = doc.address_line1 + "<br>\n" street_address = doc.address_line1 + "<br>\n"
city = doc.city + ", " if doc.city else ""
city = city + doc.state + " " if doc.state else city city_state = doc.city + ", " if doc.city else ""
city = city + doc.pincode if doc.pincode else city city_state = city_state + doc.state + " " if doc.state else city_state
city += "<br>\n" city_state = city_state + doc.pincode if doc.pincode else city_state
return street, city city_state += "<br>\n"
else:
return "", "" return street_address, city_state

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