Enhancements to Supplier Portal (#19221)

* fix: Add Purchase Order to portal

* fix: Create Customer or Supplier on first login

Based on default role set in Portal Settings, a Customer or Supplier
will be created when the user logs in for the first time.

* fix: Styling for transaction_row

* fix: Styling for RFQ page

* fix: Add Purchase Invoice route

- Make Purchase Invoice from PO

* fix: minor

- Admissions for Student role
- Remove print statement
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index bc9c178..4ea9b1c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -880,6 +880,17 @@
 		# calculate totals again after applying TDS
 		self.calculate_taxes_and_totals()
 
+def get_list_context(context=None):
+	from erpnext.controllers.website_list_for_contact import get_list_context
+	list_context = get_list_context(context)
+	list_context.update({
+		'show_sidebar': True,
+		'show_search': True,
+		'no_breadcrumbs': True,
+		'title': _('Purchase Invoices'),
+	})
+	return list_context
+
 @frappe.whitelist()
 def make_debit_note(source_name, target_doc=None):
 	from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index e3e2f1e..845ff74 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -386,7 +386,21 @@
 
 @frappe.whitelist()
 def make_purchase_invoice(source_name, target_doc=None):
+	return get_mapped_purchase_invoice(source_name, target_doc)
+
+@frappe.whitelist()
+def make_purchase_invoice_from_portal(purchase_order_name):
+	doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True)
+	if doc.contact_email != frappe.session.user:
+		frappe.throw(_('Not Permitted'), frappe.PermissionError)
+	doc.save()
+	frappe.db.commit()
+	frappe.response['type'] = 'redirect'
+	frappe.response.location = '/purchase-invoices/' + doc.name
+
+def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False):
 	def postprocess(source, target):
+		target.flags.ignore_permissions = ignore_permissions
 		set_missing_values(source, target)
 		#Get the advance paid Journal Entries in Purchase Invoice Advance
 
@@ -437,7 +451,8 @@
 			"add_if_empty": True
 		}
 
-	doc = get_mapped_doc("Purchase Order", source_name,	fields, target_doc, postprocess)
+	doc = get_mapped_doc("Purchase Order", source_name,	fields,
+		target_doc, postprocess, ignore_permissions=ignore_permissions)
 
 	return doc
 
@@ -501,6 +516,17 @@
 
 	return item_details
 
+def get_list_context(context=None):
+	from erpnext.controllers.website_list_for_contact import get_list_context
+	list_context = get_list_context(context)
+	list_context.update({
+		'show_sidebar': True,
+		'show_search': True,
+		'no_breadcrumbs': True,
+		'title': _('Purchase Orders'),
+	})
+	return list_context
+
 @frappe.whitelist()
 def update_status(status, name):
 	po = frappe.get_doc("Purchase Order", name)
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 0738fd5..ed37938 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -25,7 +25,7 @@
 
 	if not filters: filters = []
 
-	if doctype == 'Supplier Quotation':
+	if doctype in ['Supplier Quotation', 'Purchase Invoice']:
 		filters.append((doctype, 'docstatus', '<', 2))
 	else:
 		filters.append((doctype, 'docstatus', '=', 1))
@@ -175,4 +175,4 @@
 	if doctype == 'Quotation':
 		return 'party_name'
 	else:
-		return 'customer'
\ No newline at end of file
+		return 'customer'
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9742a03..5c61874 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -45,7 +45,10 @@
 leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
 
 
-on_session_creation = "erpnext.shopping_cart.utils.set_cart_count"
+on_session_creation = [
+	"erpnext.portal.utils.create_customer_or_supplier",
+	"erpnext.shopping_cart.utils.set_cart_count"
+]
 on_logout = "erpnext.shopping_cart.utils.clear_cart_count"
 
 treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
@@ -102,6 +105,20 @@
 			"parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}]
 		}
 	},
