[ 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
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index a5cf765..87a0cb8 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 @@
 
 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 d3efb70..ca6b47b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -36,7 +36,6 @@
 	},
 
 	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 @@
 	},
 
 	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 7a9e7d0..7f1a660 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 f645983..0f2a4d5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -95,6 +95,10 @@
 		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 @@
 			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 @@
 
 		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 5499ab0..fe3bedf 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -77,6 +77,12 @@
 		}
 	},
 
+	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 5215854..b0a63df 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 595180b..2c22543 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -62,6 +62,12 @@
 		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