From 26c9b94cc6af7aced485321a6d778605215a78a1 Mon Sep 17 00:00:00 2001 From: Zarrar Date: Mon, 16 Jul 2018 18:11:53 +0530 Subject: [PATCH] [ Improv ] Loyalty Program more fixes and test case (#14888) * remove console statements * set account & cost center for redemption * add test case for single/multi tier & is_return scenario * reset tier when changing loyalty program * make loyalty fields non copy type * fix test case - delete si after every test to avoid interference --- .../loyalty_program/test_loyalty_program.py | 258 +++++++++++++++++- .../doctype/sales_invoice/sales_invoice.js | 2 - .../doctype/sales_invoice/sales_invoice.json | 18 +- .../doctype/sales_invoice/sales_invoice.py | 8 +- erpnext/selling/doctype/customer/customer.js | 6 + .../selling/doctype/customer/customer.json | 19 +- erpnext/selling/doctype/customer/customer.py | 6 + 7 files changed, 297 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py index a5cf765599..87a0cb8c43 100644 --- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py @@ -5,6 +5,262 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import today, cint, flt, getdate +from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details class TestLoyaltyProgram(unittest.TestCase): - pass + @classmethod + def setUpClass(self): + # create relevant item, customer, loyalty program, etc + create_records() + + def test_loyalty_points_earned_single_tier(self): + # create a new sales invoice + si_original = create_sales_invoice_record() + si_original.insert() + si_original.submit() + + customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) + earned_points = get_points_earned(si_original) + + lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) + + self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) + self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) + self.assertEqual(lpe.loyalty_points, earned_points) + + # add redemption point + si_redeem = create_sales_invoice_record() + si_redeem.redeem_loyalty_points = 1 + si_redeem.loyalty_points = earned_points + si_redeem.insert() + si_redeem.submit() + + earned_after_redemption = get_points_earned(si_redeem) + + lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name}) + lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) + + self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) + self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) + + # cancel and delete + for d in [si_redeem, si_original]: + d.cancel() + frappe.delete_doc('Sales Invoice', d.name) + + def test_loyalty_points_earned_multiple_tier(self): + # assign multiple tier program to the customer + customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) + customer.loyalty_program = frappe.get_doc('Loyalty Program', {'loyalty_program_name': 'Test Multiple Loyalty'}).name + customer.save() + + # create a new sales invoice + si_original = create_sales_invoice_record() + si_original.insert() + si_original.submit() + + earned_points = get_points_earned(si_original) + + lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) + + self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) + self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) + self.assertEqual(lpe.loyalty_points, earned_points) + + # add redemption point + si_redeem = create_sales_invoice_record() + si_redeem.redeem_loyalty_points = 1 + si_redeem.loyalty_points = earned_points + si_redeem.insert() + si_redeem.submit() + + customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) + earned_after_redemption = get_points_earned(si_redeem) + + lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name}) + lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) + + self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) + self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) + self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier) + + # cancel and delete + for d in [si_redeem, si_original]: + d.cancel() + frappe.delete_doc('Sales Invoice', d.name) + + def test_cancel_sales_invoice(self): + ''' cancelling the sales invoice should cancel the earned points''' + # create a new sales invoice + si = create_sales_invoice_record() + si.insert() + si.submit() + + lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si.name, 'customer': si.customer}) + self.assertEqual(True, not (lpe is None)) + + # cancelling sales invoice + si.cancel() + lpe = frappe.db.exists('Loyalty Point Entry', lpe.name) + self.assertEqual(True, (lpe is None)) + + def test_sales_invoice_return(self): + # create a new sales invoice + si_original = create_sales_invoice_record(2) + si_original.conversion_rate = flt(1) + si_original.insert() + si_original.submit() + + earned_points = get_points_earned(si_original) + lpe_original = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) + self.assertEqual(lpe_original.loyalty_points, earned_points) + + # create sales invoice return + si_return = create_sales_invoice_record(-1) + si_return.conversion_rate = flt(1) + si_return.is_return = 1 + si_return.return_against = si_original.name + si_return.insert() + si_return.submit() + + # fetch original invoice again as its status would have been updated + si_original = frappe.get_doc('Sales Invoice', lpe_original.sales_invoice) + + earned_points = get_points_earned(si_original) + lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) + self.assertEqual(lpe_after_return.loyalty_points, earned_points) + self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points)) + + # cancel and delete + for d in [si_return, si_original]: + try: + d.cancel() + except frappe.TimestampMismatchError: + frappe.get_doc('Sales Invoice', d.name).cancel() + frappe.delete_doc('Sales Invoice', d.name) + +def get_points_earned(self): + def get_returned_amount(): + returned_amount = frappe.db.sql(""" + select sum(grand_total) + from `tabSales Invoice` + where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s + """, self.name) + return abs(flt(returned_amount[0][0])) if returned_amount else 0 + + lp_details = get_loyalty_program_details(self.customer, company=self.company, + loyalty_program=self.loyalty_program, expiry_date=self.posting_date) + if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \ + (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)): + returned_amount = get_returned_amount() + eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount + points_earned = cint(eligible_amount/lp_details.collection_factor) + + return points_earned or 0 + +def create_sales_invoice_record(qty=1): + # return sales invoice doc object + return frappe.get_doc({ + "doctype": "Sales Invoice", + "customer": frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}).name, + "company": '_Test Company', + "due_date": today(), + "posting_date": today(), + "taxes_and_charges": "", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [{ + 'doctype': 'Sales Invoice Item', + 'item_code': frappe.get_doc('Item', {'item_name': 'Loyal Item'}).name, + 'qty': qty, + 'income_account': 'Sales - _TC', + 'cost_center': 'Main - _TC', + 'expense_account': 'Cost of Goods Sold - _TC' + }] + }) + +def create_records(): + # create a new loyalty Account + frappe.get_doc({ + "doctype": "Account", + "account_name": "Loyalty", + "parent_account": "Direct Expenses - _TC", + "company": "_Test Company", + "is_group": 0, + "account_type": "Expense Account", + }).insert() + + # create a new loyalty program Single tier + frappe.get_doc({ + "doctype": "Loyalty Program", + "loyalty_program_name": "Test Single Loyalty", + "auto_opt_in": 1, + "from_date": today(), + "loyalty_program_type": "Single Tier Program", + "conversion_factor": 1, + "expiry_duration": 10, + "company": "_Test Company", + "cost_center": "Main - _TC", + "expense_account": "Loyalty - _TC", + "collection_rules": [{ + 'tier_name': 'Silver', + 'collection_factor': 1000, + 'min_spent': 1000 + }] + }).insert() + + # create a new customer + frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": "Test Loyalty Customer", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory" + }).insert() + + # create a new loyalty program Multiple tier + frappe.get_doc({ + "doctype": "Loyalty Program", + "loyalty_program_name": "Test Multiple Loyalty", + "auto_opt_in": 1, + "from_date": today(), + "loyalty_program_type": "Multiple Tier Program", + "conversion_factor": 1, + "expiry_duration": 10, + "company": "_Test Company", + "cost_center": "Main - _TC", + "expense_account": "Loyalty - _TC", + "collection_rules": [ + { + 'tier_name': 'Silver', + 'collection_factor': 1000, + 'min_spent': 10000 + }, + { + 'tier_name': 'Gold', + 'collection_factor': 1000, + 'min_spent': 19000 + } + ] + }).insert() + + # create an item + item = frappe.get_doc({ + "doctype": "Item", + "item_code": "Loyal Item", + "item_name": "Loyal Item", + "item_group": "All Item Groups", + "company": "_Test Company", + "is_stock_item": 1, + "opening_stock": 100, + "valuation_rate": 10000, + }).insert() + + # create item price + frappe.get_doc({ + "doctype": "Item Price", + "price_list": "Standard Selling", + "item_code": item.item_code, + "price_list_rate": 10000 + }).insert() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d3efb7021c..ca6b47b84f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -36,7 +36,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }, refresh: function(doc, dt, dn) { - console.log("triggered the SalesInvoiceController"); this._super(); if(cur_frm.msgbox && cur_frm.msgbox.$wrapper.is(":visible")) { // hide new msgbox @@ -389,7 +388,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }, loyalty_amount: function(){ - console.log("triggered the loyalty amount"); this.calculate_outstanding_amount(); this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 7a9e7d0084..7f1a660be3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2476,7 +2476,7 @@ "in_standard_filter": 0, "label": "Loyalty Points", "length": 0, - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "precision": "", "print_hide": 1, @@ -2509,7 +2509,7 @@ "in_standard_filter": 0, "label": "Loyalty Amount", "length": 0, - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "precision": "", "print_hide": 1, @@ -2541,7 +2541,7 @@ "in_standard_filter": 0, "label": "Redeem Loyalty Points", "length": 0, - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "precision": "", "print_hide": 1, @@ -2606,7 +2606,7 @@ "in_standard_filter": 0, "label": "Loyalty Program", "length": 0, - "no_copy": 0, + "no_copy": 1, "options": "Loyalty Program", "permlevel": 0, "precision": "", @@ -2640,7 +2640,7 @@ "in_standard_filter": 0, "label": "Redemption Account", "length": 0, - "no_copy": 0, + "no_copy": 1, "options": "Account", "permlevel": 0, "precision": "", @@ -2674,7 +2674,7 @@ "in_standard_filter": 0, "label": "Redemption Cost Center", "length": 0, - "no_copy": 0, + "no_copy": 1, "options": "Cost Center", "permlevel": 0, "precision": "", @@ -2735,8 +2735,8 @@ "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, + "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Apply Additional Discount On", @@ -5382,7 +5382,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-07-10 19:28:04.351108", + "modified": "2018-07-12 13:16:20.918322", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f6459837ad..0f2a4d5046 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -95,6 +95,10 @@ class SalesInvoice(SellingController): if self._action != 'submit' and self.update_stock and not self.is_return: set_batch_nos(self, 'warehouse', True) + if self.redeem_loyalty_points: + lp = frappe.get_doc('Loyalty Program', self.loyalty_program) + self.loyalty_redemption_account = lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account + self.loyalty_redemption_cost_center = lp.cost_center if not self.loyalty_redemption_cost_center else self.loyalty_redemption_cost_center self.set_against_income_account() self.validate_c_form() @@ -1015,7 +1019,7 @@ class SalesInvoice(SellingController): from `tabSales Invoice` where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s """, self.name) - return flt(returned_amount[0][0]) if returned_amount else 0 + return abs(flt(returned_amount[0][0])) if returned_amount else 0 # redeem the loyalty points. def apply_loyalty_points(self): @@ -1026,7 +1030,7 @@ class SalesInvoice(SellingController): points_to_redeem = self.loyalty_points for lp_entry in loyalty_point_entries: - available_points = lp_entry.loyalty_points - redemption_details.get(lp_entry.name) + available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name)) if available_points > points_to_redeem: redeemed_points = points_to_redeem else: diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 5499ab0791..fe3bedfe38 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -77,6 +77,12 @@ frappe.ui.form.on("Customer", { } }, + loyalty_program: function(frm) { + if(frm.doc.loyalty_program) { + frm.set_value('loyalty_program_tier', null); + } + }, + refresh: function(frm) { if(frappe.defaults.get_default("cust_master_name")!="Naming Series") { frm.toggle_display("naming_series", false); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 5215854522..b0a63df5c3 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -1493,10 +1493,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1512,7 +1514,7 @@ "in_standard_filter": 0, "label": "Loyalty Program", "length": 0, - "no_copy": 0, + "no_copy": 1, "options": "Loyalty Program", "permlevel": 0, "precision": "", @@ -1524,16 +1526,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "fieldname": "loyalty_program_tier", - "fieldtype": "Read Only", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -1543,21 +1547,23 @@ "in_standard_filter": 0, "label": "Loyalty Program Tier", "length": 0, - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 1, @@ -1768,7 +1774,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-06-27 12:12:30.677834", + "modified": "2018-07-12 13:20:13.203294", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -1955,5 +1961,6 @@ "sort_order": "ASC", "title_field": "customer_name", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 595180b6a3..2c225432bd 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -62,6 +62,12 @@ class Customer(TransactionBase): self.set_loyalty_program() self.check_customer_group_change() + # set loyalty program tier + if frappe.db.exists('Customer', self.name): + customer = frappe.get_doc('Customer', self.name) + if self.loyalty_program == customer.loyalty_program and not self.loyalty_program_tier: + self.loyalty_program_tier = customer.loyalty_program_tier + def check_customer_group_change(self): frappe.flags.customer_group_changed = False