+	{"from_route": "/purchase-orders", "to_route": "Purchase Order"},
+	{"from_route": "/purchase-orders/<path:name>", "to_route": "order",
+		"defaults": {
+			"doctype": "Purchase Order",
+			"parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}]
+		}
+	},
+	{"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"},
+	{"from_route": "/purchase-invoices/<path:name>", "to_route": "order",
+		"defaults": {
+			"doctype": "Purchase Invoice",
+			"parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}]
+		}
+	},
 	{"from_route": "/quotations", "to_route": "Quotation"},
 	{"from_route": "/quotations/<path:name>", "to_route": "order",
 		"defaults": {
@@ -148,6 +165,8 @@
 	{"title": _("Projects"), "route": "/project", "reference_doctype": "Project"},
 	{"title": _("Request for Quotations"), "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier"},
 	{"title": _("Supplier Quotation"), "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"},
+	{"title": _("Purchase Orders"), "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier"},
+	{"title": _("Purchase Invoices"), "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier"},
 	{"title": _("Quotations"), "route": "/quotations", "reference_doctype": "Quotation", "role":"Customer"},
 	{"title": _("Orders"), "route": "/orders", "reference_doctype": "Sales Order", "role":"Customer"},
 	{"title": _("Invoices"), "route": "/invoices", "reference_doctype": "Sales Invoice", "role":"Customer"},
@@ -160,8 +179,8 @@
 	{"title": _("Patient Appointment"), "route": "/patient-appointments", "reference_doctype": "Patient Appointment", "role":"Patient"},
 	{"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"},
 	{"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"},
-	{"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission"},
-	{"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application"},
+	{"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"},
+	{"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"},
 	{"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"},
 ]
 
@@ -181,6 +200,8 @@
 	"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
 	"Sales Invoice": "erpnext.controllers.website_list_for_contact.has_website_permission",
 	"Supplier Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
+	"Purchase Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
+	"Purchase Invoice": "erpnext.controllers.website_list_for_contact.has_website_permission",
 	"Material Request": "erpnext.controllers.website_list_for_contact.has_website_permission",
 	"Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission",
 	"Issue": "erpnext.support.doctype.issue.issue.has_website_permission",
diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py
index 2e710c7..56e4fcd 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -1,5 +1,8 @@
 from __future__ import unicode_literals
 import frappe
+from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
+from erpnext.shopping_cart.cart import get_debtors_account
+from frappe.utils.nestedset import get_root_of
 
 def set_default_role(doc, method):
 	'''Set customer, supplier, student, guardian based on email'''
@@ -21,3 +24,88 @@
 		doc.add_roles('Student')
 	elif frappe.get_value('Guardian', dict(email_address=doc.email)) and 'Guardian' not in roles:
 		doc.add_roles('Guardian')
+
+def create_customer_or_supplier():
+	'''Based on the default Role (Customer, Supplier), create a Customer / Supplier.
+	Called on_session_creation hook.
+	'''
+	user = frappe.session.user
+
+	if frappe.db.get_value('User', user, 'user_type') != 'Website User':
+		return
+
+	user_roles = frappe.get_roles()
+	portal_settings = frappe.get_single('Portal Settings')
+	default_role = portal_settings.default_role
+
+	if default_role not in ['Customer', 'Supplier']:
+		return
+
+	# create customer / supplier if the user has that role
+	if portal_settings.default_role and portal_settings.default_role in user_roles:
+		doctype = portal_settings.default_role
+	else:
+		doctype = None
+
+	if not doctype:
+		return
+
+	if party_exists(doctype, user):
+		return
+
+	party = frappe.new_doc(doctype)
+	fullname = frappe.utils.get_fullname(user)
+
+	if doctype == 'Customer':
+		cart_settings = get_shopping_cart_settings()
+
+		if cart_settings.enable_checkout:
+			debtors_account = get_debtors_account(cart_settings)
+		else:
+			debtors_account = ''
+
+		party.update({
+			"customer_name": fullname,
+			"customer_type": "Individual",
+			"customer_group": cart_settings.default_customer_group,
+			"territory": get_root_of("Territory")
+		})
+
+		if debtors_account:
+			party.update({
+				"accounts": [{
+					"company": cart_settings.company,
+					"account": debtors_account
+				}]
+			})
+	else:
+		party.update({
+			"supplier_name": fullname,
+			"supplier_group": "All Supplier Groups",
+			"supplier_type": "Individual"
+		})
+
+	party.flags.ignore_mandatory = True
+	party.insert(ignore_permissions=True)
+
+	contact = frappe.new_doc("Contact")
+	contact.update({
+		"first_name": fullname,
+		"email_id": user
+	})
+	contact.append('links', dict(link_doctype=doctype, link_name=party.name))
+	contact.flags.ignore_mandatory = True
+	contact.insert(ignore_permissions=True)
+
+	return party
+
+
+def party_exists(doctype, user):
+	contact_name = frappe.db.get_value("Contact", {"email_id": user})
+
+	if contact_name:
+		contact = frappe.get_doc('Contact', contact_name)
+		doctypes = [d.link_doctype for d in contact.links]
+		return doctype in doctypes
+
+	return False
diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss
index 002498f..7b9a70d 100644
--- a/erpnext/public/scss/website.scss
+++ b/erpnext/public/scss/website.scss
@@ -51,3 +51,30 @@
 	width: 24px;
 	height: 24px;
 }
