Merge pull request #15376 from jodeq/show_project-attachments-in-portal

[Proposal] Show project attachments in portal view
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 5fa0707..f3e85b2 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -123,7 +123,7 @@
 			"reference_doctype": "Payment Request",
 			"reference_docname": self.name,
 			"payer_email": self.email_to or frappe.session.user,
-			"payer_name": data.customer_name,
+			"payer_name": frappe.safe_decode(data.customer_name),
 			"order_id": self.name,
 			"currency": self.currency
 		})
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index 39ad0d4..aa29907 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -440,6 +440,38 @@
   {
    "allow_bulk_edit": 0, 
    "allow_in_quick_entry": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "generate_invoice_at_period_start", 
+   "fieldtype": "Check", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Generate Invoice At Beginning Of Period", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "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": 1, 
    "bold": 0, 
    "collapsible": 0, 
@@ -814,7 +846,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2018-08-21 16:15:44.533482", 
+ "modified": "2018-10-04 10:29:03.338893", 
  "modified_by": "Administrator", 
  "module": "Accounts", 
  "name": "Subscription", 
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index fe39161..7fb6b7a 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -321,6 +321,23 @@
 
 		self.save()
 
+	@property
+	def is_postpaid_to_invoice(self):
+		return getdate(nowdate()) > getdate(self.current_invoice_end) or \
+			(getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \
+			not self.has_outstanding_invoice()
+
+	@property
+	def is_prepaid_to_invoice(self):
+		if not self.generate_invoice_at_period_start:
+			return False
+
+		if self.is_new_subscription():
+			return True
+
+		# Check invoice dates and make sure it doesn't have outstanding invoices
+		return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice()
+
 	def process_for_active(self):
 		"""
 		Called by `process` if the status of the `Subscription` is 'Active'.
@@ -330,7 +347,7 @@
 		2. Change the `Subscription` status to 'Past Due Date'
 		3. Change the `Subscription` status to 'Cancelled'
 		"""
-		if getdate(nowdate()) > getdate(self.current_invoice_end) or (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and not self.has_outstanding_invoice():
+		if self.is_postpaid_to_invoice or self.is_prepaid_to_invoice:
 			self.generate_invoice()
 			if self.current_invoice_is_past_due():
 				self.status = 'Past Due Date'
@@ -338,7 +355,7 @@
 		if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end):
 			self.status = 'Past Due Date'
 
-		if self.cancel_at_period_end and getdate(nowdate()) > self.current_invoice_end:
+		if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end):
 			self.cancel_subscription_at_period_end()
 
 	def cancel_subscription_at_period_end(self):
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index c42b8e8..a5285ea 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -500,3 +500,51 @@
 		self.assertEqual(invoice.apply_discount_on, 'Grand Total')
 
 		subscription.delete()
+
+	def test_prepaid_subscriptions(self):
+		# Create a non pre-billed subscription, processing should not create
+		# invoices.
+		subscription = frappe.new_doc('Subscription')
+		subscription.subscriber = '_Test Customer'
+		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+		subscription.save()
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 0)
+
+		# Change the subscription type to prebilled and process it.
+		# Prepaid invoice should be generated
+		subscription.generate_invoice_at_period_start = True
+		subscription.save()
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 1)
+
+	def test_prepaid_subscriptions_with_prorate_true(self):
+		settings = frappe.get_single('Subscription Settings')
+		to_prorate = settings.prorate
+		settings.prorate = 1
+		settings.save()
+
+		subscription = frappe.new_doc('Subscription')
+		subscription.subscriber = '_Test Customer'
+		subscription.generate_invoice_at_period_start = True
+		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+		subscription.save()
+		subscription.cancel_subscription()
+
+		self.assertEqual(len(subscription.invoices), 1)
+
+		current_inv = subscription.get_current_invoice()
+		self.assertEqual(current_inv.status, "Unpaid")
+
+		diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
+		plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1)
+		prorate_factor = flt(diff / plan_days)
+
+		self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2))
+
+		settings.prorate = to_prorate
+		settings.save()
+
+		subscription.delete()
diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py
index 8897792..180fd09 100644
--- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py
+++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py
@@ -5,6 +5,7 @@
 import frappe
 from frappe import msgprint, _
 from frappe.utils import flt
+from erpnext import get_company_currency
 
 def execute(filters=None):
 	if not filters: filters = {}
@@ -14,12 +15,14 @@
 	item_details = get_item_details()
 	data = []
 
