Merge pull request #3908 from rmehta/text-editor

[fix] changed text > text editor for description fields in tables
diff --git a/erpnext/__version__.py b/erpnext/__version__.py
index 99604dc..90d02c9 100644
--- a/erpnext/__version__.py
+++ b/erpnext/__version__.py
@@ -1,2 +1,2 @@
 from __future__ import unicode_literals
-__version__ = '5.6.4'
+__version__ = '5.7.0'
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index d12b2d7..0b74948 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -232,45 +232,24 @@
 		test_recurring_document(self, test_records)
 
 	def test_total_purchase_cost_for_project(self):
-		purchase_invoice = frappe.new_doc('Purchase Invoice')
-		purchase_invoice.update({
-			"credit_to": "_Test Payable - _TC",
-			"supplier": "_Test Supplier",
-			"company": "_Test Company",
-			"items": [
-				{
-					"rate": 500,
-					"qty": 1,
-					"project_name": "_Test Project",
-					"item_code": "_Test Item Home Desktop 100",
-					"expense_account": "_Test Account Cost for Goods Sold - _TC",
-					"cost_center": "_Test Cost Center - _TC"
-				},
-				{
-					"rate": 1500,
-					"qty": 1,
-					"project_name": "_Test Project",
-					"item_code": "_Test Item Home Desktop 200",
-					"expense_account": "_Test Account Cost for Goods Sold - _TC",
-					"cost_center": "_Test Cost Center - _TC"
-				}
-			]
-		})
-		purchase_invoice.save()
-		purchase_invoice.submit()
-		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 2000)
+		existing_purchase_cost = frappe.db.sql("""select sum(ifnull(base_net_amount, 0))
+			from `tabPurchase Invoice Item` where project_name = '_Test Project' and docstatus=1""")
+		existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
+		
+		pi = make_purchase_invoice(currency="USD", conversion_rate=60, project_name="_Test Project")
+		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 
+			existing_purchase_cost + 15000)
 
-		purchase_invoice1 = frappe.copy_doc(purchase_invoice)
-		purchase_invoice1.save()
-		purchase_invoice1.submit()
+		pi1 = make_purchase_invoice(qty=10, project_name="_Test Project")
+		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 
+			existing_purchase_cost + 15500)
 
-		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 4000)
+		pi1.cancel()
+		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 
+			existing_purchase_cost + 15000)
 
-		purchase_invoice1.cancel()
-		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 2000)
-
-		purchase_invoice.cancel()
-		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), 0)
+		pi.cancel()
+		self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
 
 	def test_return_purchase_invoice(self):
 		set_perpetual_inventory()
@@ -308,6 +287,7 @@
 	pi.company = args.company or "_Test Company"
 	pi.supplier = args.supplier or "_Test Supplier"
 	pi.currency = args.currency or "INR"
+	pi.conversion_rate = args.conversion_rate or 1
 	pi.is_return = args.is_return
 	pi.return_against = args.return_against
 
@@ -318,7 +298,9 @@
 		"rate": args.rate or 50,
 		"conversion_factor": 1.0,
 		"serial_no": args.serial_no,