+
+.website-list .result {
+	margin-top: 2rem;
+}
+
+.result {
+	border-bottom: 1px solid $border-color;
+}
+
+.transaction-list-item {
+	padding: 1rem 0;
+	border-top: 1px solid $border-color;
+	position: relative;
+
+	a.transaction-item-link {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		text-decoration: none;
+		opacity: 0;
+		overflow: hidden;
+		text-indent: -9999px;
+		z-index: 0;
+	}
+}
diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html
index cb77f7e..caa15f3 100644
--- a/erpnext/templates/includes/rfq/rfq_items.html
+++ b/erpnext/templates/includes/rfq/rfq_items.html
@@ -3,13 +3,13 @@
 {% for d in doc.items %}
 <div class="rfq-item">
 	<div class="row">
-		<div class="col-sm-5 col-xs-12" style="margin-bottom: 10px;margin-top: 5px;">
+		<div class="col-sm-5 col-12" style="margin-bottom: 10px;margin-top: 5px;">
 			{{ item_name_and_description(d, doc) }}
 		</div>
-		<!-- <div class="col-sm-2 col-xs-2" style="margin-bottom: 10px;">
+		<!-- <div class="col-sm-2 col-2" style="margin-bottom: 10px;">
 			<textarea type="text" style="margin-top: 5px;" class="input-with-feedback form-control rfq-offer_detail" ></textarea>
 		</div> -->
-		<div class="col-sm-2 col-xs-4 text-right">
+		<div class="col-sm-2 col-4 text-right">
 				<input type="text" class="form-control text-right rfq-qty" style="margin-top: 5px;display: inline-block"
 				value = "{{ d.get_formatted('qty') }}"
 				data-idx="{{ d.idx }}">
@@ -17,14 +17,14 @@
 					{{_("UOM") + ":"+ d.uom}}
 				</p>
 		</div>
-		<div class="col-sm-2 col-xs-4 text-right">
+		<div class="col-sm-2 col-4 text-right">
 			<input type="text" class="form-control text-right rfq-rate"
 				style="margin-top: 5px;display: inline-block" value="0.00"
 				data-idx="{{ d.idx }}">
 		</div>
-        <div class="col-sm-3 col-xs-4 text-right" style="padding-top: 9px;">
+        <div class="col-sm-3 col-4 text-right" style="padding-top: 9px;">
            {{doc.currency_symbol}} <span class="rfq-amount" data-idx="{{ d.idx }}">0.00</span>
         </div>
 		</div>
 	</div>