+	company_currency = get_company_currency(filters["company"])
+
 	for d in entries:
 		if d.stock_qty > 0 or filters.get('show_return_entries', 0):
 			data.append([
 				d.name, d.customer, d.territory, item_details.get(d.item_code, {}).get("website_warehouse"), d.posting_date, d.item_code,
 				item_details.get(d.item_code, {}).get("item_group"), item_details.get(d.item_code, {}).get("brand"),
-				d.stock_qty, d.base_net_amount, d.sales_person, d.allocated_percentage, d.contribution_amt
+				d.stock_qty, d.base_net_amount, d.sales_person, d.allocated_percentage, d.contribution_amt, company_currency
 			])
 
 	if data:
@@ -32,13 +35,105 @@
 	if not filters.get("doc_type"):
 		msgprint(_("Please select the document type first"), raise_exception=1)
 
-	return [filters["doc_type"] + ":Link/" + filters["doc_type"] + ":140",
-		_("Customer") + ":Link/Customer:140", _("Territory") + ":Link/Territory:100", _("Warehouse") + ":Link/Warehouse:100",
-		 _("Posting Date") + ":Date:100",
-		_("Item Code") + ":Link/Item:120", _("Item Group") + ":Link/Item Group:120",
-		_("Brand") + ":Link/Brand:120", _("Qty") + ":Float:100", _("Amount") + ":Currency:120",
-		_("Sales Person") + ":Link/Sales Person:140", _("Contribution %") + "::110",
-		_("Contribution Amount") + ":Currency:140"]
+	columns = [
+		{
+			"label": _(filters["doc_type"]),
+			"options": filters["doc_type"],
+			"fieldname": frappe.scrub(filters['doc_type']),
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Customer"),
+			"options": "Customer",
+			"fieldname": "customer",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Territory"),
+			"options": "Territory",
+			"fieldname": "territory",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Warehouse"),
+			"options": "Warehouse",
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Posting Date"),
+			"fieldname": "posting_date",
+			"fieldtype": "Date",
+			"width": 140
+		},
+		{
+			"label": _("Item Code"),
+			"options": "Item",
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Item Group"),
+			"options": "Item Group",
+			"fieldname": "item_group",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Brand"),
+			"options": "Brand",
+			"fieldname": "brand",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Qty"),
+			"fieldname": "qty",
+			"fieldtype": "Float",
+			"width": 140
+		},
+		{
+			"label": _("Amount"),
+			"options": "currency",
+			"fieldname": "amount",
+			"fieldtype": "Currency",
+			"width": 140
+		},
+		{
+			"label": _("Sales Person"),
+			"options": "Sales Person",
+			"fieldname": "sales_person",
+			"fieldtype": "Link",
+			"width": 140
+		},
+		{
+			"label": _("Contribution %"),
+			"fieldname": "contribution",
+			"fieldtype": "Float",
+			"width": 140
+		},
+		{
+			"label": _("Contribution Amount"),
+			"options": "currency",
+			"fieldname": "contribution_amt",
+			"fieldtype": "Currency",
+			"width": 140
+		},
+		{
+			"label":_("Currency"),
+			"options": "Currency",
+			"fieldname":"currency",
+			"fieldtype":"Link",
+			"hidden" : 1
+		}
+	]
+
+	return columns
 
 def get_entries(filters):
 	date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date"
diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py
index 30fcad8..e539aff 100644
--- a/erpnext/stock/report/item_price_stock/item_price_stock.py
+++ b/erpnext/stock/report/item_price_stock/item_price_stock.py
@@ -13,13 +13,19 @@
 def get_columns():
 	return [
 		{
-			"label": _("Item Name"),
-			"fieldname": "item_name",
+			"label": _("Item Code"),
+			"fieldname": "item_code",
 			"fieldtype": "Link",
 			"options": "Item",
 			"width": 120
 		},
 		{
+			"label": _("Item Name"),
+			"fieldname": "item_name",
+			"fieldtype": "Data",
+			"width": 120
+		},
+		{
 			"label": _("Brand"),
 			"fieldname": "brand",
 			"fieldtype": "Data",
@@ -76,7 +82,7 @@
 	if filters.get("item_code"):
 		conditions += "where a.item_code=%(item_code)s"
 
-	item_results = frappe.db.sql("""select a.item_code as item_name, a.name as price_list_name,
+	item_results = frappe.db.sql("""select a.item_code, a.item_name, a.name as price_list_name,
 		a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty
 		from `tabItem Price` a left join `tabBin` b
 		ON a.item_code = b.item_code
@@ -92,6 +98,7 @@
 	if item_results:
 		for item_dict in item_results:
 			data = {
+				'item_code': item_dict.item_code,
 				'item_name': item_dict.item_name,
 				'brand': item_dict.brand,
 				'warehouse': item_dict.warehouse,