-		"stock_uom": "_Test UOM"
+		"stock_uom": "_Test UOM",
+		"cost_center": "_Test Cost Center - _TC",
+		"project_name": args.project_name
 	})
 	if not args.do_not_save:
 		pi.insert()
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index dd42f45..734684f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -83,7 +83,7 @@
 				cur_frm.meta.default_print_format = cur_frm.pos_print_format;
 			}
 		} else {
-			if(cur_frm.meta.cur_frm.meta._default_print_format) {
+			if(cur_frm.meta._default_print_format) {
 				cur_frm.meta.default_print_format = cur_frm.meta._default_print_format;
 				cur_frm.meta._default_print_format = null;
 			}
diff --git a/erpnext/change_log/current/item_variants.md b/erpnext/change_log/current/item_variants.md
deleted file mode 100644
index 268b595..0000000
--- a/erpnext/change_log/current/item_variants.md
+++ /dev/null
@@ -1,6 +0,0 @@
-- Make Variants added to Item Master
-- Added ability to specify Range of Numeric Values in Attribute Master 
-- Fixed an issue in filtering Templates in Item List View
-- Added Patch to fetch Item Template Attributes
-- Allow Editing variant attribute in Variant. Add validation to check duplication.
-- Removed Manage Variants
\ No newline at end of file
diff --git a/erpnext/change_log/current/pos.md b/erpnext/change_log/current/pos.md
deleted file mode 100644
index 9faf82d..0000000
--- a/erpnext/change_log/current/pos.md
+++ /dev/null
@@ -1,2 +0,0 @@
-- Set default Print Format for Point-of-Sale (POS) via **POS Profile**
-- Option to automatically print after submitting POS **Sales Invoice**
diff --git a/erpnext/change_log/v5/v5_7_0.md b/erpnext/change_log/v5/v5_7_0.md
new file mode 100644
index 0000000..c76eb60
--- /dev/null
+++ b/erpnext/change_log/v5/v5_7_0.md
@@ -0,0 +1,7 @@
+- **Item Variants**
+	- Removed **Manage Variants** tool
+	- Use **Make Variant** button in the Item form to create new variants based on a template Item
+	- Added ability to specify Range of Numeric Values in Item Attribute	
+- Set default Print Format for Point-of-Sale (POS) via **POS Profile**
+- Option to automatically print after submitting POS **Sales Invoice**
+- **Communication** added to CRM module
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 1609b87..0fdbda1 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -27,7 +27,7 @@
 """
 app_icon = "icon-th"
 app_color = "#e74c3c"
-app_version = "5.6.4"
+app_version = "5.7.0"
 github_link = "https://github.com/frappe/erpnext"
 
 error_report_email = "support@erpnext.com"
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 87a5551..551518e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -192,3 +192,4 @@
 execute:frappe.db.sql("update `tabProduction Order` pro set description = (select description from tabItem where name=pro.production_item) where ifnull(description, '') = ''")
 erpnext.patches.v5_7.item_template_attributes
 erpnext.patches.v4_2.repost_reserved_qty #2015-08-17
+erpnext.patches.v5_4.update_purchase_cost_against_project
diff --git a/erpnext/patches/v5_4/update_purchase_cost_against_project.py b/erpnext/patches/v5_4/update_purchase_cost_against_project.py
new file mode 100644
index 0000000..3a02eb7
--- /dev/null
+++ b/erpnext/patches/v5_4/update_purchase_cost_against_project.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+	for p in frappe.get_all("Project"):
+		purchase_cost = frappe.db.sql("""select sum(ifnull(base_net_amount, 0))
+			from `tabPurchase Invoice Item` where project_name = %s and docstatus=1""", p.name)
+		purchase_cost = purchase_cost and purchase_cost[0][0] or 0
+		
+		frappe.db.set_value("Project", p.name, "total_purchase_cost", purchase_cost)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 6ebafdb..de1c3f8 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -25,13 +25,13 @@
 					"description": task.description,
 					"task_id": task.name
 				})
-			
+
 	def __setup__(self):
 		self.onload()
-	
+
 	def get_tasks(self):
 		return frappe.get_all("Task", "*", {"project": self.name}, order_by="exp_start_date asc")
-		
+
 	def validate(self):
 		self.validate_dates()
 		self.sync_tasks()
@@ -74,7 +74,7 @@
 		for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}):
 			frappe.delete_doc("Task", t.name)
 			task_added_or_deleted = True
-			
+
 		if task_added_or_deleted:
 			self.update_project()
 
@@ -87,12 +87,12 @@
 		if total:
 			completed = frappe.db.sql("""select count(*) from tabTask where
 				project=%s and status in ('Closed', 'Cancelled')""", self.name)[0][0]
-				
+
 			self.percent_complete = flt(completed) / total * 100
 
 	def update_costing(self):
-		total_cost = frappe.db.sql("""select sum(total_costing_amount) as costing_amount,
-			sum(total_billing_amount) as billing_amount, sum(total_expense_claim) as expense_claim,
+		total_cost = frappe.db.sql("""select sum(ifnull(total_costing_amount, 0)) as costing_amount,
+			sum(ifnull(total_billing_amount, 0)) as billing_amount, sum(ifnull(total_expense_claim, 0)) as expense_claim,
 			min(act_start_date) as start_date, max(act_end_date) as end_date, sum(actual_time) as time
 			from `tabTask` where project = %s""", self.name, as_dict=1)[0]
 
@@ -105,10 +105,12 @@
 		self.gross_margin = flt(total_cost.billing_amount) - flt(total_cost.costing_amount)
 		if self.total_billing_amount:
 			self.per_gross_margin = (self.gross_margin / flt(self.total_billing_amount)) *100
-			
+
 	def update_purchase_costing(self):
-		self.total_purchase_cost = frappe.db.sql("""select sum(amount) as cost
-			from `tabPurchase Invoice Item` where project_name = %s and docstatus=1 """, self.name, as_dict=1)[0].cost or 0
+		total_purchase_cost = frappe.db.sql("""select sum(ifnull(base_net_amount, 0))
+			from `tabPurchase Invoice Item` where project_name = %s and docstatus=1""", self.name)
+
+		self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
 
 @frappe.whitelist()
 def get_cost_center_name(project_name):
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 20a064b..cd3d295 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -14,6 +14,7 @@
 class WarehouseNotSet(frappe.ValidationError): pass
 class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
 class ItemVariantExistsError(frappe.ValidationError): pass
+class InvalidItemAttributeValueError(frappe.ValidationError): pass
 
 class Item(WebsiteGenerator):
 	website = frappe._dict(
@@ -351,9 +352,11 @@
 					frappe.throw(_("Please specify Attribute Value for attribute {0}").format(d.attribute))
 				args[d.attribute] = d.attribute_value
 
-			variant = get_variant(self.variant_of, args)
-			if variant and variant != self.name:
-				frappe.throw(_("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError)
+			if self.get("__islocal"):
+				# test this during insert because naming is based on item_code and we cannot use condition like self.name != variant
+				variant = get_variant(self.variant_of, args)
+				if variant:
+					frappe.throw(_("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError)
 
 def validate_end_of_life(item_code, end_of_life=None, verbose=1):
 	if not end_of_life:
@@ -510,8 +513,8 @@
 		filters={"parent": ["in", args.keys()]}):
 		(attribute_values.setdefault(t.parent, [])).append(t.attribute_value)
 
-	numeric_attributes = [t.name for t in frappe.get_list("Item Attribute", filters={"numeric_values":1,
-		"parent": ["in", args.keys()]})]
+	numeric_attributes = frappe._dict((t.name, t) for t in frappe.get_list("Item Attribute", filters={"numeric_values":1,
+		"name": ["in", args.keys()]}, fields=["name", "from_range", "to_range", "increment"]))
 
 	template_item = frappe.get_doc("Item", item)
 	template_item_attributes = frappe._dict((d.attribute, d) for d in template_item.attributes)
@@ -519,18 +522,19 @@
 	for attribute, value in args.items():
 
 		if attribute in numeric_attributes:
-			template_attribute = template_item_attributes[attribute]
+			numeric_attribute = numeric_attributes[attribute]
 
-			if template_attribute.increment == 0:
+			from_range = numeric_attribute.from_range
+			to_range = numeric_attribute.to_range
+			increment = numeric_attribute.increment
+
+			if increment == 0:
 				# defensive validation to prevent ZeroDivisionError
 				frappe.throw(_("Increment for Attribute {0} cannot be 0").format(attribute))
 
-			from_range = template_attribute.from_range
-			to_range = template_attribute.to_range
-			increment = template_attribute.increment
 
 			if not ( (from_range <= flt(value) <= to_range) and (flt(value) - from_range) % increment == 0 ):
-				frappe.throw(_("Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3}").format(attribute, from_range, to_range, increment))
+				frappe.throw(_("Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3}").format(attribute, from_range, to_range, increment), InvalidItemAttributeValueError)
 
 		elif value not in attribute_values[attribute]:
 			frappe.throw(_("Value {0} for Attribute {1} does not exist in the list of valid Item Attribute Values").format(
@@ -538,7 +542,7 @@
 
 def find_variant(item, args):
 	conditions = ["""(iv_attribute.attribute="{0}" and iv_attribute.attribute_value="{1}")"""\
-		.format(frappe.db.escape(key), frappe.db.escape(value)) for key, value in args.items()]
+		.format(frappe.db.escape(key), frappe.db.escape(cstr(value))) for key, value in args.items()]
 
 	conditions = " or ".join(conditions)
 
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 428bd37..2ba9647 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,7 +6,8 @@
 import frappe
 
 from frappe.test_runner import make_test_records
-from erpnext.stock.doctype.item.item import WarehouseNotSet, ItemTemplateCannotHaveStock, create_variant, ItemVariantExistsError
+from erpnext.stock.doctype.item.item import (WarehouseNotSet, ItemTemplateCannotHaveStock, create_variant,
+	ItemVariantExistsError, InvalidItemAttributeValueError)
 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
 
 test_ignore = ["BOM"]
@@ -100,21 +101,52 @@
 			self.assertEquals(value, details.get(key))
 
 	def test_make_item_variant(self):
-		if frappe.db.exists("Item", "_Test Variant Item-L"):
-			frappe.delete_doc("Item", "_Test Variant Item-L")
-			frappe.delete_doc("Item", "_Test Variant Item-L2")
+		frappe.delete_doc_if_exists("Item", "_Test Variant Item-L")
 
-		variant = create_variant("_Test Variant Item", """{"Test Size": "Large"}""")
-		variant.item_code = "_Test Variant Item-L"
-		variant.item_name = "_Test Variant Item-L"
+		variant = create_variant("_Test Variant Item", {"Test Size": "Large"})
 		variant.save()
 
 		# doing it again should raise error
-		variant = create_variant("_Test Variant Item", """{"Test Size": "Large"}""")
-		variant.item_code = "_Test Variant Item-L2"
-		variant.item_name = "_Test Variant Item-L2"
+		variant = create_variant("_Test Variant Item", {"Test Size": "Large"})
 		self.assertRaises(ItemVariantExistsError, variant.save)
 
+	def test_make_item_variant_with_numeric_values(self):
+		# cleanup
+		frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item")
+		frappe.delete_doc_if_exists("Item", "_Test Numeric Variant-L-1.5")
+		frappe.delete_doc_if_exists("Item Attribute", "Test Item Length")
+
+		# make item attribute
+		frappe.get_doc({
+			"doctype": "Item Attribute",
+			"attribute_name": "Test Item Length",
+			"numeric_values": 1,
+			"from_range": 0.0,
+			"to_range": 100.0,
+			"increment": 0.5
+		}).insert()
+
+		# make template item
+		make_item("_Test Numeric Template Item", {
+			"attributes": [
+				{"attribute": "Test Size"},
+				{"attribute": "Test Item Length"}
+			],
+			"default_warehouse": "_Test Warehouse - _TC"
+		})
+
+		variant = create_variant("_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.1})
+		self.assertEquals(variant.item_code, None)
+		variant.item_code = "_Test Numeric Variant-L-1.1"
+		variant.item_name = "_Test Numeric Variant Large 1.1m"
+		self.assertRaises(InvalidItemAttributeValueError, variant.save)
+
+		variant = create_variant("_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5})
+		self.assertEquals(variant.item_code, None)
+		variant.item_code = "_Test Numeric Variant-L-1.5"
+		variant.item_name = "_Test Numeric Variant Large 1.5m"
+		variant.save()
+
 def make_item_variant():
 	if not frappe.db.exists("Item", "_Test Variant Item-S"):
 		variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""")
diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py
index a82a3a8..8310288 100644
--- a/erpnext/stock/doctype/item_attribute/item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/item_attribute.py
@@ -6,6 +6,8 @@
 from frappe.model.document import Document
 from frappe import _
 
+class ItemAttributeIncrementError(frappe.ValidationError): pass
+
 class ItemAttribute(Document):
 	def validate(self):
 		self.validate_numeric()
@@ -15,14 +17,14 @@
 	def validate_numeric(self):
 		if self.numeric_values:
 			self.set("item_attribute_values", [])
-			if not self.from_range or not self.to_range:
+			if self.from_range is None or self.to_range is None:
 				frappe.throw(_("Please specify from/to range"))
 
-			elif self.from_range > self.to_range:
-				frappe.throw(_("From Range cannot be greater than to Range"))
+			elif self.from_range >= self.to_range:
+				frappe.throw(_("From Range has to be less than To Range"))
 
 			if not self.increment:
-				frappe.throw(_("Increment cannot be 0"))
+				frappe.throw(_("Increment cannot be 0"), ItemAttributeIncrementError)
 		else:
 			self.from_range = self.to_range = self.increment = 0
 
diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
index 31b3b0a..6357b52 100644
--- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
@@ -6,5 +6,25 @@
 
 test_records = frappe.get_test_records('Item Attribute')
 
+from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
+
 class TestItemAttribute(unittest.TestCase):
-	pass
+	def setUp(self):
+		if frappe.db.exists("Item Attribute", "_Test_Length"):
+			frappe.delete_doc("Item Attribute", "_Test_Length")
+
+	def test_numeric_item_attribute(self):
+		item_attribute = frappe.get_doc({
+			"doctype": "Item Attribute",
+			"attribute_name": "_Test_Length",
+			"numeric_values": 1,
+			"from_range": 0.0,
+			"to_range": 100.0,
+			"increment": 0
+		})
+
+		self.assertRaises(ItemAttributeIncrementError, item_attribute.save)
+
+		item_attribute.increment = 0.5
+		item_attribute.save()
+
diff --git a/setup.py b/setup.py
index c624e0e..5c92ec0 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 from setuptools import setup, find_packages
 
-version = "5.6.4"
+version = "5.7.0"
 
 with open("requirements.txt", "r") as f:
 	install_requires = f.readlines()