-{% endfor %}
\ No newline at end of file
+{% endfor %}
diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html
index 95bbcfe..88724c3 100644
--- a/erpnext/templates/includes/rfq/rfq_macros.html
+++ b/erpnext/templates/includes/rfq/rfq_macros.html
@@ -1,13 +1,11 @@
-{% from "erpnext/templates/includes/macros.html" import product_image_square %}
+{% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %}
 
 {% macro item_name_and_description(d, doc) %}
     <div class="row">
-        <div class="col-xs-4 col-sm-2 order-image-col">
-            <div class="order-image">
-                {{ product_image_square(d.image) }}
-            </div>
+        <div class="col-3">
+			{{ product_image(d.image) }}
         </div>
-        <div class="col-xs-8 col-sm-10">
+        <div class="col-9">
             {{ d.item_code }}
             <p class="text-muted small">{{ d.description }}</p>
 			{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}
diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html
index 6c58b51..80a542f 100644
--- a/erpnext/templates/includes/transaction_row.html
+++ b/erpnext/templates/includes/transaction_row.html
@@ -1,22 +1,21 @@
 <div class="web-list-item transaction-list-item">
-	<a href="/{{ pathname }}/{{ doc.name }}">
-		<div class="row">
-			<div class="col-sm-4" style='margin-top: -3px;'>
-				<span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
-				{{ doc.name }}</span>
-				<div class="small text-muted transaction-time"
-					title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
-					{{ frappe.utils.global_date_format(doc.modified) }}
-				</div>
-			</div>
-			<div class="col-sm-5">
-				<div class="small text-muted items-preview ellipsis ellipsis-width">
-					{{ doc.items_preview }}
-				</div>
-			</div>
-			<div class="col-sm-3 text-right bold">
-				{{ doc.get_formatted("grand_total") }}
+	<div class="row">
+		<div class="col-sm-4">
+			<span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
+			{{ doc.name }}</span>
+			<div class="small text-muted transaction-time"
+				title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
+				{{ frappe.utils.global_date_format(doc.modified) }}
 			</div>
 		</div>
-	</a>
+		<div class="col-sm-5">
+			<div class="small text-muted items-preview ellipsis ellipsis-width">
+				{{ doc.items_preview }}
+			</div>
+		</div>
+		<div class="col-sm-3 text-right bold">
+			{{ doc.get_formatted("grand_total") }}
+		</div>
+	</div>
+	<a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a>
 </div>
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 67a8fed..9e3c58b 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -12,7 +12,22 @@
 {% endblock %}
 
 {% block header_actions %}
-<a href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
+<div class="dropdown">
+	<button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
+		<span>{{ _('Actions') }}</span>
+		<b class="caret"></b>
+	</button>
+	<ul class="dropdown-menu dropdown-menu-right" role="menu">
+		{% if doc.doctype == 'Purchase Order' %}
+		<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
+		{% endif %}
+		<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
+			target="_blank" rel="noopener noreferrer">
+			{{ _("Print") }}
+		</a>
+	</ul>
+</div>
+
 {% endblock %}
 
 {% block page_content %}
@@ -34,7 +49,7 @@
 </div>
 
 <p class="small my-3">
-	{%- set party_name = doc.supplier_name if doc.doctype == 'Supplier Quotation' else doc.customer_name %}
+	{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
 	<b>{{ party_name }}</b>
 
 	{% if doc.contact_display and doc.contact_display != party_name %}
@@ -172,4 +187,4 @@
 			currency: '{{ doc.currency }}'
 		}
 	</script>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/erpnext/templates/pages/order.js b/erpnext/templates/pages/order.js
index 21c3a14..0574cde 100644
--- a/erpnext/templates/pages/order.js
+++ b/erpnext/templates/pages/order.js
@@ -5,7 +5,9 @@
 
 	var loyalty_points_input = document.getElementById("loyalty-point-to-redeem");
 	var loyalty_points_status = document.getElementById("loyalty-points-status");
