diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 38d8a62f07..5a5c448026 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -132,16 +132,10 @@ def allow_regional(fn): return caller -def get_last_membership(): +def get_last_membership(member): '''Returns last membership if exists''' last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) + dict(member=member, paid=1), order_by='to_date desc', limit=1) - return last_membership and last_membership[0] - -def is_member(): - '''Returns true if the user is still a member''' - last_membership = get_last_membership() - if last_membership and getdate(last_membership.to_date) > getdate(): - return True - return False + if last_membership: + return last_membership[0] diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index de9498e075..49b2b186c4 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -1,5 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide('erpnext.integrations'); frappe.ui.form.on('Bank', { onload: function(frm) { @@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', { frm.set_df_property('address_and_contact', 'hidden', 0); frappe.contacts.render_address_and_contact(frm); } - }, + if (frm.doc.plaid_access_token) { + frm.add_custom_button(__('Refresh Plaid Link'), () => { + new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token); + }); + } + } }); @@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) { frm.doc.name).options = options; frm.fields_dict.bank_transaction_mapping.grid.refresh(); -}; \ No newline at end of file +}; + +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' }); + } +}; diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5435d3b21e..3a6dbeb51c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1861,23 +1861,6 @@ class TestSalesInvoice(unittest.TestCase): def test_einvoice_json(self): from erpnext.regional.india.e_invoice.utils import make_einvoice - customer_gstin = '27AACCM7806M1Z3' - customer_gstin_dtls = { - 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City', - 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg', - 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' - } - company_gstin = '27AAECE4835E1ZR' - company_gstin_dtls = { - 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City', - 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg', - 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' - } - # set cache gstin details to avoid fetching details which will require connection to GSP servers - frappe.local.gstin_cache = {} - frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls - frappe.local.gstin_cache[company_gstin] = company_gstin_dtls - si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' si.items = [] @@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(value_details['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value) - self.assertEqual( - value_details['TotInvVal'], - value_details['AssVal'] + value_details['CgstVal'] - + value_details['SgstVal'] + value_details['IgstVal'] + calculated_invoice_value = \ + value_details['AssVal'] + value_details['CgstVal'] \ + + value_details['SgstVal'] + value_details['IgstVal'] \ + value_details['OthChrg'] - value_details['Discount'] - ) + + self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1) self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6edc020701..4dee375e5a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -336,7 +336,7 @@ class BuyingController(StockController): raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_serial_nos = raw_material_data.get('serial_no', '') consumed_batch_nos = raw_material_data.get('batch_nos', '') transferred_qty = raw_material.qty diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 8d4b510490..66d0e5f77d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -29,14 +29,11 @@ class PlaidConnector(): response = self.client.Item.public_token.exchange(public_token) access_token = response["access_token"] return access_token - - def get_link_token(self): + + def get_token_request(self, update_mode=False): country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] - token_request = { + args = { "client_name": self.client_name, - "client_id": self.settings.plaid_client_id, - "secret": self.settings.plaid_secret, - "products": self.products, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", "country_codes": country_codes, @@ -45,6 +42,20 @@ class PlaidConnector(): } } + if update_mode: + args["access_token"] = self.access_token + else: + args.update({ + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + }) + + return args + + def get_link_token(self, update_mode=False): + token_request = self.get_token_request(update_mode) + try: response = self.client.LinkToken.create(token_request) except InvalidRequestError: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index f26b130805..bbc2ca8846 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', { refresh: function (frm) { if (frm.doc.enabled) { - frm.add_custom_button('Link a new bank account', () => { + frm.add_custom_button(__('Link a new bank account'), () => { new erpnext.integrations.plaidLink(frm); }); @@ -46,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink { this.product = ["auth", "transactions"]; this.plaid_env = this.frm.doc.plaid_env; this.client_name = frappe.boot.sitename; - this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.token = await this.get_link_token(); this.init_plaid(); } + async get_link_token() { + const token = await this.frm.call("get_link_token").then(resp => resp.message); + if (!token) { + frappe.throw(__('Cannot retrieve link token. Check Error Log for more information')); + } + return token; + } + init_plaid() { const me = this; me.loadScript(me.plaidUrl) @@ -94,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptError(error) { - frappe.msgprint("There was an issue connecting to Plaid's authentication server"); - frappe.msgprint(error); + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); } plaid_success(token, response) { @@ -123,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink { }); }, __("Select a company"), __("Continue")); } -}; +}; \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e12d9ee46c..70c7f3fe5d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -230,7 +230,6 @@ def automatic_synchronization(): if settings.enabled == 1 and settings.automatic_sync == 1: enqueue_synchronization() - @frappe.whitelist() def enqueue_synchronization(): plaid_accounts = frappe.get_all("Bank Account", @@ -243,3 +242,8 @@ def enqueue_synchronization(): bank=plaid_account.bank, bank_account=plaid_account.name ) + +@frappe.whitelist() +def get_link_token_for_update(access_token): + plaid = PlaidConnector(access_token) + return plaid.get_link_token(update_mode=True) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a2d9d861bb..f7ec1c1b6e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,7 +341,8 @@ scheduler_events = { "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", - "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" + "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", + "erpnext.non_profit.doctype.membership.membership.set_expired_status" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 992ef16d64..f190cfae75 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -12,7 +12,6 @@ "membership_expiry_date", "column_break_5", "membership_type", - "email", "email_id", "image", "customer_section", @@ -64,13 +63,6 @@ "options": "Membership Type", "reqd": 1 }, - { - "fieldname": "email", - "fieldtype": "Link", - "in_list_view": 1, - "label": "User", - "options": "User" - }, { "fieldname": "image", "fieldtype": "Attach Image", @@ -178,7 +170,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-09-16 23:44:13.596948", + "modified": "2020-11-09 12:12:10.174647", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 25d6b53830..04b99f93f2 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -18,8 +18,6 @@ class Member(Document): def validate(self): - if self.email: - self.validate_email_type(self.email) if self.email_id: self.validate_email_type(self.email_id) @@ -57,14 +55,16 @@ class Member(Document): def make_customer_and_link(self): if self.customer: frappe.msgprint(_("A customer is already linked to this Member")) - cust = create_customer(frappe._dict({ + + customer = create_customer(frappe._dict({ 'fullname': self.member_name, - 'email': self.email_id or self.email, + 'email': self.email_id, 'phone': None })) - self.customer = cust + self.customer = customer self.save() + frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer)) def get_or_create_member(user_details): diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index ee8a8c0a7b..573ac3319a 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -4,16 +4,25 @@ frappe.ui.form.on('Membership', { setup: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { - if (val) frm.set_df_property('razorpay_details_section', 'hidden', false); + if (val) frm.set_df_property("razorpay_details_section", "hidden", false); }) }, refresh: function(frm) { + if (frm.doc.__islocal) + return; + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { - frm.call("generate_invoice", { - save: true - }).then(() => { - frm.reload_doc(); + frm.call({ + doc: frm.doc, + method: "generate_invoice", + args: {save: true}, + freeze: true, + freeze_message: __("Creating Membership Invoice"), + callback: function(r) { + if (r.invoice) + frm.reload_doc(); + } }); }); @@ -27,6 +36,6 @@ frappe.ui.form.on('Membership', { }, onload: function(frm) { - frm.add_fetch('membership_type', 'amount', 'amount'); + frm.add_fetch("membership_type", "amount", "amount"); } }); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 7f218966a0..6da053f9fc 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "member", + "member_name", "membership_type", "column_break_3", "membership_status", @@ -46,6 +47,8 @@ { "fieldname": "membership_status", "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Membership Status", "options": "New\nCurrent\nExpired\nPending\nCancelled" }, @@ -122,11 +125,18 @@ "fieldtype": "Link", "label": "Invoice", "options": "Sales Invoice" + }, + { + "fetch_from": "member.member_name", + "fieldname": "member_name", + "fieldtype": "Data", + "label": "Member Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-19 14:28:11.532696", + "modified": "2021-01-21 16:31:20.032656", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", @@ -158,7 +168,9 @@ } ], "restrict_to_domain": "Non Profit", + "search_fields": "member, member_name", "sort_field": "modified", "sort_order": "DESC", + "title_field": "member_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 7d15abaa3b..c113b80d56 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -14,33 +14,43 @@ from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext - class Membership(Document): def validate(self): if not self.member or not frappe.db.exists("Member", self.member): - member_name = frappe.get_value('Member', dict(email=frappe.session.user)) + # for web forms + user_type = frappe.db.get_value("User", frappe.session.user, "user_type") + if user_type == "Website User": + self.create_member_from_website_user() + else: + frappe.throw(_("Please select a Member")) - if not member_name: - user = frappe.get_doc('User', frappe.session.user) - member = frappe.get_doc(dict( - doctype='Member', - email=frappe.session.user, - membership_type=self.membership_type, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) - member_name = member.name + self.validate_membership_period() - if self.get("__islocal"): - self.member = member_name + def create_member_from_website_user(self): + member_name = frappe.get_value("Member", dict(email_id=frappe.session.user)) + if not member_name: + user = frappe.get_doc("User", frappe.session.user) + member = frappe.get_doc(dict( + doctype="Member", + email_id=frappe.session.user, + membership_type=self.membership_type, + member_name=user.get_fullname() + )).insert(ignore_permissions=True) + member_name = member.name + + if self.get("__islocal"): + self.member = member_name + + def validate_membership_period(self): # get last membership (if active) - last_membership = erpnext.get_last_membership() + last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership if last_membership and not frappe.session.user == "Administrator": # if last membership does not expire in 30 days, then do not allow to renew if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : - frappe.throw(_('You can only renew if your membership expires within 30 days')) + frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) elif frappe.session.user == "Administrator": @@ -54,11 +64,16 @@ class Membership(Document): self.to_date = add_months(self.from_date, 1) def on_payment_authorized(self, status_changed_to=None): - if status_changed_to in ("Completed", "Authorized"): - self.load_from_db() - self.db_set('paid', 1) + if status_changed_to not in ("Completed", "Authorized"): + return + self.load_from_db() + self.db_set("paid", 1) + settings = frappe.get_doc("Membership Settings") + if settings.enable_invoicing and settings.create_for_web_forms: + self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) - def generate_invoice(self, save=True): + + def generate_invoice(self, save=True, with_payment_entry=False): if not (self.paid or self.currency or self.amount): frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) @@ -66,34 +81,64 @@ class Membership(Document): frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) - plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Membership Settings") - if not member.customer: frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) - if not settings.debit_account: - frappe.throw(_("You need to set Debit Account in Membership Settings")) - - if not settings.company: - frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) self.invoice = invoice.name + if with_payment_entry: + self.make_payment_entry(settings, invoice) + if save: self.save() return invoice + def validate_membership_type_and_settings(self, plan, settings): + settings_link = get_link_to_form("Membership Type", self.membership_type) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) + + if not settings.company: + frappe.throw(_("You need to set Default Company 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 Payment Account in {0}").format( + get_link_to_form("Membership Type", self.membership_type))) + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.flags.ignore_account_permission = True + pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) + frappe.flags.ignore_account_permission=False + pe.paid_to = settings.payment_account + pe.reference_no = self.name + pe.reference_date = getdate() + pe.save(ignore_permissions=True) + pe.submit() + def send_acknowlement(self): settings = frappe.get_doc("Membership Settings") if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( + get_link_to_form("Membership Settings", "Membership Settings"))) member = frappe.get_doc("Member", self.member) + if not member.email_id: + frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) + plan = frappe.get_doc("Membership Type", self.membership_type) - email = member.email_id if member.email_id else member.email + email = member.email_id attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] if self.invoice and settings.send_invoice: @@ -112,48 +157,56 @@ class Membership(Document): } if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) else: frappe.sendmail(**email_args) def generate_and_send_invoice(self): - invoice = self.generate_invoice(False) + self.generate_invoice(save=False) self.send_acknowlement() + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ - 'doctype': 'Sales Invoice', - 'customer': member.customer, - 'debit_to': settings.debit_account, - 'currency': membership.currency, - 'is_pos': 0, - 'items': [ + "doctype": "Sales Invoice", + "customer": member.customer, + "debit_to": settings.debit_account, + "currency": membership.currency, + "company": settings.company, + "is_pos": 0, + "items": [ { - 'item_code': plan.linked_item, - 'rate': membership.amount, - 'qty': 1 + "item_code": plan.linked_item, + "rate": membership.amount, + "qty": 1 } ] }) - + invoice.set_missing_values() invoice.insert(ignore_permissions=True) invoice.submit() + frappe.msgprint(_("Sales Invoice created successfully")) + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ - 'subscription_id': subscription_id, - 'email_id': email + "subscription_id": subscription_id, + "email_id": email }, order_by="creation desc") try: - return frappe.get_doc("Member", members[0]['name']) + return frappe.get_doc("Member", members[0]["name"]) except: return None + def verify_signature(data): - signature = frappe.request.headers.get('X-Razorpay-Signature') + if frappe.flags.in_test: + return True + signature = frappe.request.headers.get("X-Razorpay-Signature") settings = frappe.get_doc("Membership Settings") key = settings.get_webhook_secret() @@ -162,6 +215,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) + @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) @@ -170,16 +224,16 @@ def trigger_razorpay_subscription(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Webhook Verification Error") notify_failure(log) - return { 'status': 'Failed', 'reason': e} + return { "status": "Failed", "reason": e} if isinstance(data, six.string_types): data = json.loads(data) data = frappe._dict(data) - subscription = data.payload.get("subscription", {}).get('entity', {}) + subscription = data.payload.get("subscription", {}).get("entity", {}) subscription = frappe._dict(subscription) - payment = data.payload.get("payment", {}).get('entity', {}) + payment = data.payload.get("payment", {}).get("entity", {}) payment = frappe._dict(payment) try: @@ -189,15 +243,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member = get_member_based_on_subscription(subscription.id, payment.email) if not member: member = create_member(frappe._dict({ - 'fullname': payment.email, - 'email': payment.email, - 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + "fullname": payment.email, + "email": payment.email, + "plan_id": get_plan_from_razorpay_id(subscription.plan_id) })) member.subscription_id = subscription.id member.customer_id = payment.customer_id if subscription.notes and type(subscription.notes) == dict: - notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items()) member.add_comment("Comment", notes) elif subscription.notes and type(subscription.notes) == str: member.add_comment("Comment", subscription.notes) @@ -227,28 +281,39 @@ def trigger_razorpay_subscription(*args, **kwargs): message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) notify_failure(log) - return { 'status': 'Failed', 'reason': e} + return { "status": "Failed", "reason": e} - return { 'status': 'Success' } + return { "status": "Success" } def notify_failure(log): try: - content = """Dear System Manager, -Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below + content = """ + Dear System Manager, + Razorpay webhook for creating renewing membership subscription failed due to some reason. + Please check the following error log linked below + Error Log: {0} + Regards, Administrator + """.format(get_link_to_form("Error Log", log.name)) -Error Log: {0} - -Regards, -Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + def get_plan_from_razorpay_id(plan_id): - plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc") try: - return plan[0]['name'] + return plan[0]["name"] except: return None + + +def set_expired_status(): + frappe.db.sql(""" + UPDATE + `tabMembership` SET `status` = 'Expired' + WHERE + `status` not in ('Cancelled') AND `to_date` < %s + """, (nowdate())) \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js new file mode 100644 index 0000000000..a959159899 --- /dev/null +++ b/erpnext/non_profit/doctype/membership/membership_list.js @@ -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']; + } + } +}; diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b23f4062a9..ff7e6c473c 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -2,8 +2,110 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - import unittest +import frappe +import erpnext +from erpnext.non_profit.doctype.member.member import create_member +from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): - pass + def setUp(self): + # Get default company + company = frappe.get_doc("Company", erpnext.get_default_company()) + + # update membership settings + settings = frappe.get_doc("Membership Settings") + # Enable razorpay + settings.enable_razorpay = 1 + settings.billing_cycle = "Monthly" + settings.billing_frequency = 24 + # Enable invoicing + settings.enable_invoicing = 1 + settings.make_payment_entry = 1 + settings.company = company.name + settings.payment_account = company.default_cash_account + settings.debit_account = company.default_receivable_account + settings.save() + + # make test plan + if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): + plan = frappe.new_doc("Membership Type") + plan.membership_type = "_rzpy_test_milythm" + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership").name + plan.insert() + else: + plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + + # make test member + self.member_doc = create_member(frappe._dict({ + 'fullname': "_Test_Member", + 'email': "_test_member_erpnext@example.com", + 'plan_id': plan.name + })) + self.member_doc.make_customer_and_link() + self.member = self.member_doc.name + + def test_auto_generate_invoice_and_payment_entry(self): + entry = make_membership(self.member) + + # Naive test to see if at all invoice was generated and attached to member + # In any case if details were missing, the invoicing would throw an error + invoice = entry.generate_invoice(save=True) + self.assertEqual(invoice.name, entry.invoice) + + def test_renew_within_30_days(self): + # create a membership for two months + # Should work fine + make_membership(self.member, { "from_date": nowdate() }) + make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) + + from frappe.utils.user import add_role + add_role("test@example.com", "Non Profit Manager") + frappe.set_user("test@example.com") + + # create next membership with expiry not within 30 days + self.assertRaises(frappe.ValidationError, make_membership, self.member, { + "from_date": add_months(nowdate(), 2), + }) + + frappe.set_user("Administrator") + # create the same membership but as administrator + make_membership(self.member, { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3), + }) + +def set_config(key, value): + frappe.db.set_value("Membership Settings", None, key, value) + +def make_membership(member, payload={}): + data = { + "doctype": "Membership", + "member": member, + "membership_status": "Current", + "membership_type": "_rzpy_test_milythm", + "currency": "INR", + "paid": 1, + "from_date": nowdate(), + "amount": 100 + } + data.update(payload) + membership = frappe.get_doc(data) + membership.insert(ignore_permissions=True, ignore_if_duplicate=True) + return membership + +def create_item(item_code): + if not frappe.db.exists("Item", item_code): + item = frappe.new_doc("Item") + item.item_code = item_code + item.item_name = item_code + item.stock_uom = "Nos" + item.description = item_code + item.item_group = "All Item Groups" + item.is_stock_item = 0 + item.save() + else: + item = frappe.get_doc("Item", item_code) + return item diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 1d894027b0..c95aab2a7a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('inv_print_format', function(doc) { + frm.set_query("inv_print_format", function() { return { filters: { "doc_type": "Sales Invoice" @@ -19,7 +19,7 @@ frappe.ui.form.on("Membership Settings", { }; }); - frm.set_query('membership_print_format', function(doc) { + frm.set_query("membership_print_format", function() { return { filters: { "doc_type": "Membership" @@ -27,12 +27,23 @@ frappe.ui.form.on("Membership Settings", { }; }); - frm.set_query('debit_account', function(doc) { + frm.set_query("debit_account", function() { return { filters: { - 'account_type': 'Receivable', - 'is_group': 0, - 'company': frm.doc.company + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.company + } + }; + }); + + frm.set_query("payment_account", function () { + var account_types = ["Bank", "Cash"]; + return { + filters: { + "account_type": ["in", account_types], + "is_group": 0, + "company": frm.doc.company } }; }); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 5b6bab5b0a..3887b0a2be 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,9 +11,12 @@ "billing_frequency", "webhook_secret", "column_break_6", - "enable_auto_invoicing", + "enable_invoicing", + "create_for_web_forms", + "make_payment_entry", "company", "debit_account", + "payment_account", "column_break_9", "send_email", "send_invoice", @@ -58,14 +61,7 @@ "label": "Invoicing" }, { - "default": "0", - "fieldname": "enable_auto_invoicing", - "fieldtype": "Check", - "label": "Enable Auto Invoicing", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", @@ -77,7 +73,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -86,7 +82,7 @@ }, { "default": "0", - "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", + "depends_on": "eval:doc.enable_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", "label": "Send Invoice with Email" @@ -119,11 +115,43 @@ "label": "Email Template", "mandatory_depends_on": "eval:doc.send_email", "options": "Email Template" + }, + { + "default": "0", + "fieldname": "enable_invoicing", + "fieldtype": "Check", + "label": "Enable Invoicing", + "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_invoicing", + "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", + "fieldname": "make_payment_entry", + "fieldtype": "Check", + "label": "Make Payment Entry" + }, + { + "depends_on": "eval:doc.make_payment_entry", + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment To", + "mandatory_depends_on": "eval:doc.make_payment_entry", + "options": "Account" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_invoicing", + "description": "Automatically create an invoice when payment is authorized from a web form entry", + "fieldname": "create_for_web_forms", + "fieldtype": "Check", + "label": "Auto Create Invoice for Web Forms" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-05 17:26:37.287395", + "modified": "2021-01-21 19:57:53.213286", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 43311a2c96..91a5cb74ba 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -2,13 +2,21 @@ // For license information, please see license.txt frappe.ui.form.on('Membership Type', { - refresh: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + refresh: function (frm) { + frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); }); - frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => { if (val) frm.set_df_property('linked_item', 'hidden', false); }); + + frm.set_query('linked_item', () => { + return { + filters: { + is_stock_item: 0 + } + }; + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index b95b04316f..022829bd3a 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -5,9 +5,14 @@ from __future__ import unicode_literals from frappe.model.document import Document import frappe +from frappe import _ class MembershipType(Document): - pass + def validate(self): + if self.linked_item: + is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item") + if is_stock_item: + frappe.throw(_("The Linked Item should be a service item")) def get_membership_type(razorpay_id): return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7b2428ebb5..62e9b5cb6a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -736,8 +736,9 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee -erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.update_member_email_address +erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py new file mode 100644 index 0000000000..4056f84069 --- /dev/null +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -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") diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 5c1eb61281..393f647cc8 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -9,6 +9,7 @@ "abbr", "column_break_3", "amount", + "year_to_date", "section_break_5", "additional_salary", "statistical_component", @@ -226,11 +227,19 @@ { "fieldname": "column_break_24", "fieldtype": "Column Break" + }, + { + "description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-11-25 13:12:41.081106", + "modified": "2021-01-14 13:39:15.847158", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 51fb3596e9..b50c774fbe 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -138,11 +138,11 @@ frappe.ui.form.on("Salary Slip", { }, change_grid_labels: function(frm) { - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "earnings"); + let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"]; - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "deductions"); + frm.set_currency_labels(fields, frm.doc.currency, "earnings"); + frm.set_currency_labels(fields, frm.doc.currency, "deductions"); }, refresh: function(frm) { diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 43deee43aa..9f9691b59d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -584,6 +584,7 @@ "fieldtype": "Column Break" }, { + "description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", "fieldname": "year_to_date", "fieldtype": "Currency", "label": "Year To Date", @@ -591,6 +592,7 @@ "read_only": 1 }, { + "description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.", "fieldname": "month_to_date", "fieldtype": "Currency", "label": "Month To Date", @@ -616,7 +618,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-21 23:43:44.959840", + "modified": "2021-01-14 13:37:38.180920", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 183ad13411..2d3bc57900 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -52,6 +52,7 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() self.compute_year_to_date() self.compute_month_to_date() + self.compute_component_wise_year_to_date() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") @@ -1138,16 +1139,7 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) - - if payroll_period: - period_start_date = payroll_period.start_date - period_end_date = payroll_period.end_date - else: - # get dates based on fiscal year if no payroll period exists - fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) - period_start_date = fiscal_year.year_start_date - period_end_date = fiscal_year.year_end_date + period_start_date, period_end_date = self.get_year_to_date_period() salary_slip_sum = frappe.get_list('Salary Slip', fields = ['sum(net_pay) as sum'], @@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase): month_to_date += self.net_pay self.month_to_date = month_to_date + def compute_component_wise_year_to_date(self): + period_start_date, period_end_date = self.get_year_to_date_period() + + for key in ('earnings', 'deductions'): + for component in self.get(key): + year_to_date = 0 + component_sum = frappe.db.sql(""" + SELECT sum(detail.amount) as sum + FROM `tabSalary Detail` as detail + INNER JOIN `tabSalary Slip` as salary_slip + ON detail.parent = salary_slip.name + WHERE + salary_slip.employee_name = %(employee_name)s + AND detail.salary_component = %(component)s + AND salary_slip.start_date >= %(period_start_date)s + AND salary_slip.end_date < %(period_end_date)s + AND salary_slip.name != %(docname)s + AND salary_slip.docstatus = 1""", + {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date, + 'period_end_date': period_end_date, 'docname': self.name} + ) + + year_to_date = flt(component_sum[0][0]) if component_sum else 0.0 + year_to_date += component.amount + component.year_to_date = year_to_date + + def get_year_to_date_period(self): + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) + + if payroll_period: + period_start_date = payroll_period.start_date + period_end_date = payroll_period.end_date + else: + # get dates based on fiscal year if no payroll period exists + fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) + period_start_date = fiscal_year.year_start_date + period_end_date = fiscal_year.year_end_date + + return period_start_date, period_end_date + + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4368c03c2a..f58a8e58c2 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -321,6 +321,38 @@ class TestSalarySlip(unittest.TestCase): year_to_date += flt(slip.net_pay) self.assertEqual(slip.year_to_date, year_to_date) + def test_component_wise_year_to_date_computation(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + applicant = make_employee("test_ytd@salary.com", company="_Test Company") + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), + company="_Test Company") + + salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + + # clear salary slip for this employee + frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") + + create_salary_slips_for_payroll_period(applicant, salary_structure.name, + payroll_period, deduct_random=False, num=3) + + salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": + "test_ytd@salary.com"}, order_by = "posting_date") + + year_to_date = dict() + for slip in salary_slips: + doc = frappe.get_doc("Salary Slip", slip.name) + for entry in doc.get("earnings"): + if not year_to_date.get(entry.salary_component): + year_to_date[entry.salary_component] = 0 + + year_to_date[entry.salary_component] += entry.amount + self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -714,10 +746,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = else: return income_tax_slab_name -def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): +def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12): deducted_dates = [] i = 0 - while i < 12: + while i < num: slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, "salary_structure": salary_structure, "frequency": "Monthly"}) if i == 0: diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ba824c5d6f..6c7b382264 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -70,6 +70,9 @@ frappe.ui.form.on('Salary Structure', { }); }, + company: function(frm) { + frm.trigger('set_earning_deduction_component'); + }, currency: function(frm) { calculate_totals(frm.doc); @@ -117,6 +120,7 @@ frappe.ui.form.on('Salary Structure', { fields_read_only.forEach(function(field) { frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; }); + frm.trigger('set_earning_deduction_component'); }, assign_to_employees:function (frm) { diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 77914bb531..e71803172c 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -216,8 +216,13 @@ def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, return frappe.db.sql(""" select t1.salary_component from `tabSalary Component` t1, `tabSalary Component Account` t2 - where t1.salary_component = t2.parent - and t1.type = %s - and t2.company = %s + where (t1.name = t2.parent + and t1.type = %(type)s + and t2.company = %(company)s) + or (t1.type = %(type)s + and t1.statistical_component = 1) order by salary_component - """, (filters['type'], filters['company']) ) + """,{ + "type": filters['type'], + "company": filters['company'] + }) diff --git a/erpnext/payroll/print_format/__init__.py b/erpnext/payroll/print_format/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py b/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json new file mode 100644 index 0000000000..71ba37f6ed --- /dev/null +++ b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json @@ -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\": \"

