Merge pull request #10600 from netchampfaris/pos-refactor

POS Online
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 03d84e9..97bbc122 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -8,10 +8,6 @@
 		return { filters: { selling: 1 } };
 	});
 
-	frm.set_query("print_format", function() {
-		return { filters: { doc_type: "Sales Invoice", print_format_type: "Js"} };
-	});
-
 	erpnext.queries.setup_queries(frm, "Warehouse", function() {
 		return erpnext.queries.warehouse(frm.doc);
 	});
@@ -27,6 +23,27 @@
 });
 
 frappe.ui.form.on('POS Profile', {
+	setup: function(frm) {
+		frm.set_query("online_print_format", function() {
+			return {
+				filters: [
+					['Print Format', 'doc_type', '=', 'Sales Invoice'],
+					['Print Format', 'print_format_type', '!=', 'Js'],
+				]
+			};
+		});
+
+		frm.set_query("print_format", function() {
+			return { filters: { doc_type: "Sales Invoice", print_format_type: "Js"} };
+		});
+
+		frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => {
+			is_online = r && cint(r.is_online)
+			frm.toggle_display('offline_pos_section', !is_online);
+			frm.toggle_display('print_format_for_online', is_online);
+		});
+	},
+
 	refresh: function(frm) {
 		if(frm.doc.company) {
 			frm.trigger("toggle_display_account_head");
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 6991da2..187454e 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -631,8 +631,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
-   "default": "Point of Sale", 
-   "fieldname": "print_format", 
+   "fieldname": "print_format_for_online", 
    "fieldtype": "Link", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -641,7 +640,7 @@
    "in_global_search": 0, 
    "in_list_view": 0, 
    "in_standard_filter": 0, 
-   "label": "Print Format", 
+   "label": "Print Format for Online", 
    "length": 0, 
    "no_copy": 0, 
    "options": "Print Format", 
@@ -822,7 +821,7 @@
    "columns": 0, 
    "fieldname": "apply_discount", 
    "fieldtype": "Check", 
-   "hidden": 0, 
+   "hidden": 1, 
    "ignore_user_permissions": 0, 
    "ignore_xss_filter": 0, 
    "in_filter": 0, 
@@ -836,7 +835,7 @@
    "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, 
@@ -851,7 +850,7 @@
    "collapsible": 0, 
    "columns": 0, 
    "default": "Grand Total", 
-   "depends_on": "apply_discount", 
+   "depends_on": "", 
    "fieldname": "apply_discount_on", 
    "fieldtype": "Select", 
    "hidden": 0, 
@@ -883,7 +882,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
-   "fieldname": "customer_details", 
+   "fieldname": "offline_pos_section", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -892,7 +891,7 @@
    "in_global_search": 0, 
    "in_list_view": 0, 
    "in_standard_filter": 0, 
-   "label": "New Customer Details", 
+   "label": "Offline POS Section", 
    "length": 0, 
    "no_copy": 0, 
    "permlevel": 0, 
@@ -975,6 +974,38 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "default": "Point of Sale", 
+   "fieldname": "print_format", 
+   "fieldtype": "Link", 
+   "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": "Print Format", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "Print Format", 
+   "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, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
    "fieldname": "customer_group", 
    "fieldtype": "Link", 
    "hidden": 0, 
@@ -1291,7 +1322,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2017-07-28 03:40:03.253088", 
+ "modified": "2017-09-01 15:55:14.890452", 
  "modified_by": "Administrator", 
  "module": "Accounts", 
  "name": "POS Profile", 
diff --git a/erpnext/accounts/doctype/pos_settings/__init__.py b/erpnext/accounts/doctype/pos_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_settings/__init__.py
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js
new file mode 100644
index 0000000..1a14618
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('POS Settings', {
+	refresh: function() {
+
+	}
+});
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json
new file mode 100644
index 0000000..a04558d
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json
@@ -0,0 +1,94 @@
+{
+ "allow_copy": 0, 
+ "allow_guest_to_view": 0, 
+ "allow_import": 0, 
+ "allow_rename": 0, 
+ "beta": 0, 
+ "creation": "2017-08-28 16:46:41.732676", 
+ "custom": 0, 
+ "docstatus": 0, 
+ "doctype": "DocType", 
+ "document_type": "", 
+ "editable_grid": 1, 
+ "engine": "InnoDB", 
+ "fields": [
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "default": "1", 
+   "fieldname": "is_online", 
+   "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": "Online", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "", 
+   "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, 
+   "unique": 0
+  }
+ ], 
+ "has_web_view": 0, 
+ "hide_heading": 0, 
+ "hide_toolbar": 0, 
+ "idx": 0, 
+ "image_view": 0, 
+ "in_create": 0, 
+ "is_submittable": 0, 
+ "issingle": 1, 
+ "istable": 0, 
+ "max_attachments": 0, 
+ "modified": "2017-08-30 18:34:58.960276", 
+ "modified_by": "Administrator", 
+ "module": "Accounts", 
+ "name": "POS Settings", 
+ "name_case": "", 
+ "owner": "Administrator", 
+ "permissions": [
+  {
+   "amend": 0, 
+   "apply_user_permissions": 0, 
+   "cancel": 0, 
+   "create": 0, 
+   "delete": 0, 
+   "email": 1, 
+   "export": 0, 
+   "if_owner": 0, 
+   "import": 0, 
+   "permlevel": 0, 
+   "print": 1, 
+   "read": 1, 
+   "report": 0, 
+   "role": "System Manager", 
+   "set_user_permissions": 0, 
+   "share": 1, 
+   "submit": 0, 
+   "write": 1
+  }
+ ], 
+ "quick_entry": 1, 
+ "read_only": 0, 
+ "read_only_onload": 0, 
+ "show_name_in_global_search": 0, 
+ "sort_field": "modified", 
+ "sort_order": "DESC", 
+ "track_changes": 1, 
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py
new file mode 100644
index 0000000..736d36e
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+class POSSettings(Document):
+	pass
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_settings/test_pos_settings.js b/erpnext/accounts/doctype/pos_settings/test_pos_settings.js
new file mode 100644
index 0000000..639c94e
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_settings/test_pos_settings.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: POS Settings", function (assert) {
+	let done = assert.async();
+
+	// number of asserts
+	assert.expect(1);
+
+	frappe.run_serially([
+		// insert a new POS Settings
+		() => frappe.tests.make('POS Settings', [
+			// values to be set
+			{key: 'value'}
+		]),
+		() => {
+			assert.equal(cur_frm.doc.key, 'value');
+		},
+		() => done()
+	]);
+
+});
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index a29defa..065fb94 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -313,7 +313,7 @@
 
 			for fieldname in ('territory', 'naming_series', 'currency', 'taxes_and_charges', 'letter_head', 'tc_name',
 				'selling_price_list', 'company', 'select_print_heading', 'cash_bank_account',
-				'write_off_account', 'write_off_cost_center'):
+				'write_off_account', 'write_off_cost_center', 'apply_discount_on'):
 					if (not for_validate) or (for_validate and not self.get(fieldname)):
 						self.set(fieldname, pos.get(fieldname))
 
diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js
index 33b41e9..2f42524 100644
--- a/erpnext/accounts/page/pos/pos.js
+++ b/erpnext/accounts/page/pos/pos.js
@@ -8,7 +8,16 @@
 		single_column: true
 	});
 