-	loyalty_points_input.onblur = apply_loyalty_points;
+	if (loyalty_points_input) {
+		loyalty_points_input.onblur = apply_loyalty_points;
+	}
 
 	function apply_loyalty_points() {
 		var loyalty_points = parseInt(loyalty_points_input.value);
@@ -37,4 +39,4 @@
 			});
 		}
 	}
-})
\ No newline at end of file
+})
diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html
index 591d046..5b27a94 100644
--- a/erpnext/templates/pages/rfq.html
+++ b/erpnext/templates/pages/rfq.html
@@ -22,10 +22,10 @@
 
 {% block page_content %}
 <div class="row">
-    <div class="col-xs-6">
+    <div class="col-6">
         <div class="rfq-supplier">{{ doc.supplier }}</div>
 	</div>
-    <div class="col-xs-6 text-muted text-right h6">
+    <div class="col-6 text-muted text-right h6">
         {{ doc.get_formatted("transaction_date") }}
     </div>
 </div>
@@ -33,16 +33,16 @@
 	<div id="order-container">
 			<div id="rfq-items">
 				<div class="row cart-item-header">
-					<div class="col-sm-5 col-xs-12">
+					<div class="col-sm-5 col-12">
 						{{ _("Items") }}
 					</div>
-					<div class="col-sm-2 col-xs-4 text-right">
+					<div class="col-sm-2 col-4 text-right">
 						{{ _("Qty") }}
 					</div>
-					<div class="col-sm-2 col-xs-4 text-right">
+					<div class="col-sm-2 col-4 text-right">
 						{{ _("Rate") }}
 					</div>
-					<div class="col-sm-3 col-xs-4 text-right">
+					<div class="col-sm-3 col-4 text-right">
 						{{ _("Amount") }}
 					</div>
 				</div>
@@ -55,30 +55,29 @@
 		</div>
         {% if doc.items %}
 		<div class="row grand-total-row">
-			<div class="col-xs-9 text-right">{{ _("Grand Total") }}</div>
-			<div class="col-xs-3 text-right">
+			<div class="col-9 text-right">{{ _("Grand Total") }}</div>
+			<div class="col-3 text-right">
 			{{doc.currency_symbol}}  <span class="tax-grand-total">0.0</span>
 			</div>
 		</div>
         {% endif %}
 		<div class="row terms">
-			<div class="col-xs-6">
+			<div class="col-6">
 				<br><br>
 				<p class="text-muted small">{{ _("Notes: ") }}</p>
 				<textarea class="form-control terms-feedback" style="height: 100px;"></textarea>
 			</div>
 		</div>
-		<hr>
-		<div class="row">
-			<div class="result">
-				<div class="col-xs-12">
-					<p class="text-muted small">{{ _("Quotations: ") }}</p>
-					{% if doc.rfq_links %}
+		<div class="row mt-5">
+			<div class="col-12">
+				<p class="text-muted small">{{ _("Quotations: ") }}</p>
+				{% if doc.rfq_links %}
+					<div class="result">
 						{% for d in doc.rfq_links %}
 							<div class="web-list-item transaction-list-item quotations" idx="{{d.name}}">
 								<div class="row">
 									<div class="col-sm-6">
-										<span class="indicator darkgrey"><a href="/quotations/{{d.name}}">{{d.name}}</a></span>
+										<span class="indicator darkgrey">{{d.name}}</span>
 									</div>
 									<div class="col-sm-3">
 										<span class="small darkgrey">{{d.status}}</span>
@@ -87,10 +86,11 @@
 										<span class="small darkgrey">{{d.transaction_date}}</span>
 									</div>
 								</div>
+								<a class="transaction-item-link" href="/quotations/{{d.name}}">Link</a>
 							</div>
 						{% endfor %}
-					{% endif %}
-				</div>
+					</div>
+				{% endif %}
 			</div>
 		</div>
     </div>