{{doc.name}}

\\n
\\n
\\n
\"}, {\"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" +} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index d0cac90e4d..eb210be16a 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -161,9 +161,9 @@ def get_item_list(invoice): item.qty = abs(item.qty) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_amount / item.qty) - item.gross_amount = abs(item.base_amount) - item.taxable_value = abs(item.base_amount) + item.unit_rate = abs(item.base_net_amount / item.qty) + item.gross_amount = abs(item.base_net_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 = 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: item_tax_rate = item_tax_detail[0] # 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: 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): 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.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) @@ -244,9 +250,9 @@ def update_invoice_taxes(invoice, invoice_value_details): for tax_type in ['igst', 'cgst', 'sgst']: 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: - 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 @@ -473,7 +479,7 @@ class GSPConnector(): "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, "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() def fetch_auth_token(self): @@ -486,7 +492,8 @@ class GSPConnector(): 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.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: self.log_error(res) @@ -757,7 +764,7 @@ class GSPConnector(): 'label': _('IRN Generated') } self.update_invoice() - + def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype @@ -768,7 +775,7 @@ class GSPConnector(): 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), 'attached_to_doctype': doctype, 'attached_to_name': docname, - 'content': 'qrcode', + 'content': str(base64.b64encode(os.urandom(64))), 'is_private': 1 }) _file.insert() diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 8aaf7abcbe..ff603fcfb3 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -233,7 +233,8 @@ def get_stock_ledger_entries(filters): from `tabItem` {item_conditions}) item where item_code = item.name and company = %(company)s and - posting_date <= %(to_date)s + posting_date <= %(to_date)s and + is_cancelled != 1 {sle_conditions} order by posting_date, posting_time, sle.creation, actual_qty""" #nosec .format(item_conditions=get_item_conditions(filters),