-	wrapper.pos = new erpnext.pos.PointOfSale(wrapper)
+	frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => {
+		if (r && r.is_online && !cint(r.is_online)) {
+			// offline
+			wrapper.pos = new erpnext.pos.PointOfSale(wrapper);
+			cur_pos = wrapper.pos;
+		} else {
+			// online
+			frappe.set_route('point-of-sale');
+		}
+	});
 }
 
 frappe.pages['pos'].refresh = function (wrapper) {
diff --git a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json b/erpnext/accounts/print_format/point_of_sale/point_of_sale.json
index 28c853c..4e69cad 100644
--- a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json
+++ b/erpnext/accounts/print_format/point_of_sale/point_of_sale.json
@@ -10,7 +10,7 @@
  "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ company }}<br>\n\t{{  __(\"POS No : \") }} {{ offline_pos_name }}<br>\n</p>\n<p>\n\t<b>{{ __(\"Customer\") }}:</b> {{ customer }}<br>\n</p>\n\n<p>\n\t<b>{{ __(\"Date\") }}:</b> {{ dateutil.global_date_format(posting_date) }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ __(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{% for item in items %}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_name }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ format_number(item.qty, null,precision(\"difference\")) }}<br>@ {{ format_currency(item.rate, currency) }}</td>\n\t\t\t<td class=\"text-right\">{{ format_currency(item.amount, currency) }}</td>\n\t\t</tr>\n\t\t{% endfor %}\n\t</tbody>\n</table>\n\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ row.description }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n\n<hr>\n<p>{{ terms }}</p>\n<p class=\"text-center\">{{ __(\"Thank you, please visit again.\") }}</p>", 
  "idx": 0, 
  "line_breaks": 0, 
- "modified": "2017-05-19 14:36:04.740728", 
+ "modified": "2017-09-01 14:27:04.871233", 
  "modified_by": "Administrator", 
  "module": "Accounts", 
  "name": "Point of Sale", 
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d11b668..50c64b5 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -435,6 +435,7 @@
 erpnext.patches.v8_7.add_more_gst_fields
 erpnext.patches.v8_7.fix_purchase_receipt_status
 erpnext.patches.v8_6.rename_bom_update_tool
+erpnext.patches.v8_7.set_offline_in_pos_settings
 erpnext.patches.v8_9.add_setup_progress_actions
 erpnext.patches.v8_9.rename_company_sales_target_field
 erpnext.patches.v8_8.set_bom_rate_as_per_uom
diff --git a/erpnext/patches/v8_7/set_offline_in_pos_settings.py b/erpnext/patches/v8_7/set_offline_in_pos_settings.py
new file mode 100644
index 0000000..64a3a7c
--- /dev/null
+++ b/erpnext/patches/v8_7/set_offline_in_pos_settings.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+	frappe.reload_doc('accounts', 'doctype', 'pos_settings')
+
+	doc = frappe.get_doc('POS Settings')
+	doc.is_online = 0
+	doc.save()
\ No newline at end of file
diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css
index 0660b39..13fdcf1 100644
--- a/erpnext/public/css/erpnext.css
+++ b/erpnext/public/css/erpnext.css
@@ -308,16 +308,6 @@
 body[data-route="pos"] .item-list .pos-item-wrapper {
   position: relative;
 }
-body[data-route="pos"] .item-list .price-info {
-  position: absolute;
-  left: 0;
-  bottom: 0;
-  margin: 0 0 15px 15px;
-  background-color: rgba(141, 153, 166, 0.6);
-  padding: 5px 9px;
-  border-radius: 3px;
-  color: #fff;
-}
 body[data-route="pos"] .pos-bill-toolbar {
   margin-top: 10px;
 }
@@ -356,3 +346,13 @@
 body[data-route="pos"] .collapse-btn {
   cursor: pointer;
 }
+.price-info {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  margin: 0 0 15px 15px;
+  background-color: rgba(141, 153, 166, 0.6);
+  padding: 5px 9px;
+  border-radius: 3px;
+  color: #fff;
+}
diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css
new file mode 100644
index 0000000..f66abc8
--- /dev/null
+++ b/erpnext/public/css/pos.css
@@ -0,0 +1,174 @@
+[data-route="point-of-sale"] .layout-main-section-wrapper {
+  margin-bottom: 0;
+}
+[data-route="point-of-sale"] .pos-items-wrapper {
+  max-height: calc(100vh - 210px);
+}
+.pos {
+  padding: 15px;
+}
+.list-item {
+  min-height: 40px;
+  height: auto;
+}
+.cart-container {
+  padding: 0 15px;
+  display: inline-block;
+  width: 39%;
+  vertical-align: top;
+}
+.item-container {
+  padding: 0 15px;
+  display: inline-block;
+  width: 60%;
+  vertical-align: top;
+}
+.search-field {
+  width: 60%;
+}
+.search-field input::placeholder {
+  font-size: 12px;
+}
+.item-group-field {
+  width: 40%;
+  margin-left: 15px;
+}
+.cart-wrapper {
+  margin-bottom: 10px;
+}
+.cart-wrapper .list-item__content:not(:first-child) {
+  justify-content: flex-end;
+}
+.cart-wrapper .list-item--head .list-item__content:nth-child(2) {
+  flex: 1.5;
+}
+.cart-items {
+  height: 150px;
+  overflow: auto;
+}
+.cart-items .list-item.current-item {
+  background-color: #fffce7;
+}
+.cart-items .list-item.current-item.qty input {
+  border: 1px solid #5E64FF;
+  font-weight: bold;
+}
+.cart-items .list-item.current-item.disc .discount {
+  font-weight: bold;
+}
+.cart-items .list-item.current-item.rate .rate {
+  font-weight: bold;
+}
+.cart-items .list-item .quantity {
+  flex: 1.5;
+}
+.cart-items input {
+  text-align: right;
+  height: 22px;
+  font-size: 12px;
+}
+.fields {
+  display: flex;
+}
+.pos-items-wrapper {
+  max-height: 480px;
+  overflow-y: auto;
+}
+.pos-items {
+  overflow: hidden;
+}
+.pos-item-wrapper {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  width: 25%;
+}
+.image-view-container {
+  display: block;
+}
+.image-view-container .image-field {
+  height: auto;
+}
+.empty-state {
+  height: 100%;
+  position: relative;
+}
+.empty-state span {
+  position: absolute;
+  color: #8D99A6;
+  font-size: 12px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+@keyframes yellow-fade {
+  0% {
+    background-color: #fffce7;
+  }
+  100% {
+    background-color: transparent;
+  }
+}
+.highlight {
+  animation: yellow-fade 1s ease-in 1;
+}
+input[type=number]::-webkit-inner-spin-button,
+input[type=number]::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+.number-pad {
+  border-collapse: collapse;
+  cursor: pointer;
+  display: table;
+  margin: auto;
+}
+.num-row {
+  display: table-row;
+}
+.num-col {
+  display: table-cell;
+  border: 1px solid #d1d8dd;
+}
+.num-col > div {
+  width: 50px;
+  height: 50px;
+  text-align: center;
+  line-height: 50px;
+}
+.num-col.active {
+  background-color: #fffce7;
+}
+.num-col.brand-primary {
+  background-color: #5E64FF;
+  color: #ffffff;
+}
+.discount-amount .discount-inputs {
+  display: flex;
+  flex-direction: column;
+  padding: 15px 0;
+}
+.discount-amount input:first-child {
+  margin-bottom: 10px;
+}
+.taxes-and-totals {
+  border-top: 1px solid #d1d8dd;
+}
+.taxes-and-totals .taxes {
+  display: flex;
+  flex-direction: column;
+  padding: 15px 0;
+  align-items: flex-end;
+}
+.taxes-and-totals .taxes > div:first-child {
+  margin-bottom: 10px;
+}
+.grand-total {
+  border-top: 1px solid #d1d8dd;
+}
+.grand-total .list-item {
+  height: 60px;
+}
+.grand-total .grand-total-value {
+  font-size: 24px;
+}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index be55406..f5bcf1c 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -4,6 +4,7 @@
 erpnext.TransactionController = erpnext.taxes_and_totals.extend({
 	setup: function() {
 		this._super();
+		frappe.flags.hide_serial_batch_dialog = false;
 		frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
 			var item = frappe.get_doc(cdt, cdn);
 			var has_margin_field = frappe.meta.has_field(cdt, 'margin_type');
@@ -314,12 +315,15 @@
 						if(!r.exc) {
 							me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
 							me.toggle_conversion_factor(item);
-							if(show_batch_dialog) {
+							if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
 								var d = locals[cdt][cdn];
 								$.each(r.message, function(k, v) {
 									if(!d[k]) d[k] = v;
 								});
-								erpnext.show_serial_batch_selector(me.frm, d);
+
+								erpnext.show_serial_batch_selector(me.frm, d, (item) => {
+									me.frm.script_manager.trigger('qty', item.doctype, item.name);
+								});
 							}
 						}
 					}
@@ -1113,7 +1117,7 @@
 	},
 });
 
-erpnext.show_serial_batch_selector = function(frm, d) {
+erpnext.show_serial_batch_selector = function(frm, d, callback, show_dialog) {
 	frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
 		new erpnext.SerialNoBatchSelector({
 			frm: frm,
@@ -1122,6 +1126,7 @@
 				type: "Warehouse",
 				name: d.warehouse
 			},
-		});
+			callback: callback
+		}, show_dialog);
 	});
 }
diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js
new file mode 100644
index 0000000..075c9ca
--- /dev/null
+++ b/erpnext/public/js/pos/clusterize.js
@@ -0,0 +1,330 @@
+/* eslint-disable */
+/*! Clusterize.js - v0.17.6 - 2017-03-05
+* http://NeXTs.github.com/Clusterize.js/
+* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
+
+;(function(name, definition) {
+    if (typeof module != 'undefined') module.exports = definition();
+    else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
+    else this[name] = definition();
+}('Clusterize', function() {
+  "use strict"
+
+  // detect ie9 and lower
+  // https://gist.github.com/padolsey/527683#comment-786682
+  var ie = (function(){
+    for( var v = 3,
+             el = document.createElement('b'),
+             all = el.all || [];
+         el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
+         all[0];
+       ){}
+    return v > 4 ? v : document.documentMode;
+  }()),
+  is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
+  var Clusterize = function(data) {
+    if( ! (this instanceof Clusterize))
+      return new Clusterize(data);
+    var self = this;
+
+    var defaults = {
+      rows_in_block: 50,
+      blocks_in_cluster: 4,
+      tag: null,
+      show_no_data_row: true,
+      no_data_class: 'clusterize-no-data',
+      no_data_text: 'No data',
+      keep_parity: true,
+      callbacks: {}
+    }
+
+    // public parameters
+    self.options = {};
+    var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
+    for(var i = 0, option; option = options[i]; i++) {
+      self.options[option] = typeof data[option] != 'undefined' && data[option] != null
+        ? data[option]
+        : defaults[option];
+    }
+
+    var elems = ['scroll', 'content'];
+    for(var i = 0, elem; elem = elems[i]; i++) {
+      self[elem + '_elem'] = data[elem + 'Id']
+        ? document.getElementById(data[elem + 'Id'])
+        : data[elem + 'Elem'];
+      if( ! self[elem + '_elem'])
+        throw new Error("Error! Could not find " + elem + " element");
+    }
+
+    // tabindex forces the browser to keep focus on the scrolling list, fixes #11
+    if( ! self.content_elem.hasAttribute('tabindex'))
+      self.content_elem.setAttribute('tabindex', 0);
+
+    // private parameters
+    var rows = isArray(data.rows)
+        ? data.rows
+        : self.fetchMarkup(),
+      cache = {},
+      scroll_top = self.scroll_elem.scrollTop;
+
+    // append initial data
+    self.insertToDOM(rows, cache);
+
+    // restore the scroll position
+    self.scroll_elem.scrollTop = scroll_top;
+
+    // adding scroll handler
+    var last_cluster = false,
+    scroll_debounce = 0,
+    pointer_events_set = false,
+    scrollEv = function() {
+      // fixes scrolling issue on Mac #3
+      if (is_mac) {
+          if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
+          pointer_events_set = true;
+          clearTimeout(scroll_debounce);
+          scroll_debounce = setTimeout(function () {
+              self.content_elem.style.pointerEvents = 'auto';
+              pointer_events_set = false;
+          }, 50);
+      }
+      if (last_cluster != (last_cluster = self.getClusterNum()))
+        self.insertToDOM(rows, cache);
+      if (self.options.callbacks.scrollingProgress)
+        self.options.callbacks.scrollingProgress(self.getScrollProgress());
+    },
+    resize_debounce = 0,
+    resizeEv = function() {
+      clearTimeout(resize_debounce);
+      resize_debounce = setTimeout(self.refresh, 100);
+    }
+    on('scroll', self.scroll_elem, scrollEv);
+    on('resize', window, resizeEv);
+
+    // public methods
+    self.destroy = function(clean) {
+      off('scroll', self.scroll_elem, scrollEv);
+      off('resize', window, resizeEv);
+      self.html((clean ? self.generateEmptyRow() : rows).join(''));
+    }
+    self.refresh = function(force) {
+      if(self.getRowsHeight(rows) || force) self.update(rows);
+    }
+    self.update = function(new_rows) {
+      rows = isArray(new_rows)
+        ? new_rows
+        : [];
+      var scroll_top = self.scroll_elem.scrollTop;
+      // fixes #39
+      if(rows.length * self.options.item_height < scroll_top) {
+        self.scroll_elem.scrollTop = 0;
+        last_cluster = 0;
+      }
+      self.insertToDOM(rows, cache);
+      self.scroll_elem.scrollTop = scroll_top;
+    }
+    self.clear = function() {
+      self.update([]);
+    }
+    self.getRowsAmount = function() {
+      return rows.length;
+    }
+    self.getScrollProgress = function() {
+      return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
+    }
+
+    var add = function(where, _new_rows) {
+      var new_rows = isArray(_new_rows)
+        ? _new_rows
+        : [];
+      if( ! new_rows.length) return;
+      rows = where == 'append'
+        ? rows.concat(new_rows)
+        : new_rows.concat(rows);
+      self.insertToDOM(rows, cache);
+    }
+    self.append = function(rows) {
+      add('append', rows);
+    }
+    self.prepend = function(rows) {
+      add('prepend', rows);
+    }
+  }
+
+  Clusterize.prototype = {
+    constructor: Clusterize,
+    // fetch existing markup
+    fetchMarkup: function() {
+      var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
+      while (rows_nodes.length) {
+        rows.push(rows_nodes.shift().outerHTML);
+      }
+      return rows;
+    },
+    // get tag name, content tag name, tag height, calc cluster height
+    exploreEnvironment: function(rows, cache) {
+      var opts = this.options;
+      opts.content_tag = this.content_elem.tagName.toLowerCase();
+      if( ! rows.length) return;
+      if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
+      if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);
+      if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
+      this.getRowsHeight(rows);
+    },
+    getRowsHeight: function(rows) {
+      var opts = this.options,
+        prev_item_height = opts.item_height;
+      opts.cluster_height = 0;
+      if( ! rows.length) return;
+      var nodes = this.content_elem.children;
+      var node = nodes[Math.floor(nodes.length / 2)];
+      opts.item_height = node.offsetHeight;
+      // consider table's border-spacing
+      if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
+        opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
+      // consider margins (and margins collapsing)
+      if(opts.tag != 'tr') {
+        var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
+        var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
+        opts.item_height += Math.max(marginTop, marginBottom);
+      }
+      opts.block_height = opts.item_height * opts.rows_in_block;
+      opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
+      opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
+      return prev_item_height != opts.item_height;
+    },
+    // get current cluster number
+    getClusterNum: function () {
+      this.options.scroll_top = this.scroll_elem.scrollTop;
+      return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0;
+    },
+    // generate empty row if no data provided
+    generateEmptyRow: function() {
+      var opts = this.options;
+      if( ! opts.tag || ! opts.show_no_data_row) return [];
+      var empty_row = document.createElement(opts.tag),
+        no_data_content = document.createTextNode(opts.no_data_text), td;
+      empty_row.className = opts.no_data_class;
+      if(opts.tag == 'tr') {
+        td = document.createElement('td');
+        // fixes #53
+        td.colSpan = 100;
+        td.appendChild(no_data_content);
+      }
+      empty_row.appendChild(td || no_data_content);
+      return [empty_row.outerHTML];
+    },
+    // generate cluster for current scroll position
+    generate: function (rows, cluster_num) {
+      var opts = this.options,
+        rows_len = rows.length;
+      if (rows_len < opts.rows_in_block) {
+        return {
+          top_offset: 0,
+          bottom_offset: 0,
+          rows_above: 0,
+          rows: rows_len ? rows : this.generateEmptyRow()
+        }
+      }
+      var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0),
+        items_end = items_start + opts.rows_in_cluster,
+        top_offset = Math.max(items_start * opts.item_height, 0),
+        bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
+        this_cluster_rows = [],
+        rows_above = items_start;
+      if(top_offset < 1) {
+        rows_above++;
+      }
+      for (var i = items_start; i < items_end; i++) {
+        rows[i] && this_cluster_rows.push(rows[i]);
+      }
+      return {
+        top_offset: top_offset,
+        bottom_offset: bottom_offset,
+        rows_above: rows_above,
+        rows: this_cluster_rows
+      }
+    },
+    renderExtraTag: function(class_name, height) {
+      var tag = document.createElement(this.options.tag),
+        clusterize_prefix = 'clusterize-';
+      tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
+      height && (tag.style.height = height + 'px');
+      return tag.outerHTML;
+    },
+    // if necessary verify data changed and insert to DOM
+    insertToDOM: function(rows, cache) {
+      // explore row's height
+      if( ! this.options.cluster_height) {
+        this.exploreEnvironment(rows, cache);
+      }
+      var data = this.generate(rows, this.getClusterNum()),
+        this_cluster_rows = data.rows.join(''),
+        this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
+        top_offset_changed = this.checkChanges('top', data.top_offset, cache),
+        only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
+        callbacks = this.options.callbacks,
+        layout = [];
+
+      if(this_cluster_content_changed || top_offset_changed) {
+        if(data.top_offset) {
+          this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
+          layout.push(this.renderExtraTag('top-space', data.top_offset));
+        }
+        layout.push(this_cluster_rows);
+        data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
+        callbacks.clusterWillChange && callbacks.clusterWillChange();
+        this.html(layout.join(''));
+        this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
+        callbacks.clusterChanged && callbacks.clusterChanged();
+      } else if(only_bottom_offset_changed) {
+        this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
+      }
+    },
+    // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
+    html: function(data) {
+      var content_elem = this.content_elem;
+      if(ie && ie <= 9 && this.options.tag == 'tr') {
+        var div = document.createElement('div'), last;
+        div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
+        while((last = content_elem.lastChild)) {
+          content_elem.removeChild(last);
+        }
+        var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
+        while (rows_nodes.length) {
+          content_elem.appendChild(rows_nodes.shift());
+        }
+      } else {
+        content_elem.innerHTML = data;
+      }
+    },
+    getChildNodes: function(tag) {
+        var child_nodes = tag.children, nodes = [];
+        for (var i = 0, ii = child_nodes.length; i < ii; i++) {
+            nodes.push(child_nodes[i]);
+        }
+        return nodes;
+    },
+    checkChanges: function(type, value, cache) {
+      var changed = value != cache[type];
+      cache[type] = value;
+      return changed;
+    }
+  }
+
+  // support functions
+  function on(evt, element, fnc) {
+    return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
+  }
+  function off(evt, element, fnc) {
+    return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
+  }
+  function isArray(arr) {
+    return Object.prototype.toString.call(arr) === '[object Array]';
+  }
+  function getStyle(prop, elem) {
+    return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
+  }
+
+  return Clusterize;
+}));
\ No newline at end of file
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 08630e5..3e2414e 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -1,15 +1,16 @@
 
 erpnext.SerialNoBatchSelector = Class.extend({
-	init: function(opts) {
+	init: function(opts, show_dialog) {
 		$.extend(this, opts);
+		this.show_dialog = show_dialog;
 		// frm, item, warehouse_details, has_batch, oldest
 		let d = this.item;
 
 		// Don't show dialog if batch no or serial no already set
-		if(d && d.has_batch_no && !d.batch_no) {
+		if(d && d.has_batch_no && (!d.batch_no || this.show_dialog)) {
 			this.has_batch = 1;
 			this.setup();
-		} else if(d && d.has_serial_no && !d.serial_no) {
+		} else if(d && d.has_serial_no && (!d.serial_no || this.show_dialog)) {
 			this.has_batch = 0;
 			this.setup();
 		}
@@ -93,6 +94,11 @@
 			}
 		});
 
+		if(this.show_dialog) {
+			let d = this.item;
+			this.dialog.set_value('serial_no', d.serial_no);
+		}
+
 		this.dialog.show();
 	},
 
@@ -140,6 +146,7 @@
 			this.map_row_values(this.item, this.values, 'serial_no', 'qty');
 		}
 		refresh_field("items");
+		this.callback && this.callback(this.item);
 	},
 
 	map_row_values: function(row, values, number, qty_field, warehouse) {
diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less
index 6c616c9..de46c53 100644
--- a/erpnext/public/less/erpnext.less
+++ b/erpnext/public/less/erpnext.less
@@ -364,17 +364,6 @@
 		.pos-item-wrapper {
 			position: relative;
 		}
-
-		.price-info {
-			position: absolute;
-			left: 0;
-			bottom: 0;
-			margin: 0 0 15px 15px;
-			background-color: rgba(141, 153, 166, 0.6);
-			padding: 5px 9px;
-			border-radius: 3px;
-			color: #fff;
-		}
 	}
 
 	.pos-bill-toolbar {
@@ -423,4 +412,15 @@
 	.collapse-btn {
 		cursor: pointer;
 	}
+}
+
+.price-info {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	margin: 0 0 15px 15px;
+	background-color: rgba(141, 153, 166, 0.6);
+	padding: 5px 9px;
+	border-radius: 3px;
+	color: #fff;
 }
\ No newline at end of file
diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less
new file mode 100644
index 0000000..9653a82
--- /dev/null
+++ b/erpnext/public/less/pos.less
@@ -0,0 +1,222 @@
+@import "../../../../frappe/frappe/public/less/variables.less";
+
+[data-route="point-of-sale"] {
+	.layout-main-section-wrapper {
+		margin-bottom: 0;
+	}
+
+	.pos-items-wrapper {
+		max-height: ~"calc(100vh - 210px)";
+	}
+}
+
+.pos {
+	// display: flex;
+	padding: 15px;
+}
+
+.list-item {
+	min-height: 40px;
+	height: auto;
+}
+
+.cart-container {
+	padding: 0 15px;
+	// flex: 2;
+	display: inline-block;
+	width: 39%;
+	vertical-align: top;
+}
+
+.item-container {
+	padding: 0 15px;
+	// flex: 3;
+	display: inline-block;
+	width: 60%;
+	vertical-align: top;
+}
+
+.search-field {
+	width: 60%;
+
+	input::placeholder {
+		font-size: @text-medium;
+	}
+}
+
+.item-group-field {
+	width: 40%;
+	margin-left: 15px;
+}
+
+.cart-wrapper {
+	margin-bottom: 10px;
+	.list-item__content:not(:first-child) {
+		justify-content: flex-end;
+	}
+
+	.list-item--head .list-item__content:nth-child(2) {
+		flex: 1.5;
+	}
+}
+
+.cart-items {
+	height: 150px;
+	overflow: auto;
+
+	.list-item.current-item {
+		background-color: @light-yellow;
+	}
+
+	.list-item.current-item.qty input {
+		border: 1px solid @brand-primary;
+		font-weight: bold;
+	}
+
+	.list-item.current-item.disc .discount {
+		font-weight: bold;
+	}
+
+	.list-item.current-item.rate .rate {
+		font-weight: bold;
+	}
+
+	.list-item .quantity {
+		flex: 1.5;
+	}
+
+	input {
+		text-align: right;
+		height: 22px;
+		font-size: @text-medium;
+	}
+}
+
+.fields {
+	display: flex;
+}
+
+.pos-items-wrapper {
+	max-height: 480px;
+	overflow-y: auto;
+}
+
+.pos-items {
+	overflow: hidden;
+}
+
+.pos-item-wrapper {
+	display: flex;
+	flex-direction: column;
+	position: relative;
+	width: 25%;
+}
+
+.image-view-container {
+	display: block;
+}
+
+.image-view-container .image-field {
+	height: auto;
+}
+
+.empty-state {
+	height: 100%;
+	position: relative;
+
+	span {
+		position: absolute;
+		color: @text-muted;
+		font-size: @text-medium;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+	}
+}
+
+@keyframes yellow-fade {
+	0% {background-color: @light-yellow;}
+	100% {background-color: transparent;}
+}
+
+.highlight {
+	animation: yellow-fade 1s ease-in 1;
+}
+
+input[type=number]::-webkit-inner-spin-button,
+input[type=number]::-webkit-outer-spin-button {
+	-webkit-appearance: none;
+	margin: 0;
+}
+
+// number pad
+
+.number-pad {
+	border-collapse: collapse;
+	cursor: pointer;
+	display: table;
+	margin: auto;
+}
+.num-row {
+	display: table-row;
+}
+.num-col {
+	display: table-cell;
+	border: 1px solid @border-color;
+
+	& > div {
+		width: 50px;
+		height: 50px;
+		text-align: center;
+		line-height: 50px;
+	}
+
+	&.active {
+		background-color: @light-yellow;
+	}
+
+	&.brand-primary {
+		background-color: @brand-primary;
+		color: #ffffff;
+	}
+}
+
+// taxes, totals and discount area
+.discount-amount {
+	.discount-inputs {
+		display: flex;
+		flex-direction: column;
+		padding: 15px 0;
+	}
+
+	input:first-child {
+		margin-bottom: 10px;
+	}
+}
+
+.taxes-and-totals {
+	border-top: 1px solid @border-color;
+
+	.taxes {
+		display: flex;
+		flex-direction: column;
+		padding: 15px 0;
+		align-items: flex-end;
+
+		& > div:first-child {
+			margin-bottom: 10px;
+		}
+	}
+}
+
+.grand-total {
+	border-top: 1px solid @border-color;
+
+	.list-item {
+		height: 60px;
+	}
+
+	.grand-total-value {
+		font-size: 24px;
+	}
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/__init__.py b/erpnext/selling/page/point_of_sale/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/__init__.py
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
new file mode 100644
index 0000000..9cd2a49
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -0,0 +1,1284 @@
+/* global Clusterize */
+frappe.provide('erpnext.pos');
+
+frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
+	frappe.ui.make_app_page({
+		parent: wrapper,
+		title: 'Point of Sale',
+		single_column: true
+	});
+
+	frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => {
+		if (r && r.is_online && cint(r.is_online)) {
+			// online
+			wrapper.pos = new erpnext.pos.PointOfSale(wrapper);
+			window.cur_pos = wrapper.pos;
+		} else {
+			// offline
+			frappe.set_route('pos');
+		}
+	});
+};
+
+erpnext.pos.PointOfSale = class PointOfSale {
+	constructor(wrapper) {
+		this.wrapper = $(wrapper).find('.layout-main-section');
+		this.page = wrapper.page;
+
+		const assets = [
+			'assets/erpnext/js/pos/clusterize.js',
+			'assets/erpnext/css/pos.css'
+		];
+
+		frappe.require(assets, () => {
+			this.make();
+		});
+	}
+
+	make() {
+		return frappe.run_serially([
+			() => {
+				this.prepare_dom();
+				this.prepare_menu();
+				this.set_online_status();
+			},
+			() => this.setup_pos_profile(),
+			() => {
+				this.make_items();
+				this.bind_events();
+			},
+			() => this.make_new_invoice(),
+			() => this.page.set_title(__('Point of Sale'))
+		]);
+	}
+
+	set_online_status() {
+		this.connection_status = false;
+		this.page.set_indicator(__("Offline"), "grey");
+		frappe.call({
+			method: "frappe.handler.ping",
+			callback: r => {
+				if (r.message) {
+					this.connection_status = true;
+					this.page.set_indicator(__("Online"), "green");
+				}
+			}
+		});
+	}
+
+	prepare_dom() {
+		this.wrapper.append(`
+			<div class="pos">
+				<section class="cart-container">
+
+				</section>
+				<section class="item-container">
+
+				</section>
+			</div>
+		`);
+	}
+
+	make_cart() {
+		this.cart = new POSCart({
+			frm: this.frm,
+			wrapper: this.wrapper.find('.cart-container'),
+			events: {
+				on_customer_change: (customer) => this.frm.set_value('customer', customer),
+				on_field_change: (item_code, field, value) => {
+					this.update_item_in_cart(item_code, field, value);
+				},
+				on_numpad: (value) => {
+					if (value == 'Pay') {
+						if (!this.payment) {
+							this.make_payment_modal();
+						}
+						this.payment.open_modal();
+					}
+				},
+				on_select_change: () => {
+					this.cart.numpad.set_inactive();
+				}
+			}
+		});
+	}
+
+	toggle_editing(flag) {
+		let disabled;
+		if (flag !== undefined) {
+			disabled = !flag;
+		} else {
+			disabled = this.frm.doc.docstatus == 1 ? true: false;
+		}
+		const pointer_events = disabled ? 'none' : 'inherit';
+
+		this.wrapper.find('input, button, select').prop("disabled", disabled);
+		this.wrapper.find('.number-pad-container').toggleClass("hide", disabled);
+
+		this.wrapper.find('.cart-container').css('pointer-events', pointer_events);
+		this.wrapper.find('.item-container').css('pointer-events', pointer_events);
+
+		this.page.clear_actions();
+	}
+
+	make_items() {
+		this.items = new POSItems({
+			wrapper: this.wrapper.find('.item-container'),
+			pos_profile: this.pos_profile,
+			events: {
+				update_cart: (item, field, value) => {
+					if(!this.frm.doc.customer) {
+						frappe.throw(__('Please select a customer'));
+					}
+					this.update_item_in_cart(item, field, value);
+					this.cart && this.cart.unselect_all();
+				}
+			}
+		});
+	}
+
+	update_item_in_cart(item_code, field='qty', value=1) {
+		if(this.cart.exists(item_code)) {
+			const item = this.frm.doc.items.find(i => i.item_code === item_code);
+			frappe.flags.hide_serial_batch_dialog = false;
+
+			if (typeof value === 'string' && !in_list(['serial_no', 'batch_no'], field)) {
+				// value can be of type '+1' or '-1'
+				value = item[field] + flt(value);
+			}
+
+			if(field === 'serial_no') {
+				value = item.serial_no + '\n'+ value;
+			}
+
+			if(field === 'qty' && (item.serial_no || item.batch_no)) {
+				this.select_batch_and_serial_no(item);
+			} else {
+				this.update_item_in_frm(item, field, value)
+					.then(() => {
+						// update cart
+						this.update_cart_data(item);
+					});
+			}
+			return;
+		}
+
+		let args = { item_code: item_code };
+		if (in_list(['serial_no', 'batch_no'], field)) {
+			args[field] = value;
+		}
+
+		// add to cur_frm
+		const item = this.frm.add_child('items', args);
+		frappe.flags.hide_serial_batch_dialog = true;
+		this.frm.script_manager
+			.trigger('item_code', item.doctype, item.name)
+			.then(() => {
+				const show_dialog = item.has_serial_no || item.has_batch_no;
+				if (show_dialog && field == 'qty') {
+					// check has serial no/batch no and update cart
+					this.select_batch_and_serial_no(item);
+				} else {
+					// update cart
+					this.update_cart_data(item);
+				}
+			});
+	}
+
+	select_batch_and_serial_no(item) {
+		erpnext.show_serial_batch_selector(this.frm, item, () => {
+			this.update_item_in_frm(item)
+				.then(() => {
+					// update cart
+					this.update_cart_data(item);
+				});
+		}, true);
+	}
+
+	update_cart_data(item) {
+		this.cart.add_item(item);
+		this.cart.update_taxes_and_totals();
+		this.cart.update_grand_total();
+	}
+
+	update_item_in_frm(item, field, value) {
+		if (field) {
+			frappe.model.set_value(item.doctype, item.name, field, value);
+		}
+
+		return this.frm.script_manager
+			.trigger('qty', item.doctype, item.name)
+			.then(() => {
+				if (field === 'qty' && value === 0) {
+					frappe.model.clear_doc(item.doctype, item.name);
+				}
+			});
+	}
+
+	make_payment_modal() {
+		this.payment = new Payment({
+			frm: this.frm,
+			events: {
+				submit_form: () => {
+					this.submit_sales_invoice();
+				}
+			}
+		});
+	}
+
+	submit_sales_invoice() {
+
+		frappe.confirm(__("Permanently Submit {0}?", [this.frm.doc.name]), () => {
+			frappe.call({
+				method: 'erpnext.selling.page.point_of_sale.point_of_sale.submit_invoice',
+				freeze: true,
+				args: {
+					doc: this.frm.doc
+				}
+			}).then(r => {
+				if(r.message) {
+					this.frm.doc = r.message;
+					frappe.show_alert({
+						indicator: 'green',
+						message: __(`Sales invoice ${r.message.name} created succesfully`)
+					});
+
+					this.toggle_editing();
+					this.set_form_action();
+				}
+			});
+		});
+	}
+
+	bind_events() {
+
+	}
+
+	setup_pos_profile() {
+		return frappe.call({
+			method: 'erpnext.stock.get_item_details.get_pos_profile',
+			args: {
+				company: frappe.sys_defaults.company
+			}
+		}).then(r => {
+			this.pos_profile = r.message;
+
+			if (!this.pos_profile) {
+				this.pos_profile = {
+					currency: frappe.defaults.get_default('currency'),
+					selling_price_list: frappe.defaults.get_default('selling_price_list')
+				};
+			}
+		});
+	}
+
+	make_new_invoice() {
+		return frappe.run_serially([
+			() => this.make_sales_invoice_frm(),
+			() => {
+				if (this.cart) {
+					this.cart.frm = this.frm;
+					this.cart.reset();
+				} else {
+					this.make_cart();
+				}
+				this.toggle_editing(true);
+			}
+		]);
+	}
+
+	make_sales_invoice_frm() {
+		const doctype = 'Sales Invoice';
+		return new Promise(resolve => {
+			if (this.frm) {
+				this.frm = get_frm(this.frm);
+				resolve();
+			} else {
+				frappe.model.with_doctype(doctype, () => {
+					this.frm = get_frm();
+					resolve();
+				});
+			}
+		});
+
+		function get_frm(_frm) {
+			const page = $('<div>');
+			const frm = _frm || new _f.Frm(doctype, page, false);
+			const name = frappe.model.make_new_doc_and_get_name(doctype, true);
+			frm.refresh(name);
+			frm.doc.items = [];
+			frm.set_value('is_pos', 1);
+			frm.meta.default_print_format = 'POS Invoice';
+			return frm;
+		}
+	}
+
+	prepare_menu() {
+		var me = this;
+		this.page.clear_menu();
+
+		// for mobile
+		// this.page.add_menu_item(__("Pay"), function () {
+		//
+		// }).addClass('visible-xs');
+
+		this.page.add_menu_item(__("Form View"), function () {
+			frappe.model.sync(me.frm.doc);
+			frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name);
+		});
+
+		this.page.add_menu_item(__("POS Profile"), function () {
+			frappe.set_route('List', 'POS Profile');
+		});
+
+		this.page.add_menu_item(__('POS Settings'), function() {
+			frappe.set_route('Form', 'POS Settings');
+		});
+	}
+
+	set_form_action() {
+		if(this.frm.doc.docstatus !== 1) return;
+
+		this.page.set_secondary_action(__("Print"), () => {
+			if (this.pos_profile && this.pos_profile.print_format_for_online) {
+				this.frm.meta.default_print_format = this.pos_profile.print_format_for_online;
+			}
+			this.frm.print_preview.printit(true);
+		});
+
+		this.page.set_primary_action(__("New"), () => {
+			this.make_new_invoice();
+		});
+
+		this.page.add_menu_item(__("Email"), () => {
+			this.frm.email_doc();
+		});
+	}
+};
+
+class POSCart {
+	constructor({frm, wrapper, events}) {
+		this.frm = frm;
+		this.wrapper = wrapper;
+		this.events = events;
+		this.make();
+		this.bind_events();
+	}
+
+	make() {
+		this.make_dom();
+		this.make_customer_field();
+		this.make_numpad();
+	}
+
+	make_dom() {
+		this.wrapper.append(`
+			<div class="pos-cart">
+				<div class="customer-field">
+				</div>
+				<div class="cart-wrapper">
+					<div class="list-item-table">
+						<div class="list-item list-item--head">
+							<div class="list-item__content list-item__content--flex-1.5 text-muted">${__('Item Name')}</div>
+							<div class="list-item__content text-muted text-right">${__('Quantity')}</div>
+							<div class="list-item__content text-muted text-right">${__('Discount')}</div>
+							<div class="list-item__content text-muted text-right">${__('Rate')}</div>
+						</div>
+						<div class="cart-items">
+							<div class="empty-state">
+								<span>No Items added to cart</span>
+							</div>
+						</div>
+						<div class="taxes-and-totals">
+							${this.get_taxes_and_totals()}
+						</div>
+						<div class="discount-amount">
+							${this.get_discount_amount()}
+						</div>
+						<div class="grand-total">
+							${this.get_grand_total()}
+						</div>
+					</div>
+				</div>
+				<div class="number-pad-container">
+				</div>
+			</div>
+		`);
+		this.$cart_items = this.wrapper.find('.cart-items');
+		this.$empty_state = this.wrapper.find('.cart-items .empty-state');
+		this.$taxes_and_totals = this.wrapper.find('.taxes-and-totals');
+		this.$discount_amount = this.wrapper.find('.discount-amount');
+		this.$grand_total = this.wrapper.find('.grand-total');
+
+		this.toggle_taxes_and_totals(false);
+		this.$grand_total.on('click', () => {
+			this.toggle_taxes_and_totals();
+		});
+	}
+
+	reset() {
+		this.$cart_items.find('.list-item').remove();
+		this.$empty_state.show();
+		this.$taxes_and_totals.html(this.get_taxes_and_totals());
+		this.numpad && this.numpad.reset_value();
+		this.customer_field.set_value("");
+	}
+
+	get_grand_total() {
+		return `
+			<div class="list-item">
+				<div class="list-item__content text-muted">${__('Grand Total')}</div>
+				<div class="list-item__content list-item__content--flex-2 grand-total-value">0.00</div>
+			</div>
+		`;
+	}
+
+	get_discount_amount() {
+		const get_currency_symbol = window.get_currency_symbol;
+
+		return `
+			<div class="list-item">
+				<div class="list-item__content list-item__content--flex-2 text-muted">${__('Discount')}</div>
+				<div class="list-item__content discount-inputs">
+					<input type="text"
+						class="form-control additional_discount_percentage text-right"
+						placeholder="% 0.00"
+					>
+					<input type="text"
+						class="form-control discount_amount text-right"
+						placeholder="${get_currency_symbol(this.frm.doc.currency)} 0.00"
+					>
+				</div>
+			</div>
+		`;
+	}
+
+	get_taxes_and_totals() {
+		return `
+			<div class="list-item">
+				<div class="list-item__content list-item__content--flex-2 text-muted">${__('Net Total')}</div>
+				<div class="list-item__content net-total">0.00</div>
+			</div>
+			<div class="list-item">
+				<div class="list-item__content list-item__content--flex-2 text-muted">${__('Taxes')}</div>
+				<div class="list-item__content taxes">0.00</div>
+			</div>
+		`;
+	}
+
+	toggle_taxes_and_totals(flag) {
+		if (flag !== undefined) {
+			this.tax_area_is_shown = flag;
+		} else {
+			this.tax_area_is_shown = !this.tax_area_is_shown;
+		}
+
+		this.$taxes_and_totals.toggle(this.tax_area_is_shown);
+		this.$discount_amount.toggle(this.tax_area_is_shown);
+	}
+
+	update_taxes_and_totals() {
+		const currency = this.frm.doc.currency;
+		this.frm.refresh_field('taxes');
+
+		// Update totals
+		this.$taxes_and_totals.find('.net-total')
+			.html(format_currency(this.frm.doc.net_total, currency));
+
+		// Update taxes
+		const taxes_html = this.frm.doc.taxes.map(tax => {
+			return `
+				<div>
+					<span>${tax.description}</span>
+					<span class="text-right bold">
+						${format_currency(tax.tax_amount, currency)}
+					</span>
+				</div>
+			`;
+		}).join("");
+		this.$taxes_and_totals.find('.taxes').html(taxes_html);
+	}
+
+	update_grand_total() {
+		this.$grand_total.find('.grand-total-value').text(
+			format_currency(this.frm.doc.grand_total, this.frm.currency)
+		);
+	}
+
+	make_customer_field() {
+		this.customer_field = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Link',
+				label: 'Customer',
+				fieldname: 'customer',
+				options: 'Customer',
+				reqd: 1,
+				default: this.frm.doc.customer,
+				onchange: () => {
+					this.events.on_customer_change(this.customer_field.get_value());
+				}
+			},
+			parent: this.wrapper.find('.customer-field'),
+			render_input: true
+		});
+	}
+
+	make_numpad() {
+		this.numpad = new NumberPad({
+			button_array: [
+				[1, 2, 3, 'Qty'],
+				[4, 5, 6, 'Disc'],
+				[7, 8, 9, 'Rate'],
+				['Del', 0, '.', 'Pay']
+			],
+			add_class: {
+				'Pay': 'brand-primary'
+			},
+			disable_highlight: ['Qty', 'Disc', 'Rate', 'Pay'],
+			reset_btns: ['Qty', 'Disc', 'Rate', 'Pay'],
+			del_btn: 'Del',
+			wrapper: this.wrapper.find('.number-pad-container'),
+			onclick: (btn_value) => {
+				// on click
+				if (!this.selected_item && btn_value !== 'Pay') {
+					frappe.show_alert({
+						indicator: 'red',
+						message: __('Please select an item in the cart')
+					});
+					return;
+				}
+				if (['Qty', 'Disc', 'Rate'].includes(btn_value)) {
+					this.set_input_active(btn_value);
+				} else if (btn_value !== 'Pay') {
+					if (!this.selected_item.active_field) {
+						frappe.show_alert({
+							indicator: 'red',
+							message: __('Please select a field to edit from numpad')
+						});
+						return;
+					}
+
+					const item_code = this.selected_item.attr('data-item-code');
+					const field = this.selected_item.active_field;
+					const value = this.numpad.get_value();
+
+					this.events.on_field_change(item_code, field, value);
+				}
+
+				this.events.on_numpad(btn_value);
+			}
+		});
+	}
+
+	set_input_active(btn_value) {
+		this.selected_item.removeClass('qty disc rate');
+
+		this.numpad.set_active(btn_value);
+		if (btn_value === 'Qty') {
+			this.selected_item.addClass('qty');
+			this.selected_item.active_field = 'qty';
+		} else if (btn_value == 'Disc') {
+			this.selected_item.addClass('disc');
+			this.selected_item.active_field = 'discount_percentage';
+		} else if (btn_value == 'Rate') {
+			this.selected_item.addClass('rate');
+			this.selected_item.active_field = 'rate';
+		}
+	}
+
+	add_item(item) {
+		this.$empty_state.hide();
+
+		if (this.exists(item.item_code)) {
+			// update quantity
+			this.update_item(item);
+		} else {
+			// add to cart
+			const $item = $(this.get_item_html(item));
+			$item.appendTo(this.$cart_items);
+		}
+		this.highlight_item(item.item_code);
+		this.scroll_to_item(item.item_code);
+	}
+
+	update_item(item) {
+		const $item = this.$cart_items.find(`[data-item-code="${item.item_code}"]`);
+
+		if(item.qty > 0) {
+			const indicator_class = item.actual_qty >= item.qty ? 'green' : 'red';
+			const remove_class = indicator_class == 'green' ? 'red' : 'green';
+
+			$item.find('.quantity input').val(item.qty);
+			$item.find('.discount').text(item.discount_percentage + '%');
+			$item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency));
+			$item.addClass(indicator_class);
+			$item.removeClass(remove_class);
+		} else {
+			$item.remove();
+		}
+	}
+
+	get_item_html(item) {
+		const rate = format_currency(item.rate, this.frm.doc.currency);
+		const indicator_class = item.actual_qty >= item.qty ? 'green' : 'red';
+		return `
+			<div class="list-item indicator ${indicator_class}" data-item-code="${item.item_code}" title="Item: ${item.item_name}  Available Qty: ${item.actual_qty}">
+				<div class="item-name list-item__content list-item__content--flex-1.5 ellipsis">
+					${item.item_name}
+				</div>
+				<div class="quantity list-item__content text-right">
+					${get_quantity_html(item.qty)}
+				</div>
+				<div class="discount list-item__content text-right">
+					${item.discount_percentage}%
+				</div>
+				<div class="rate list-item__content text-right">
+					${rate}
+				</div>
+			</div>
+		`;
+
+		function get_quantity_html(value) {
+			return `
+				<div class="input-group input-group-xs">
+					<span class="input-group-btn">
+						<button class="btn btn-default btn-xs" data-action="increment">+</button>
+					</span>
+
+					<input class="form-control" type="number" value="${value}">
+
+					<span class="input-group-btn">
+						<button class="btn btn-default btn-xs" data-action="decrement">-</button>
+					</span>
+				</div>
+			`;
+		}
+	}
+
+	exists(item_code) {
+		let $item = this.$cart_items.find(`[data-item-code="${item_code}"]`);
+		return $item.length > 0;
+	}
+
+	highlight_item(item_code) {
+		const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`);
+		$item.addClass('highlight');
+		setTimeout(() => $item.removeClass('highlight'), 1000);
+	}
+
+	scroll_to_item(item_code) {
+		const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`);
+		if ($item.length === 0) return;
+		const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop();
+		this.$cart_items.animate({ scrollTop });
+	}
+
+	bind_events() {
+		const me = this;
+		const events = this.events;
+
+		// quantity change
+		this.$cart_items.on('click',
+			'[data-action="increment"], [data-action="decrement"]', function() {
+				const $btn = $(this);
+				const $item = $btn.closest('.list-item[data-item-code]');
+				const item_code = $item.attr('data-item-code');
+				const action = $btn.attr('data-action');
+
+				if(action === 'increment') {
+					events.on_field_change(item_code, 'qty', '+1');
+				} else if(action === 'decrement') {
+					events.on_field_change(item_code, 'qty', '-1');
+				}
+			});
+
+		// this.$cart_items.on('focus', '.quantity input', function(e) {
+		// 	const $input = $(this);
+		// 	const $item = $input.closest('.list-item[data-item-code]');
+		// 	me.set_selected_item($item);
+		// 	me.set_input_active('Qty');
+		// 	e.preventDefault();
+		// 	e.stopPropagation();
+		// 	return false;
+		// });
+
+		this.$cart_items.on('change', '.quantity input', function() {
+			const $input = $(this);
+			const $item = $input.closest('.list-item[data-item-code]');
+			const item_code = $item.attr('data-item-code');
+			events.on_field_change(item_code, 'qty', flt($input.val()));
+		});
+
+		// current item
+		this.$cart_items.on('click', '.list-item', function() {
+			me.set_selected_item($(this));
+		});
+
+		// disable current item
+		// $('body').on('click', function(e) {
+		// 	console.log(e);
+		// 	if($(e.target).is('.list-item')) {
+		// 		return;
+		// 	}
+		// 	me.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
+		// 	me.selected_item = null;
+		// });
+
+		this.wrapper.find('.additional_discount_percentage').on('change', (e) => {
+			frappe.model.set_value(this.frm.doctype, this.frm.docname,
+				'additional_discount_percentage', e.target.value)
+				.then(() => {
+					let discount_wrapper = this.wrapper.find('.discount_amount');
+					discount_wrapper.val(this.frm.doc.discount_amount);
+					discount_wrapper.trigger('change');
+				});
+		});
+
+		this.wrapper.find('.discount_amount').on('change', (e) => {
+			frappe.model.set_value(this.frm.doctype, this.frm.docname,
+				'discount_amount', e.target.value);
+			this.frm.trigger('discount_amount')
+				.then(() => {
+					let discount_wrapper = this.wrapper.find('.additional_discount_percentage');
+					discount_wrapper.val(this.frm.doc.additional_discount_percentage);
+					this.update_taxes_and_totals();
+					this.update_grand_total();
+				});
+		});
+	}
+
+	set_selected_item($item) {
+		this.selected_item = $item;
+		this.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
+		this.selected_item.addClass('current-item');
+		this.events.on_select_change();
+	}
+
+	unselect_all() {
+		this.$cart_items.find('.list-item').removeClass('current-item qty disc rate');
+		this.selected_item = null;
+		this.events.on_select_change();
+	}
+}
+
+class POSItems {
+	constructor({wrapper, pos_profile, events}) {
+		this.wrapper = wrapper;
+		this.pos_profile = pos_profile;
+		this.items = {};
+		this.events = events;
+		this.currency = this.pos_profile.currency;
+
+		this.make_dom();
+		this.make_fields();
+
+		this.init_clusterize();
+		this.bind_events();
+
+		// bootstrap with 20 items
+		this.get_items()
+			.then(({ items }) => {
+				this.all_items = items;
+				this.items = items;
+				this.render_items(items);
+			});
+	}
+
+	make_dom() {
+		this.wrapper.html(`
+			<div class="fields">
+				<div class="search-field">
+				</div>
+				<div class="item-group-field">
+				</div>
+			</div>
+			<div class="items-wrapper">
+			</div>
+		`);
+
+		this.items_wrapper = this.wrapper.find('.items-wrapper');
+		this.items_wrapper.append(`
+			<div class="list-item-table pos-items-wrapper">
+				<div class="pos-items image-view-container">
+				</div>
+			</div>
+		`);
+	}
+
+	make_fields() {
+		// Search field
+		this.search_field = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Data',
+				label: 'Search Item (Ctrl + I)',
+				placeholder: 'Search by item code, serial number, batch no or barcode'
+			},
+			parent: this.wrapper.find('.search-field'),
+			render_input: true,
+		});
+
+		frappe.ui.keys.on('ctrl+i', () => {
+			this.search_field.set_focus();
+		});
+
+		this.search_field.$input.on('input', (e) => {
+			clearTimeout(this.last_search);
+			this.last_search = setTimeout(() => {
+				const search_term = e.target.value;
+				this.filter_items({ search_term });
+			}, 300);
+		});
+
+		this.item_group_field = frappe.ui.form.make_control({
+			df: {
+				fieldtype: 'Link',
+				label: 'Item Group',
+				options: 'Item Group',
+				default: 'All Item Groups',
+				onchange: () => {
+					const item_group = this.item_group_field.get_value();
+					if (item_group) {
+						this.filter_items({ item_group: item_group });
+					}
+				},
+			},
+			parent: this.wrapper.find('.item-group-field'),
+			render_input: true
+		});
+	}
+
+	init_clusterize() {
+		this.clusterize = new Clusterize({
+			scrollElem: this.wrapper.find('.pos-items-wrapper')[0],
+			contentElem: this.wrapper.find('.pos-items')[0],
+			rows_in_block: 6
+		});
+	}
+
+	render_items(items) {
+		let _items = items || this.items;
+
+		const all_items = Object.values(_items).map(item => this.get_item_html(item));
+		let row_items = [];
+
+		const row_container = '<div style="display: flex; border-bottom: 1px solid #ebeff2">';
+		let curr_row = row_container;
+
+		for (let i=0; i < all_items.length; i++) {
+			// wrap 4 items in a div to emulate
+			// a row for clusterize
+			if(i % 4 === 0 && i !== 0) {
+				curr_row += '</div>';
+				row_items.push(curr_row);
+				curr_row = row_container;
+			}
+			curr_row += all_items[i];
+
+			if(i == all_items.length - 1 && all_items.length % 4 !== 0) {
+				row_items.push(curr_row);
+			}
+		}
+
+		this.clusterize.update(row_items);
+	}
+
+	filter_items({ search_term='', item_group='All Item Groups' }={}) {
+		if (search_term) {
+			search_term = search_term.toLowerCase();
+
+			// memoize
+			this.search_index = this.search_index || {};
+			if (this.search_index[search_term]) {
+				const items = this.search_index[search_term];
+				this.render_items(items);
+				return;
+			}
+		} else if (item_group == "All Item Groups") {
+			return this.render_items(this.all_items);
+		}
+
+		this.get_items({search_value: search_term, item_group })
+			.then(({ items, serial_no, batch_no }) => {
+				if (search_term) {
+					this.search_index[search_term] = items;
+				}
+
+				this.render_items(items);
+				if(serial_no) {
+					this.events.update_cart(items[0].item_code,
+						'serial_no', serial_no);
+					this.search_field.set_value('');
+				}
+				if(batch_no) {
+					this.events.update_cart(items[0].item_code,
+						'batch_no', serial_no);
+					this.search_field.set_value('');
+				}
+			});
+	}
+
+	bind_events() {
+		var me = this;
+		this.wrapper.on('click', '.pos-item-wrapper', function() {
+			const $item = $(this);
+			const item_code = $item.attr('data-item-code');
+			me.events.update_cart(item_code, 'qty', '+1');
+		});
+	}
+
+	get(item_code) {
+		return this.items[item_code];
+	}
+
+	get_all() {
+		return this.items;
+	}
+
+	get_item_html(item) {
+		const price_list_rate = format_currency(item.price_list_rate, this.currency);
+		const { item_code, item_name, item_image} = item;
+		const item_title = item_name || item_code;
+
+		const template = `
+			<div class="pos-item-wrapper image-view-item" data-item-code="${item_code}">
+				<div class="image-view-header">
+					<div>
+						<a class="grey list-id" data-name="${item_code}" title="${item_title}">
+							${item_title}
+						</a>
+					</div>
+				</div>
+				<div class="image-view-body">
+					<a	data-item-code="${item_code}"
+						title="${item_title}"
+					>
+						<div class="image-field"
+							style="${!item_image ? 'background-color: #fafbfc;' : ''} border: 0px;"
+						>
+							${!item_image ? `<span class="placeholder-text">
+									${frappe.get_abbr(item_title)}
+								</span>` : '' }
+							${item_image ? `<img src="${item_image}" alt="${item_title}">` : '' }
+						</div>
+						<span class="price-info">
+							${price_list_rate}
+						</span>
+					</a>
+				</div>
+			</div>
+		`;
+
+		return template;
+	}
+
+	get_items({start = 0, page_length = 40, search_value='', item_group="All Item Groups"}={}) {
+		return new Promise(res => {
+			frappe.call({
+				method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
+				args: {
+					start,
+					page_length,
+					'price_list': this.pos_profile.selling_price_list,
+					item_group,
+					search_value
+				}
+			}).then(r => {
+				// const { items, serial_no, batch_no } = r.message;
+
+				// this.serial_no = serial_no || "";
+				res(r.message);
+			});
+		});
+	}
+}
+
+class NumberPad {
+	constructor({
+		wrapper, onclick, button_array,
+		add_class={}, disable_highlight=[],
+		reset_btns=[], del_btn='',
+	}) {
+		this.wrapper = wrapper;
+		this.onclick = onclick;
+		this.button_array = button_array;
+		this.add_class = add_class;
+		this.disable_highlight = disable_highlight;
+		this.reset_btns = reset_btns;
+		this.del_btn = del_btn;
+		this.make_dom();
+		this.bind_events();
+		this.value = '';
+	}
+
+	make_dom() {
+		if (!this.button_array) {
+			this.button_array = [
+				[1, 2, 3],
+				[4, 5, 6],
+				[7, 8, 9],
+				['', 0, '']
+			];
+		}
+
+		this.wrapper.html(`
+			<div class="number-pad">
+				${this.button_array.map(get_row).join("")}
+			</div>
+		`);
+
+		function get_row(row) {
+			return '<div class="num-row">' + row.map(get_col).join("") + '</div>';
+		}
+
+		function get_col(col) {
+			return `<div class="num-col" data-value="${col}"><div>${col}</div></div>`;
+		}
+
+		this.set_class();
+	}
+
+	set_class() {
+		for (const btn in this.add_class) {
+			const class_name = this.add_class[btn];
+			this.get_btn(btn).addClass(class_name);
+		}
+	}
+
+	bind_events() {
+		// bind click event
+		const me = this;
+		this.wrapper.on('click', '.num-col', function() {
+			const $btn = $(this);
+			const btn_value = $btn.attr('data-value');
+			if (!me.disable_highlight.includes(btn_value)) {
+				me.highlight_button($btn);
+			}
+			if (me.reset_btns.includes(btn_value)) {
+				me.reset_value();
+			} else {
+				if (btn_value === me.del_btn) {
+					me.value = me.value.substr(0, me.value.length - 1);
+				} else {
+					me.value += btn_value;
+				}
+			}
+			me.onclick(btn_value);
+		});
+	}
+
+	reset_value() {
+		this.value = '';
+	}
+
+	get_value() {
+		return flt(this.value);
+	}
+
+	get_btn(btn_value) {
+		return this.wrapper.find(`.num-col[data-value="${btn_value}"]`);
+	}
+
+	highlight_button($btn) {
+		$btn.addClass('highlight');
+		setTimeout(() => $btn.removeClass('highlight'), 1000);
+	}
+
+	set_active(btn_value) {
+		const $btn = this.get_btn(btn_value);
+		this.wrapper.find('.num-col').removeClass('active');
+		$btn.addClass('active');
+	}
+
+	set_inactive() {
+		this.wrapper.find('.num-col').removeClass('active');
+	}
+}
+
+class Payment {
+	constructor({frm, events}) {
+		this.frm = frm;
+		this.events = events;
+		this.make();
+		this.bind_events();
+		this.set_primary_action();
+	}
+
+	open_modal() {
+		this.dialog.show();
+	}
+
+	make() {
+		this.set_flag();
+
+		let title = __('Total Amount {0}',
+			[format_currency(this.frm.doc.grand_total, this.frm.doc.currency)]);
+
+		this.dialog = new frappe.ui.Dialog({
+			title: title,
+			fields: this.get_fields(),
+			width: 800
+		});
+
+		this.$body = this.dialog.body;
+
+		this.numpad = new NumberPad({
+			wrapper: $(this.$body).find('[data-fieldname="numpad"]'),
+			button_array: [
+				[1, 2, 3],
+				[4, 5, 6],
+				[7, 8, 9],
+				['Del', 0, '.'],
+			],
+			onclick: () => {
+				if(this.fieldname) {
+					this.dialog.set_value(this.fieldname, this.numpad.get_value());
+				}
+			}
+		});
+	}
+
+	bind_events() {
+		var me = this;
+		$(this.dialog.body).find('.input-with-feedback').focusin(function() {
+			me.numpad.reset_value();
+			me.fieldname = $(this).prop('dataset').fieldname;
+		});
+	}
+
+	set_primary_action() {
+		var me = this;
+
+		this.dialog.set_primary_action(__("Submit"), function() {
+			me.dialog.hide();
+			me.events.submit_form();
+		});
+	}
+
+	get_fields() {
+		const me = this;
+
+		let fields = this.frm.doc.payments.map(p => {
+			return {
+				fieldtype: 'Currency',
+				label: __(p.mode_of_payment),
+				options: me.frm.doc.currency,
+				fieldname: p.mode_of_payment,
+				default: p.amount,
+				onchange: () => {
+					const value = this.dialog.get_value(this.fieldname);
+					me.update_payment_value(this.fieldname, value);
+				}
+			};
+		});
+
+		fields = fields.concat([
+			{
+				fieldtype: 'Column Break',
+			},
+			{
+				fieldtype: 'HTML',
+				fieldname: 'numpad'
+			},
+			{
+				fieldtype: 'Section Break',
+			},
+			{
+				fieldtype: 'Currency',
+				label: __("Write off Amount"),
+				options: me.frm.doc.currency,
+				fieldname: "write_off_amount",
+				default: me.frm.doc.write_off_amount,
+				onchange: () => {
+					me.update_cur_frm_value('write_off_amount', () => {
+						frappe.flags.change_amount = false;
+						me.update_change_amount();
+					});
+				}
+			},
+			{
+				fieldtype: 'Column Break',
+			},
+			{
+				fieldtype: 'Currency',
+				label: __("Change Amount"),
+				options: me.frm.doc.currency,
+				fieldname: "change_amount",
+				default: me.frm.doc.change_amount,
+				onchange: () => {
+					me.update_cur_frm_value('change_amount', () => {
+						frappe.flags.write_off_amount = false;
+						me.update_write_off_amount();
+					});
+				}
+			},
+			{
+				fieldtype: 'Section Break',
+			},
+			{
+				fieldtype: 'Currency',
+				label: __("Paid Amount"),
+				options: me.frm.doc.currency,
+				fieldname: "paid_amount",
+				default: me.frm.doc.paid_amount,
+				read_only: 1
+			},
+			{
+				fieldtype: 'Column Break',
+			},
+			{
+				fieldtype: 'Currency',
+				label: __("Outstanding Amount"),
+				options: me.frm.doc.currency,
+				fieldname: "outstanding_amount",
+				default: me.frm.doc.outstanding_amount,
+				read_only: 1
+			},
+		]);
+
+		return fields;
+	}
+
+	set_flag() {
+		frappe.flags.write_off_amount = true;
+		frappe.flags.change_amount = true;
+	}
+
+	update_cur_frm_value(fieldname, callback) {
+		if (frappe.flags[fieldname]) {
+			const value = this.dialog.get_value(fieldname);
+			this.frm.set_value(fieldname, value)
+				.then(() => {
+					callback();
+				});
+		}
+
+		frappe.flags[fieldname] = true;
+	}
+
+	update_payment_value(fieldname, value) {
+		var me = this;
+		$.each(this.frm.doc.payments, function(i, data) {
+			if (__(data.mode_of_payment) == __(fieldname)) {
+				frappe.model.set_value('Sales Invoice Payment', data.name, 'amount', value)
+					.then(() => {
+						me.update_change_amount();
+						me.update_write_off_amount();
+					});
+			}
+		});
+	}
+
+	update_change_amount() {
+		this.dialog.set_value("change_amount", this.frm.doc.change_amount);
+		this.show_paid_amount();
+	}
+
+	update_write_off_amount() {
+		this.dialog.set_value("write_off_amount", this.frm.doc.write_off_amount);
+	}
+
+	show_paid_amount() {
+		this.dialog.set_value("paid_amount", this.frm.doc.paid_amount);
+		this.dialog.set_value("outstanding_amount", this.frm.doc.outstanding_amount);
+	}
+}
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.json b/erpnext/selling/page/point_of_sale/point_of_sale.json
new file mode 100644
index 0000000..1e348c0
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.json
@@ -0,0 +1,20 @@
+{
+ "content": null, 
+ "creation": "2017-08-07 17:08:56.737947", 
+ "docstatus": 0, 
+ "doctype": "Page", 
+ "idx": 0, 
+ "modified": "2017-08-07 17:08:56.737947", 
+ "modified_by": "Administrator", 
+ "module": "Selling", 
+ "name": "point-of-sale", 
+ "owner": "Administrator", 
+ "page_name": "Point of Sale", 
+ "restrict_to_domain": "Retail", 
+ "roles": [], 
+ "script": null, 
+ "standard": "Yes", 
+ "style": null, 
+ "system_page": 0, 
+ "title": "Point of Sale"
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
new file mode 100644
index 0000000..8ed288b
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe, json
+
+@frappe.whitelist()
+def get_items(start, page_length, price_list, item_group, search_value=""):
+	serial_no = ""
+	batch_no = ""
+	item_code = search_value
+
+	if search_value:
+		# search serial no
+		serial_no_data = frappe.db.get_value('Serial No', search_value, ['name', 'item_code'])
+		if serial_no_data:
+			serial_no, item_code = serial_no_data
+
+		if not serial_no:
+			batch_no_data = frappe.db.get_value('Batch', search_value, ['name', 'item'])
+			if batch_no_data:
+				batch_no, item_code = batch_no_data
+
+	lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
+	# locate function is used to sort by closest match from the beginning of the value
+	res = frappe.db.sql("""select i.name as item_code, i.item_name, i.image as item_image,
+		item_det.price_list_rate, item_det.currency
+		from `tabItem` i LEFT JOIN
+			(select item_code, price_list_rate, currency from
+				`tabItem Price`	where price_list=%(price_list)s) item_det
+		ON
+			(item_det.item_code=i.name or item_det.item_code=i.variant_of)
+		where
+			i.disabled = 0 and i.has_variants = 0
+			and i.item_group in (select name from `tabItem Group` where lft >= {lft} and rgt <= {rgt})
+			and (i.item_code like %(item_code)s
+			or i.item_name like %(item_code)s or i.barcode like %(item_code)s)
+		limit {start}, {page_length}""".format(start=start, page_length=page_length, lft=lft, rgt=rgt),
+		{
+			'item_code': '%%%s%%'%(frappe.db.escape(item_code)),
+			'price_list': price_list
+		} , as_dict=1)
+
+	res = {
+		'items': res
+	}
+
+	if serial_no:
+		res.update({
+			'serial_no': serial_no
+		})
+
+	if batch_no:
+		res.update({
+			'batch_no': batch_no
+		})
+
+	return res
+
+@frappe.whitelist()
+def submit_invoice(doc):
+	if isinstance(doc, basestring):
+		args = json.loads(doc)
+
+	doc = frappe.new_doc('Sales Invoice')
+	doc.update(args)
+	doc.run_method("set_missing_values")
+	doc.run_method("calculate_taxes_and_totals")
+	doc.submit()
+
+	return doc
diff --git a/erpnext/selling/page/point_of_sale/test_point_of_sale.js b/erpnext/selling/page/point_of_sale/test_point_of_sale.js
new file mode 100644
index 0000000..d5053b5
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/test_point_of_sale.js
@@ -0,0 +1,56 @@
+QUnit.test("test:POS Settings", function(assert) {
+	assert.expect(1);
+	let done = assert.async();
+
+	frappe.run_serially([
+		() => frappe.set_route('Form', 'POS Settings'),
+		() => cur_frm.set_value('is_online', 1),
+		() => frappe.timeout(0.2),
+		() => cur_frm.save(),
+		() => frappe.timeout(1),
+		() => frappe.ui.toolbar.clear_cache(),
+		() => frappe.timeout(2),
+		() => assert.ok(cur_frm.doc.is_online==1, "Enabled online"),
+		() => frappe.timeout(2),
+		() => done()
+	]);
+});
+
+QUnit.test("test:Point of Sales", function(assert) {
+	assert.expect(1);
+	let done = assert.async();
+
+	frappe.run_serially([
+		() => frappe.set_route('point-of-sale'),
+		() => frappe.timeout(2),
+		() => frappe.set_control('customer', 'Test Customer 1'),
+		() => frappe.timeout(0.2),
+		() => cur_frm.set_value('customer', 'Test Customer 1'),
+		() => frappe.timeout(2),
+		() => frappe.click_link('Test Product 2'),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.cart-items [title="Test Product 2"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.number-pad [data-value="Rate"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.number-pad [data-value="2"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.number-pad [data-value="5"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.number-pad [data-value="0"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.number-pad [data-value="Pay"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.frappe-control [data-value="4"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.frappe-control [data-value="5"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_element(`.frappe-control [data-value="0"]`),
+		() => frappe.timeout(0.2),
+		() => frappe.click_button('Submit'),
+		() => frappe.click_button('Yes'),
+		() => frappe.timeout(5),
+		() => assert.ok(cur_frm.doc.docstatus==1, "Sales invoice created successfully"),
+		() => done()
+	]);
+});
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 80ef708..8d084dc 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -79,7 +79,7 @@
 		and out.warehouse and out.stock_qty > 0:
 
 		if out.has_serial_no:
-			out.serial_no = get_serial_no(out)
+			out.serial_no = get_serial_no(out, args.serial_no)
 
 		if out.has_batch_no and not args.get("batch_no"):
 			out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
@@ -554,7 +554,8 @@
 	return out
 
 @frappe.whitelist()
-def get_serial_no(args):
+def get_serial_no(args, serial_nos=None):
+	serial_no = None
 	if isinstance(args, basestring):
 		args = json.loads(args)
 		args = frappe._dict(args)
@@ -568,4 +569,9 @@
 			args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')})
 			args = process_args(args)
 			serial_no = get_serial_nos_by_fifo(args)
-			return serial_no
+
+	if not serial_no and serial_nos:
+		# For POS
+		serial_no = serial_nos
+
+	return serial_no
diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt
index 6017f6f..9bb520f 100644
--- a/erpnext/tests/ui/tests.txt
+++ b/erpnext/tests/ui/tests.txt
@@ -50,6 +50,7 @@
 erpnext/schools/doctype/instructor/test_instructor.js
 erpnext/stock/doctype/warehouse/test_warehouse.js
 erpnext/manufacturing/doctype/production_order/test_production_order.js #long
+erpnext/selling/page/point_of_sale/test_point_of_sale.js
 erpnext/accounts/page/pos/test_pos.js
 erpnext/selling/doctype/product_bundle/test_product_bundle.js
 erpnext/stock/doctype/delivery_note/test_delivery_note.js