From 519cc0997978761b0c6a87abfda8db2d5df14c23 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 11:22:03 +0530 Subject: [PATCH 01/36] [wip] New POS UI --- erpnext/accounts/page/pos/pos.js | 3 +- erpnext/public/css/pos.css | 53 ++ erpnext/public/less/pos.less | 81 +++ .../selling/page/point_of_sale/__init__.py | 0 .../page/point_of_sale/point_of_sale.js | 478 ++++++++++++++++++ .../page/point_of_sale/point_of_sale.json | 20 + 6 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/css/pos.css create mode 100644 erpnext/public/less/pos.less create mode 100644 erpnext/selling/page/point_of_sale/__init__.py create mode 100644 erpnext/selling/page/point_of_sale/point_of_sale.js create mode 100644 erpnext/selling/page/point_of_sale/point_of_sale.json diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index d69a306670..599372411a 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -8,7 +8,8 @@ frappe.pages['pos'].on_page_load = function (wrapper) { single_column: true }); - wrapper.pos = new erpnext.pos.PointOfSale(wrapper) + wrapper.pos = new erpnext.pos.PointOfSale(wrapper); + cur_pos = wrapper.pos; } frappe.pages['pos'].refresh = function (wrapper) { diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css new file mode 100644 index 0000000000..d44b17caaf --- /dev/null +++ b/erpnext/public/css/pos.css @@ -0,0 +1,53 @@ +.pos { + padding: 15px; +} +.customer-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; +} +.item-group-field { + margin-left: 15px; +} +.cart-wrapper .list-item__content:not(:first-child) { + justify-content: flex-end; +} +.cart-items { + height: 200px; + overflow: auto; +} +.fields { + display: flex; +} +.pos-items-wrapper { + max-height: 480px; + overflow: auto; +} +.pos-item-wrapper { + height: 250px; +} +.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%); +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less new file mode 100644 index 0000000000..c94f8b59f5 --- /dev/null +++ b/erpnext/public/less/pos.less @@ -0,0 +1,81 @@ +@import "../../../../frappe/frappe/public/less/variables.less"; + +.pos { + // display: flex; + padding: 15px; +} + +.customer-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; +} + +.item-group-field { + margin-left: 15px; +} + +.cart-wrapper { + .list-item__content:not(:first-child) { + justify-content: flex-end; + } +} + +.cart-items { + height: 200px; + overflow: auto; + + // .list-item { + // background-color: @light-yellow; + // transition: background-color 1s linear; + // } + + // .list-item.added { + // background-color: white; + // } +} + +.fields { + display: flex; +} + +.pos-items-wrapper { + max-height: 480px; + overflow: auto; +} + +.pos-item-wrapper { + height: 250px; +} + +.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%); + } +} \ 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 0000000000..e69de29bb2 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 0000000000..c9ef30e7eb --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -0,0 +1,478 @@ +frappe.pages['point-of-sale'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Point of Sale', + single_column: true + }); + + wrapper.pos = new erpnext.PointOfSale(wrapper); + cur_pos = wrapper.pos; +} + +erpnext.PointOfSale = class PointOfSale { + constructor(wrapper) { + this.wrapper = $(wrapper).find('.layout-main-section'); + this.page = wrapper.page; + + const assets = [ + 'assets/frappe/js/lib/clusterize.js', + 'assets/erpnext/css/pos.css' + ]; + + frappe.require(assets, () => { + this.prepare(); + this.make(); + this.bind_events(); + }); + } + + prepare() { + this.set_online_status(); + this.prepare_menu(); + } + + make() { + this.make_dom(); + this.make_customer_field(); + this.make_cart(); + this.make_items(); + } + + 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_menu() { + this.page.clear_menu(); + + // for mobile + this.page.add_menu_item(__("Pay"), function () { + // + }).addClass('visible-xs'); + + this.page.add_menu_item(__("New Sales Invoice"), function () { + // + }) + + this.page.add_menu_item(__("Sync Master Data"), function () { + // + }); + + this.page.add_menu_item(__("Sync Offline Invoices"), function () { + // + }); + + this.page.add_menu_item(__("POS Profile"), function () { + frappe.set_route('List', 'POS Profile'); + }); + } + + make_dom() { + this.wrapper.append(` +
+
+
+
+
+
+
+
+ +
+
+ `); + } + + make_customer_field() { + this.customer_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + label: 'Customer', + options: 'Customer' + }, + parent: this.wrapper.find('.customer-field'), + render_input: true + }); + } + + make_cart() { + this.cart = new erpnext.POSCart(this.wrapper.find('.cart-wrapper')); + } + + make_items() { + this.items = new erpnext.POSItems(this.wrapper.find('.item-container'), { + item_click: (item_code) => this.add_item_to_cart(item_code) + }); + } + + add_item_to_cart(item_code) { + const item = this.items.get(item_code); + this.cart.add_item(item); + } + + bind_events() { + + } + + make_sales_invoice_frm() { + const dt = 'Sales Invoice'; + frappe.model.with_doctype(dt, function() { + const page = $('
'); + const frm = new _f.Frm(dt, page, false); + const name = frappe.model.make_new_doc_and_get_name(dt, true); + frm.refresh(name); + }); + } +} + +erpnext.POSCart = class POSCart { + constructor(wrapper) { + this.wrapper = wrapper; + this.items = {}; + this.make(); + } + + make() { + this.wrapper.append(` +
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
+
+
+
+ No Items added to cart +
+
+
+ `); + } + + add_item(item) { + const { item_code } = item; + const _item = this.items[item_code]; + + if (_item) { + // exists, increase quantity + _item.quantity += 1; + this.update_quantity(_item); + } else { + // add it to this.items + const _item = { + doc: item, + quantity: 1, + discount: 2, + rate: 2 + } + Object.assign(this.items, { + [item_code]: _item + }); + this.add_item_to_cart(_item); + } + } + + add_item_to_cart(item) { + this.wrapper.find('.cart-items .empty-state').hide(); + const $item = $(this.get_item_html(item)) + $item.appendTo(this.wrapper.find('.cart-items')); + // $item.addClass('added'); + // this.wrapper.find('.cart-items').append(this.get_item_html(item)) + } + + update_quantity(item) { + this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) + .text(item.quantity); + } + + remove_item(item_code) { + delete this.items[item_code]; + + // this.refresh(); + } + + refresh() { + const item_codes = Object.keys(this.items); + const html = item_codes + .map(item_code => this.get_item_html(item_code)) + .join(""); + this.wrapper.find('.cart-items').html(html); + } + + get_item_html(_item) { + + let item; + if (typeof _item === "object") { + item = _item; + } + else if (typeof _item === "string") { + item = this.items[_item]; + } + + return ` +
+
+ ${item.doc.item_name} +
+
+ ${item.quantity} +
+
+ ${item.discount} +
+
+ ${item.rate} +
+
+ `; + } +} + +erpnext.POSItems = class POSItems { + constructor(wrapper, events) { + this.wrapper = wrapper; + this.items = {}; + this.make_dom(); + this.make_fields(); + + this.init_clusterize(); + this.bind_events(events); + + // bootstrap with 20 items + this.get_items() + .then(items => { + this.items = items + }) + .then(() => this.render_items()); + } + + make_dom() { + this.wrapper.html(` +
+
+
+
+
+
+
+
+ `); + + this.items_wrapper = this.wrapper.find('.items-wrapper'); + this.items_wrapper.append(` +
+
+
+
+ `); + } + + make_fields() { + this.search_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Data', + label: 'Search Item', + onchange: (e) => { + const search_term = e.target.value; + this.filter_items(search_term); + } + }, + parent: this.wrapper.find('.search-field'), + render_input: true, + }); + + this.item_group_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Select', + label: 'Item Group', + options: [ + 'All Item Groups', + 'Raw Materials', + 'Finished Goods' + ], + default: 'All Item Groups' + }, + 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 = '
'; + 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 += '
'; + row_items.push(curr_row); + curr_row = row_container; + } + curr_row += all_items[i]; + } + + this.clusterize.update(row_items); + } + + filter_items(search_term) { + search_term = search_term.toLowerCase(); + + const filtered_items = + Object.values(this.items) + .filter( + item => item.item_name.toLowerCase().includes(search_term) + ); + this.render_items(filtered_items); + } + + bind_events(events) { + this.wrapper.on('click', '.pos-item-wrapper', function(e) { + const $item = $(this); + const item_code = $item.attr('data-item-code'); + events.item_click.apply(null, [item_code]); + }); + } + + get(item_code) { + return this.items[item_code]; + } + + get_all() { + return this.items; + } + + get_item_html(item) { + const { item_code, item_name, image: item_image, item_stock=0, item_price=0} = item; + const item_title = item_name || item_code; + + const template = ` + + `; + + // const template = ` + + //
+ //
+ // ${item_name} + // Stock: ${item_stock} + //
+ //
+ //
+ // ${item_image ? + // `${item_title}` : + // ` + // ${frappe.get_abbr(item_title)} + // ` + // } + //
+ //
+ //
+ + // `; + + return template; + } + + get_items(start = 0, page_length = 2000) { + return new Promise(res => { + frappe.call({ + method: "frappe.desk.reportview.get", + type: "GET", + args: { + doctype: "Item", + fields: [ + "`tabItem`.`name`", + "`tabItem`.`owner`", + "`tabItem`.`docstatus`", + "`tabItem`.`modified`", + "`tabItem`.`modified_by`", + "`tabItem`.`item_name`", + "`tabItem`.`item_code`", + "`tabItem`.`disabled`", + "`tabItem`.`item_group`", + "`tabItem`.`stock_uom`", + "`tabItem`.`image`", + "`tabItem`.`variant_of`", + "`tabItem`.`has_variants`", + "`tabItem`.`end_of_life`", + "`tabItem`.`total_projected_qty`" + ], + order_by: "`tabItem`.`modified` desc", + page_length: page_length, + start: start + } + }) + .then(r => { + const data = r.message; + const items = frappe.utils.dict(data.keys, data.values); + + // convert to key, value + let items_dict = {}; + items.map(item => { + items_dict[item.item_code] = item; + }); + + res(items_dict); + }); + }); + } +} \ No newline at end of file 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 0000000000..1e348c09af --- /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 From 65c4bd6db60c06b18c07dabcc15c4ec16dc7964d Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 17:17:34 +0530 Subject: [PATCH 02/36] Add clusterize, move customer field to POSCart, get POS profile --- erpnext/public/css/pos.css | 2 +- erpnext/public/js/pos/clusterize.js | 329 ++++++++++++++++++ erpnext/public/less/pos.less | 2 +- .../page/point_of_sale/point_of_sale.js | 167 +++++---- 4 files changed, 428 insertions(+), 72 deletions(-) create mode 100644 erpnext/public/js/pos/clusterize.js diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index d44b17caaf..4ba00ba227 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -1,7 +1,7 @@ .pos { padding: 15px; } -.customer-container { +.cart-container { padding: 0 15px; display: inline-block; width: 39%; diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js new file mode 100644 index 0000000000..6d331ba761 --- /dev/null +++ b/erpnext/public/js/pos/clusterize.js @@ -0,0 +1,329 @@ +/*! 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 = '', + 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 = '' + data + '
'; + 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/less/pos.less b/erpnext/public/less/pos.less index c94f8b59f5..e627dbc080 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -5,7 +5,7 @@ padding: 15px; } -.customer-container { +.cart-container { padding: 0 15px; // flex: 2; display: inline-block; diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c9ef30e7eb..fb3f666e8a 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -15,25 +15,26 @@ erpnext.PointOfSale = class PointOfSale { this.page = wrapper.page; const assets = [ - 'assets/frappe/js/lib/clusterize.js', + 'assets/erpnext/js/pos/clusterize.js', 'assets/erpnext/css/pos.css' ]; frappe.require(assets, () => { - this.prepare(); - this.make(); - this.bind_events(); + this.prepare().then(() => { + this.make(); + this.bind_events(); + }); }); } prepare() { this.set_online_status(); this.prepare_menu(); + return this.get_pos_profile(); } make() { this.make_dom(); - this.make_customer_field(); this.make_cart(); this.make_items(); } @@ -52,6 +53,66 @@ erpnext.PointOfSale = class PointOfSale { }); } + make_dom() { + this.wrapper.append(` +
+
+ +
+
+ +
+
+ `); + } + + make_cart() { + this.cart = new erpnext.POSCart(this.wrapper.find('.cart-container')); + } + + make_items() { + this.items = new erpnext.POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + item_click: (item_code) => this.add_item_to_cart(item_code) + } + }); + } + + add_item_to_cart(item_code) { + const item = this.items.get(item_code); + this.cart.add_item(item); + } + + bind_events() { + + } + + get_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; + }); + } + + make_sales_invoice_frm() { + const dt = 'Sales Invoice'; + return new Promise(resolve => { + frappe.model.with_doctype(dt, function() { + const page = $('
'); + const frm = new _f.Frm(dt, page, false); + const name = frappe.model.make_new_doc_and_get_name(dt, true); + frm.refresh(name); + resolve(frm); + }); + }); + } + prepare_menu() { this.page.clear_menu(); @@ -76,19 +137,38 @@ erpnext.PointOfSale = class PointOfSale { frappe.set_route('List', 'POS Profile'); }); } +} + +erpnext.POSCart = class POSCart { + constructor(wrapper) { + this.wrapper = wrapper; + this.items = {}; + this.make(); + } + + make() { + this.make_dom(); + this.make_customer_field(); + } make_dom() { this.wrapper.append(` -
-
-
+
+
+
+
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
-
+
+
+ No Items added to cart +
-
-
- -
+
`); } @@ -105,61 +185,6 @@ erpnext.PointOfSale = class PointOfSale { }); } - make_cart() { - this.cart = new erpnext.POSCart(this.wrapper.find('.cart-wrapper')); - } - - make_items() { - this.items = new erpnext.POSItems(this.wrapper.find('.item-container'), { - item_click: (item_code) => this.add_item_to_cart(item_code) - }); - } - - add_item_to_cart(item_code) { - const item = this.items.get(item_code); - this.cart.add_item(item); - } - - bind_events() { - - } - - make_sales_invoice_frm() { - const dt = 'Sales Invoice'; - frappe.model.with_doctype(dt, function() { - const page = $('
'); - const frm = new _f.Frm(dt, page, false); - const name = frappe.model.make_new_doc_and_get_name(dt, true); - frm.refresh(name); - }); - } -} - -erpnext.POSCart = class POSCart { - constructor(wrapper) { - this.wrapper = wrapper; - this.items = {}; - this.make(); - } - - make() { - this.wrapper.append(` -
-
-
${__('Item Name')}
-
${__('Quantity')}
-
${__('Discount')}
-
${__('Rate')}
-
-
-
- No Items added to cart -
-
-
- `); - } - add_item(item) { const { item_code } = item; const _item = this.items[item_code]; @@ -240,9 +265,11 @@ erpnext.POSCart = class POSCart { } erpnext.POSItems = class POSItems { - constructor(wrapper, events) { + constructor({wrapper, pos_profile, events}) { this.wrapper = wrapper; + this.pos_profile = pos_profile; this.items = {}; + this.make_dom(); this.make_fields(); @@ -432,7 +459,7 @@ erpnext.POSItems = class POSItems { return template; } - get_items(start = 0, page_length = 2000) { + get_items(start = 0, page_length = 20) { return new Promise(res => { frappe.call({ method: "frappe.desk.reportview.get", From f7c7ff4aae3441b050425a7b4dee42adba36b6d6 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 18:28:05 +0530 Subject: [PATCH 03/36] Styling for search fields --- erpnext/public/css/pos.css | 11 +++++++- erpnext/public/less/pos.less | 22 ++++++++------- .../page/point_of_sale/point_of_sale.js | 27 +++++++++++++------ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 4ba00ba227..81b109838f 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -13,7 +13,14 @@ 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 .list-item__content:not(:first-child) { @@ -31,7 +38,9 @@ overflow: auto; } .pos-item-wrapper { - height: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; } .image-view-container { display: block; diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index e627dbc080..1ae0dfd993 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -21,7 +21,16 @@ vertical-align: top; } +.search-field { + width: 60%; + + input::placeholder { + font-size: @text-medium; + } +} + .item-group-field { + width: 40%; margin-left: 15px; } @@ -34,15 +43,6 @@ .cart-items { height: 200px; overflow: auto; - - // .list-item { - // background-color: @light-yellow; - // transition: background-color 1s linear; - // } - - // .list-item.added { - // background-color: white; - // } } .fields { @@ -55,7 +55,9 @@ } .pos-item-wrapper { - height: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; } .image-view-container { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index fb3f666e8a..1fb8c1ec0b 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -306,19 +306,28 @@ erpnext.POSItems = class POSItems { } make_fields() { + // Search field this.search_field = frappe.ui.form.make_control({ df: { fieldtype: 'Data', - label: 'Search Item', - onchange: (e) => { - const search_term = e.target.value; - this.filter_items(search_term); - } + 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) => { + const search_term = e.target.value; + this.filter_items(search_term); + }); + + + // Item group field this.item_group_field = frappe.ui.form.make_control({ df: { fieldtype: 'Select', @@ -370,9 +379,11 @@ erpnext.POSItems = class POSItems { const filtered_items = Object.values(this.items) - .filter( - item => item.item_name.toLowerCase().includes(search_term) - ); + .filter(item => { + return item.item_code.toLowerCase().includes(search_term) || + item.item_name.toLowerCase().includes(search_term) + }); + this.render_items(filtered_items); } From 5a130879394c2db6403cd6e880b5399d5934b985 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 11 Aug 2017 15:49:23 +0530 Subject: [PATCH 04/36] more styling --- erpnext/public/css/pos.css | 5 ++++- erpnext/public/less/pos.less | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 81b109838f..399613d497 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -35,7 +35,10 @@ } .pos-items-wrapper { max-height: 480px; - overflow: auto; + overflow-y: auto; +} +.pos-items { + overflow: hidden; } .pos-item-wrapper { display: flex; diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 1ae0dfd993..9358f0a24c 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -51,7 +51,11 @@ .pos-items-wrapper { max-height: 480px; - overflow: auto; + overflow-y: auto; +} + +.pos-items { + overflow: hidden; } .pos-item-wrapper { From 854d335ab1f086d69136b75d07a217612e517581 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 11 Aug 2017 18:44:19 +0530 Subject: [PATCH 05/36] Added functional part --- .../selling/page/point_of_sale/point_of_sale.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 1fb8c1ec0b..c28a5bd8c6 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -30,6 +30,7 @@ erpnext.PointOfSale = class PointOfSale { prepare() { this.set_online_status(); this.prepare_menu(); + this.make_sales_invoice_frm() return this.get_pos_profile(); } @@ -108,6 +109,7 @@ erpnext.PointOfSale = class PointOfSale { const frm = new _f.Frm(dt, page, false); const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); + frm.doc.items = []; resolve(frm); }); }); @@ -178,7 +180,10 @@ erpnext.POSCart = class POSCart { df: { fieldtype: 'Link', label: 'Customer', - options: 'Customer' + options: 'Customer', + onchange: (e) => { + cur_frm.set_value('customer', this.customer_field.value); + } }, parent: this.wrapper.find('.customer-field'), render_input: true @@ -195,6 +200,10 @@ erpnext.POSCart = class POSCart { this.update_quantity(_item); } else { // add it to this.items + item['qty'] = 1; + this.child = cur_frm.add_child('items', item) + cur_frm.script_manager.trigger("item_code", this.child.doctype, this.child.name); + const _item = { doc: item, quantity: 1, @@ -219,6 +228,12 @@ erpnext.POSCart = class POSCart { update_quantity(item) { this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) .text(item.quantity); + + $.each(cur_frm.doc["items"] || [], function(i, d) { + if (d.item_code == item.doc.item_code) { + frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); + } + }); } remove_item(item_code) { From 21fc26c2a22452c7b01547ab521625b511791a77 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 14 Aug 2017 18:25:59 +0530 Subject: [PATCH 06/36] Update cart ui from cur_frm, Add number pad --- erpnext/public/css/pos.css | 42 +++ erpnext/public/less/pos.less | 44 +++ .../page/point_of_sale/point_of_sale.js | 334 ++++++++++++------ 3 files changed, 305 insertions(+), 115 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 399613d497..de1e097bb8 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -23,6 +23,9 @@ width: 40%; margin-left: 15px; } +.cart-wrapper { + margin-bottom: 10px; +} .cart-wrapper .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -30,6 +33,10 @@ height: 200px; overflow: auto; } +.cart-items input { + height: 22px; + font-size: 12px; +} .fields { display: flex; } @@ -63,3 +70,38 @@ 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; +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 9358f0a24c..de165144f1 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -35,6 +35,7 @@ } .cart-wrapper { + margin-bottom: 10px; .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -43,6 +44,11 @@ .cart-items { height: 200px; overflow: auto; + + input { + height: 22px; + font-size: @text-medium; + } } .fields { @@ -84,4 +90,42 @@ 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; + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c28a5bd8c6..19eb70ee81 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,3 +1,5 @@ +/* global Clusterize */ + frappe.pages['point-of-sale'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, @@ -5,11 +7,11 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { single_column: true }); - wrapper.pos = new erpnext.PointOfSale(wrapper); - cur_pos = wrapper.pos; + wrapper.pos = new PointOfSale(wrapper); + window.cur_pos = wrapper.pos; } -erpnext.PointOfSale = class PointOfSale { +class PointOfSale { constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -20,24 +22,25 @@ erpnext.PointOfSale = class PointOfSale { ]; frappe.require(assets, () => { - this.prepare().then(() => { - this.make(); - this.bind_events(); - }); + this.make(); }); } - prepare() { - this.set_online_status(); - this.prepare_menu(); - this.make_sales_invoice_frm() - return this.get_pos_profile(); - } - make() { - this.make_dom(); - this.make_cart(); - this.make_items(); + return frappe.run_serially([ + () => { + this.prepare_dom(); + this.prepare_menu(); + this.set_online_status(); + }, + () => this.make_sales_invoice_frm(), + () => this.setup_pos_profile(), + () => { + this.make_cart(); + this.make_items(); + this.bind_events(); + } + ]); } set_online_status() { @@ -54,7 +57,7 @@ erpnext.PointOfSale = class PointOfSale { }); } - make_dom() { + prepare_dom() { this.wrapper.append(`
@@ -68,29 +71,64 @@ erpnext.PointOfSale = class PointOfSale { } make_cart() { - this.cart = new erpnext.POSCart(this.wrapper.find('.cart-container')); - } - - make_items() { - this.items = new erpnext.POSItems({ - wrapper: this.wrapper.find('.item-container'), - pos_profile: this.pos_profile, + this.cart = new POSCart({ + wrapper: this.wrapper.find('.cart-container'), events: { - item_click: (item_code) => this.add_item_to_cart(item_code) + customer_change: (customer) => this.cur_frm.set_value('customer', customer), + increase_qty: (item_code) => { + this.add_item_to_cart(item_code); + }, + decrease_qty: (item_code) => { + this.add_item_to_cart(item_code, -1); + } } }); } - add_item_to_cart(item_code) { - const item = this.items.get(item_code); - this.cart.add_item(item); + make_items() { + this.items = new POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + item_click: (item_code) => { + if(!this.cur_frm.doc.customer) { + frappe.throw(__('Please select a customer')); + } + this.add_item_to_cart(item_code); + } + } + }); + } + + add_item_to_cart(item_code, qty = 1) { + + if(this.cart.exists(item_code)) { + // increase qty by 1 + this.cur_frm.doc.items.forEach((item) => { + if (item.item_code === item_code) { + frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty); + // update cart + this.cart.add_item(item); + } + }); + return; + } + + // add to cur_frm + const item = this.cur_frm.add_child('items', { item_code: item_code }); + this.cur_frm.script_manager + .trigger('item_code', item.doctype, item.name) + .then(() => { + // update cart + this.cart.add_item(item); + }); } bind_events() { } - get_pos_profile() { + setup_pos_profile() { return frappe.call({ method: 'erpnext.stock.get_item_details.get_pos_profile', args: { @@ -104,13 +142,14 @@ erpnext.PointOfSale = class PointOfSale { make_sales_invoice_frm() { const dt = 'Sales Invoice'; return new Promise(resolve => { - frappe.model.with_doctype(dt, function() { + frappe.model.with_doctype(dt, () => { const page = $('
'); const frm = new _f.Frm(dt, page, false); const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); frm.doc.items = []; - resolve(frm); + this.cur_frm = frm; + resolve(); }); }); } @@ -141,16 +180,18 @@ erpnext.PointOfSale = class PointOfSale { } } -erpnext.POSCart = class POSCart { - constructor(wrapper) { +class POSCart { + constructor({wrapper, events}) { this.wrapper = wrapper; - this.items = {}; + this.events = events; this.make(); + this.bind_events(); } make() { this.make_dom(); this.make_customer_field(); + this.make_numpad(); } make_dom() { @@ -172,7 +213,10 @@ erpnext.POSCart = class POSCart {
+
+
`); + this.$cart_items = this.wrapper.find('.cart-items'); } make_customer_field() { @@ -181,8 +225,9 @@ erpnext.POSCart = class POSCart { fieldtype: 'Link', label: 'Customer', options: 'Customer', + reqd: 1, onchange: (e) => { - cur_frm.set_value('customer', this.customer_field.value); + this.events.customer_change.apply(null, [this.customer_field.get_value()]); } }, parent: this.wrapper.find('.customer-field'), @@ -190,96 +235,113 @@ erpnext.POSCart = class POSCart { }); } - add_item(item) { - const { item_code } = item; - const _item = this.items[item_code]; - - if (_item) { - // exists, increase quantity - _item.quantity += 1; - this.update_quantity(_item); - } else { - // add it to this.items - item['qty'] = 1; - this.child = cur_frm.add_child('items', item) - cur_frm.script_manager.trigger("item_code", this.child.doctype, this.child.name); - - const _item = { - doc: item, - quantity: 1, - discount: 2, - rate: 2 + make_numpad() { + this.numpad = new NumberPad({ + wrapper: this.wrapper.find('.number-pad-container'), + onclick: (btn_value) => { + // on click + console.log(btn_value); } - Object.assign(this.items, { - [item_code]: _item - }); - this.add_item_to_cart(_item); - } + }); } - add_item_to_cart(item) { + add_item(item) { this.wrapper.find('.cart-items .empty-state').hide(); - const $item = $(this.get_item_html(item)) - $item.appendTo(this.wrapper.find('.cart-items')); - // $item.addClass('added'); - // this.wrapper.find('.cart-items').append(this.get_item_html(item)) - } - update_quantity(item) { - this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) - .text(item.quantity); - - $.each(cur_frm.doc["items"] || [], function(i, d) { - if (d.item_code == item.doc.item_code) { - frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); - } - }); - } - - remove_item(item_code) { - delete this.items[item_code]; - - // this.refresh(); - } - - refresh() { - const item_codes = Object.keys(this.items); - const html = item_codes - .map(item_code => this.get_item_html(item_code)) - .join(""); - this.wrapper.find('.cart-items').html(html); - } - - get_item_html(_item) { - - let item; - if (typeof _item === "object") { - item = _item; - } - else if (typeof _item === "string") { - item = this.items[_item]; + 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) { + $item.find('.quantity input').val(item.qty); + $item.find('.discount').text(item.discount_percentage); + $item.find('.rate').text(item.rate); + } else { + $item.remove(); + } + } + + 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}"]`); + const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + this.$cart_items.animate({ scrollTop }); + } + + get_item_html(item) { return ` -
+
- ${item.doc.item_name} + ${item.item_name}
- ${item.quantity} + ${get_quantity_html(item.qty)}
- ${item.discount} + ${item.discount_percentage}%
${item.rate}
`; + + function get_quantity_html(value) { + return ` +
+ + + + + + + + + +
+ `; + } + } + + bind_events() { + const events = this.events; + 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.increase_qty(item_code); + } else if(action === 'decrement') { + events.decrease_qty(item_code); + } + }); } } -erpnext.POSItems = class POSItems { +class POSItems { constructor({wrapper, pos_profile, events}) { this.wrapper = wrapper; this.pos_profile = pos_profile; @@ -439,16 +501,10 @@ erpnext.POSItems = class POSItems {
- ${!item_image ? - ` + ${!item_image ? ` ${frappe.get_abbr(item_title)} - ` : - '' - } - ${item_image ? - `${item_title}` : - '' - } + ` : '' } + ${item_image ? `${item_title}` : '' }
${item_price} @@ -509,12 +565,12 @@ erpnext.POSItems = class POSItems { "`tabItem`.`end_of_life`", "`tabItem`.`total_projected_qty`" ], + filters: [['disabled', '=', '0']], order_by: "`tabItem`.`modified` desc", page_length: page_length, start: start } - }) - .then(r => { + }).then(r => { const data = r.message; const items = frappe.utils.dict(data.keys, data.values); @@ -528,4 +584,52 @@ erpnext.POSItems = class POSItems { }); }); } +} + +class NumberPad { + constructor({wrapper, onclick}) { + this.wrapper = wrapper; + this.onclick = onclick; + this.make_dom(); + this.bind_events(); + } + + make_dom() { + const button_array = [ + [1, 2, 3, 'Qty'], + [4, 5, 6, 'Disc'], + [7, 8, 9, 'Price'], + ['Del', 0, '.', 'Pay'] + ]; + + this.wrapper.html(` +
+ ${button_array.map(get_row).join("")} +
+ `); + + function get_row(row) { + return '
' + row.map(get_col).join("") + '
'; + } + + function get_col(col) { + return `
${col}
`; + } + } + + bind_events() { + // bind click event + const me = this; + this.wrapper.on('click', '.num-col', function() { + const $btn = $(this); + me.highlight_button($btn); + me.onclick.apply(null, [$btn.attr('data-value')]); + }); + } + + highlight_button($btn) { + // const $btn = this.wrapper.find(`[data-value="${value}"]`); + $btn.addClass('highlight'); + setTimeout(() => $btn.removeClass('highlight'), 1000); + } } \ No newline at end of file From 6e7db034f2eb0a0e18e1071fd2bfdb6fab6982f5 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 11:22:03 +0530 Subject: [PATCH 07/36] [wip] New POS UI --- erpnext/accounts/page/pos/pos.js | 3 +- erpnext/public/css/pos.css | 53 ++ erpnext/public/less/pos.less | 81 +++ .../selling/page/point_of_sale/__init__.py | 0 .../page/point_of_sale/point_of_sale.js | 478 ++++++++++++++++++ .../page/point_of_sale/point_of_sale.json | 20 + 6 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/css/pos.css create mode 100644 erpnext/public/less/pos.less create mode 100644 erpnext/selling/page/point_of_sale/__init__.py create mode 100644 erpnext/selling/page/point_of_sale/point_of_sale.js create mode 100644 erpnext/selling/page/point_of_sale/point_of_sale.json diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index d69a306670..599372411a 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -8,7 +8,8 @@ frappe.pages['pos'].on_page_load = function (wrapper) { single_column: true }); - wrapper.pos = new erpnext.pos.PointOfSale(wrapper) + wrapper.pos = new erpnext.pos.PointOfSale(wrapper); + cur_pos = wrapper.pos; } frappe.pages['pos'].refresh = function (wrapper) { diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css new file mode 100644 index 0000000000..d44b17caaf --- /dev/null +++ b/erpnext/public/css/pos.css @@ -0,0 +1,53 @@ +.pos { + padding: 15px; +} +.customer-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; +} +.item-group-field { + margin-left: 15px; +} +.cart-wrapper .list-item__content:not(:first-child) { + justify-content: flex-end; +} +.cart-items { + height: 200px; + overflow: auto; +} +.fields { + display: flex; +} +.pos-items-wrapper { + max-height: 480px; + overflow: auto; +} +.pos-item-wrapper { + height: 250px; +} +.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%); +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less new file mode 100644 index 0000000000..c94f8b59f5 --- /dev/null +++ b/erpnext/public/less/pos.less @@ -0,0 +1,81 @@ +@import "../../../../frappe/frappe/public/less/variables.less"; + +.pos { + // display: flex; + padding: 15px; +} + +.customer-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; +} + +.item-group-field { + margin-left: 15px; +} + +.cart-wrapper { + .list-item__content:not(:first-child) { + justify-content: flex-end; + } +} + +.cart-items { + height: 200px; + overflow: auto; + + // .list-item { + // background-color: @light-yellow; + // transition: background-color 1s linear; + // } + + // .list-item.added { + // background-color: white; + // } +} + +.fields { + display: flex; +} + +.pos-items-wrapper { + max-height: 480px; + overflow: auto; +} + +.pos-item-wrapper { + height: 250px; +} + +.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%); + } +} \ 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 0000000000..e69de29bb2 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 0000000000..c9ef30e7eb --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -0,0 +1,478 @@ +frappe.pages['point-of-sale'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Point of Sale', + single_column: true + }); + + wrapper.pos = new erpnext.PointOfSale(wrapper); + cur_pos = wrapper.pos; +} + +erpnext.PointOfSale = class PointOfSale { + constructor(wrapper) { + this.wrapper = $(wrapper).find('.layout-main-section'); + this.page = wrapper.page; + + const assets = [ + 'assets/frappe/js/lib/clusterize.js', + 'assets/erpnext/css/pos.css' + ]; + + frappe.require(assets, () => { + this.prepare(); + this.make(); + this.bind_events(); + }); + } + + prepare() { + this.set_online_status(); + this.prepare_menu(); + } + + make() { + this.make_dom(); + this.make_customer_field(); + this.make_cart(); + this.make_items(); + } + + 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_menu() { + this.page.clear_menu(); + + // for mobile + this.page.add_menu_item(__("Pay"), function () { + // + }).addClass('visible-xs'); + + this.page.add_menu_item(__("New Sales Invoice"), function () { + // + }) + + this.page.add_menu_item(__("Sync Master Data"), function () { + // + }); + + this.page.add_menu_item(__("Sync Offline Invoices"), function () { + // + }); + + this.page.add_menu_item(__("POS Profile"), function () { + frappe.set_route('List', 'POS Profile'); + }); + } + + make_dom() { + this.wrapper.append(` +
+
+
+
+
+
+
+
+ +
+
+ `); + } + + make_customer_field() { + this.customer_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + label: 'Customer', + options: 'Customer' + }, + parent: this.wrapper.find('.customer-field'), + render_input: true + }); + } + + make_cart() { + this.cart = new erpnext.POSCart(this.wrapper.find('.cart-wrapper')); + } + + make_items() { + this.items = new erpnext.POSItems(this.wrapper.find('.item-container'), { + item_click: (item_code) => this.add_item_to_cart(item_code) + }); + } + + add_item_to_cart(item_code) { + const item = this.items.get(item_code); + this.cart.add_item(item); + } + + bind_events() { + + } + + make_sales_invoice_frm() { + const dt = 'Sales Invoice'; + frappe.model.with_doctype(dt, function() { + const page = $('
'); + const frm = new _f.Frm(dt, page, false); + const name = frappe.model.make_new_doc_and_get_name(dt, true); + frm.refresh(name); + }); + } +} + +erpnext.POSCart = class POSCart { + constructor(wrapper) { + this.wrapper = wrapper; + this.items = {}; + this.make(); + } + + make() { + this.wrapper.append(` +
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
+
+
+
+ No Items added to cart +
+
+
+ `); + } + + add_item(item) { + const { item_code } = item; + const _item = this.items[item_code]; + + if (_item) { + // exists, increase quantity + _item.quantity += 1; + this.update_quantity(_item); + } else { + // add it to this.items + const _item = { + doc: item, + quantity: 1, + discount: 2, + rate: 2 + } + Object.assign(this.items, { + [item_code]: _item + }); + this.add_item_to_cart(_item); + } + } + + add_item_to_cart(item) { + this.wrapper.find('.cart-items .empty-state').hide(); + const $item = $(this.get_item_html(item)) + $item.appendTo(this.wrapper.find('.cart-items')); + // $item.addClass('added'); + // this.wrapper.find('.cart-items').append(this.get_item_html(item)) + } + + update_quantity(item) { + this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) + .text(item.quantity); + } + + remove_item(item_code) { + delete this.items[item_code]; + + // this.refresh(); + } + + refresh() { + const item_codes = Object.keys(this.items); + const html = item_codes + .map(item_code => this.get_item_html(item_code)) + .join(""); + this.wrapper.find('.cart-items').html(html); + } + + get_item_html(_item) { + + let item; + if (typeof _item === "object") { + item = _item; + } + else if (typeof _item === "string") { + item = this.items[_item]; + } + + return ` +
+
+ ${item.doc.item_name} +
+
+ ${item.quantity} +
+
+ ${item.discount} +
+
+ ${item.rate} +
+
+ `; + } +} + +erpnext.POSItems = class POSItems { + constructor(wrapper, events) { + this.wrapper = wrapper; + this.items = {}; + this.make_dom(); + this.make_fields(); + + this.init_clusterize(); + this.bind_events(events); + + // bootstrap with 20 items + this.get_items() + .then(items => { + this.items = items + }) + .then(() => this.render_items()); + } + + make_dom() { + this.wrapper.html(` +
+
+
+
+
+
+
+
+ `); + + this.items_wrapper = this.wrapper.find('.items-wrapper'); + this.items_wrapper.append(` +
+
+
+
+ `); + } + + make_fields() { + this.search_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Data', + label: 'Search Item', + onchange: (e) => { + const search_term = e.target.value; + this.filter_items(search_term); + } + }, + parent: this.wrapper.find('.search-field'), + render_input: true, + }); + + this.item_group_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Select', + label: 'Item Group', + options: [ + 'All Item Groups', + 'Raw Materials', + 'Finished Goods' + ], + default: 'All Item Groups' + }, + 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 = '
'; + 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 += '
'; + row_items.push(curr_row); + curr_row = row_container; + } + curr_row += all_items[i]; + } + + this.clusterize.update(row_items); + } + + filter_items(search_term) { + search_term = search_term.toLowerCase(); + + const filtered_items = + Object.values(this.items) + .filter( + item => item.item_name.toLowerCase().includes(search_term) + ); + this.render_items(filtered_items); + } + + bind_events(events) { + this.wrapper.on('click', '.pos-item-wrapper', function(e) { + const $item = $(this); + const item_code = $item.attr('data-item-code'); + events.item_click.apply(null, [item_code]); + }); + } + + get(item_code) { + return this.items[item_code]; + } + + get_all() { + return this.items; + } + + get_item_html(item) { + const { item_code, item_name, image: item_image, item_stock=0, item_price=0} = item; + const item_title = item_name || item_code; + + const template = ` + + `; + + // const template = ` + + //
+ //
+ // ${item_name} + // Stock: ${item_stock} + //
+ //
+ //
+ // ${item_image ? + // `${item_title}` : + // ` + // ${frappe.get_abbr(item_title)} + // ` + // } + //
+ //
+ //
+ + // `; + + return template; + } + + get_items(start = 0, page_length = 2000) { + return new Promise(res => { + frappe.call({ + method: "frappe.desk.reportview.get", + type: "GET", + args: { + doctype: "Item", + fields: [ + "`tabItem`.`name`", + "`tabItem`.`owner`", + "`tabItem`.`docstatus`", + "`tabItem`.`modified`", + "`tabItem`.`modified_by`", + "`tabItem`.`item_name`", + "`tabItem`.`item_code`", + "`tabItem`.`disabled`", + "`tabItem`.`item_group`", + "`tabItem`.`stock_uom`", + "`tabItem`.`image`", + "`tabItem`.`variant_of`", + "`tabItem`.`has_variants`", + "`tabItem`.`end_of_life`", + "`tabItem`.`total_projected_qty`" + ], + order_by: "`tabItem`.`modified` desc", + page_length: page_length, + start: start + } + }) + .then(r => { + const data = r.message; + const items = frappe.utils.dict(data.keys, data.values); + + // convert to key, value + let items_dict = {}; + items.map(item => { + items_dict[item.item_code] = item; + }); + + res(items_dict); + }); + }); + } +} \ No newline at end of file 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 0000000000..1e348c09af --- /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 From 03e7ec29e75a4d2746c4a80b20ec41d419b68930 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 17:17:34 +0530 Subject: [PATCH 08/36] Add clusterize, move customer field to POSCart, get POS profile --- erpnext/public/css/pos.css | 2 +- erpnext/public/js/pos/clusterize.js | 329 ++++++++++++++++++ erpnext/public/less/pos.less | 2 +- .../page/point_of_sale/point_of_sale.js | 167 +++++---- 4 files changed, 428 insertions(+), 72 deletions(-) create mode 100644 erpnext/public/js/pos/clusterize.js diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index d44b17caaf..4ba00ba227 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -1,7 +1,7 @@ .pos { padding: 15px; } -.customer-container { +.cart-container { padding: 0 15px; display: inline-block; width: 39%; diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js new file mode 100644 index 0000000000..6d331ba761 --- /dev/null +++ b/erpnext/public/js/pos/clusterize.js @@ -0,0 +1,329 @@ +/*! 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 = '', + 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 = '' + data + '
'; + 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/less/pos.less b/erpnext/public/less/pos.less index c94f8b59f5..e627dbc080 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -5,7 +5,7 @@ padding: 15px; } -.customer-container { +.cart-container { padding: 0 15px; // flex: 2; display: inline-block; diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c9ef30e7eb..fb3f666e8a 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -15,25 +15,26 @@ erpnext.PointOfSale = class PointOfSale { this.page = wrapper.page; const assets = [ - 'assets/frappe/js/lib/clusterize.js', + 'assets/erpnext/js/pos/clusterize.js', 'assets/erpnext/css/pos.css' ]; frappe.require(assets, () => { - this.prepare(); - this.make(); - this.bind_events(); + this.prepare().then(() => { + this.make(); + this.bind_events(); + }); }); } prepare() { this.set_online_status(); this.prepare_menu(); + return this.get_pos_profile(); } make() { this.make_dom(); - this.make_customer_field(); this.make_cart(); this.make_items(); } @@ -52,6 +53,66 @@ erpnext.PointOfSale = class PointOfSale { }); } + make_dom() { + this.wrapper.append(` +
+
+ +
+
+ +
+
+ `); + } + + make_cart() { + this.cart = new erpnext.POSCart(this.wrapper.find('.cart-container')); + } + + make_items() { + this.items = new erpnext.POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + item_click: (item_code) => this.add_item_to_cart(item_code) + } + }); + } + + add_item_to_cart(item_code) { + const item = this.items.get(item_code); + this.cart.add_item(item); + } + + bind_events() { + + } + + get_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; + }); + } + + make_sales_invoice_frm() { + const dt = 'Sales Invoice'; + return new Promise(resolve => { + frappe.model.with_doctype(dt, function() { + const page = $('
'); + const frm = new _f.Frm(dt, page, false); + const name = frappe.model.make_new_doc_and_get_name(dt, true); + frm.refresh(name); + resolve(frm); + }); + }); + } + prepare_menu() { this.page.clear_menu(); @@ -76,19 +137,38 @@ erpnext.PointOfSale = class PointOfSale { frappe.set_route('List', 'POS Profile'); }); } +} + +erpnext.POSCart = class POSCart { + constructor(wrapper) { + this.wrapper = wrapper; + this.items = {}; + this.make(); + } + + make() { + this.make_dom(); + this.make_customer_field(); + } make_dom() { this.wrapper.append(` -
-
-
+
+
+
+
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
-
+
+
+ No Items added to cart +
-
-
- -
+
`); } @@ -105,61 +185,6 @@ erpnext.PointOfSale = class PointOfSale { }); } - make_cart() { - this.cart = new erpnext.POSCart(this.wrapper.find('.cart-wrapper')); - } - - make_items() { - this.items = new erpnext.POSItems(this.wrapper.find('.item-container'), { - item_click: (item_code) => this.add_item_to_cart(item_code) - }); - } - - add_item_to_cart(item_code) { - const item = this.items.get(item_code); - this.cart.add_item(item); - } - - bind_events() { - - } - - make_sales_invoice_frm() { - const dt = 'Sales Invoice'; - frappe.model.with_doctype(dt, function() { - const page = $('
'); - const frm = new _f.Frm(dt, page, false); - const name = frappe.model.make_new_doc_and_get_name(dt, true); - frm.refresh(name); - }); - } -} - -erpnext.POSCart = class POSCart { - constructor(wrapper) { - this.wrapper = wrapper; - this.items = {}; - this.make(); - } - - make() { - this.wrapper.append(` -
-
-
${__('Item Name')}
-
${__('Quantity')}
-
${__('Discount')}
-
${__('Rate')}
-
-
-
- No Items added to cart -
-
-
- `); - } - add_item(item) { const { item_code } = item; const _item = this.items[item_code]; @@ -240,9 +265,11 @@ erpnext.POSCart = class POSCart { } erpnext.POSItems = class POSItems { - constructor(wrapper, events) { + constructor({wrapper, pos_profile, events}) { this.wrapper = wrapper; + this.pos_profile = pos_profile; this.items = {}; + this.make_dom(); this.make_fields(); @@ -432,7 +459,7 @@ erpnext.POSItems = class POSItems { return template; } - get_items(start = 0, page_length = 2000) { + get_items(start = 0, page_length = 20) { return new Promise(res => { frappe.call({ method: "frappe.desk.reportview.get", From a0f8687945806119c8ca9a318f4097abad00c899 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Aug 2017 18:28:05 +0530 Subject: [PATCH 09/36] Styling for search fields --- erpnext/public/css/pos.css | 11 +++++++- erpnext/public/less/pos.less | 22 ++++++++------- .../page/point_of_sale/point_of_sale.js | 27 +++++++++++++------ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 4ba00ba227..81b109838f 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -13,7 +13,14 @@ 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 .list-item__content:not(:first-child) { @@ -31,7 +38,9 @@ overflow: auto; } .pos-item-wrapper { - height: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; } .image-view-container { display: block; diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index e627dbc080..1ae0dfd993 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -21,7 +21,16 @@ vertical-align: top; } +.search-field { + width: 60%; + + input::placeholder { + font-size: @text-medium; + } +} + .item-group-field { + width: 40%; margin-left: 15px; } @@ -34,15 +43,6 @@ .cart-items { height: 200px; overflow: auto; - - // .list-item { - // background-color: @light-yellow; - // transition: background-color 1s linear; - // } - - // .list-item.added { - // background-color: white; - // } } .fields { @@ -55,7 +55,9 @@ } .pos-item-wrapper { - height: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; } .image-view-container { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index fb3f666e8a..1fb8c1ec0b 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -306,19 +306,28 @@ erpnext.POSItems = class POSItems { } make_fields() { + // Search field this.search_field = frappe.ui.form.make_control({ df: { fieldtype: 'Data', - label: 'Search Item', - onchange: (e) => { - const search_term = e.target.value; - this.filter_items(search_term); - } + 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) => { + const search_term = e.target.value; + this.filter_items(search_term); + }); + + + // Item group field this.item_group_field = frappe.ui.form.make_control({ df: { fieldtype: 'Select', @@ -370,9 +379,11 @@ erpnext.POSItems = class POSItems { const filtered_items = Object.values(this.items) - .filter( - item => item.item_name.toLowerCase().includes(search_term) - ); + .filter(item => { + return item.item_code.toLowerCase().includes(search_term) || + item.item_name.toLowerCase().includes(search_term) + }); + this.render_items(filtered_items); } From 7bcb1cfc428f79114b6a7ef747daa895509d518f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 11 Aug 2017 15:49:23 +0530 Subject: [PATCH 10/36] more styling --- erpnext/public/css/pos.css | 5 ++++- erpnext/public/less/pos.less | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 81b109838f..399613d497 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -35,7 +35,10 @@ } .pos-items-wrapper { max-height: 480px; - overflow: auto; + overflow-y: auto; +} +.pos-items { + overflow: hidden; } .pos-item-wrapper { display: flex; diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 1ae0dfd993..9358f0a24c 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -51,7 +51,11 @@ .pos-items-wrapper { max-height: 480px; - overflow: auto; + overflow-y: auto; +} + +.pos-items { + overflow: hidden; } .pos-item-wrapper { From b1daab428409b29fb39dc1b4e4acf59f6f303e38 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 11 Aug 2017 18:44:19 +0530 Subject: [PATCH 11/36] Added functional part --- .../selling/page/point_of_sale/point_of_sale.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 1fb8c1ec0b..c28a5bd8c6 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -30,6 +30,7 @@ erpnext.PointOfSale = class PointOfSale { prepare() { this.set_online_status(); this.prepare_menu(); + this.make_sales_invoice_frm() return this.get_pos_profile(); } @@ -108,6 +109,7 @@ erpnext.PointOfSale = class PointOfSale { const frm = new _f.Frm(dt, page, false); const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); + frm.doc.items = []; resolve(frm); }); }); @@ -178,7 +180,10 @@ erpnext.POSCart = class POSCart { df: { fieldtype: 'Link', label: 'Customer', - options: 'Customer' + options: 'Customer', + onchange: (e) => { + cur_frm.set_value('customer', this.customer_field.value); + } }, parent: this.wrapper.find('.customer-field'), render_input: true @@ -195,6 +200,10 @@ erpnext.POSCart = class POSCart { this.update_quantity(_item); } else { // add it to this.items + item['qty'] = 1; + this.child = cur_frm.add_child('items', item) + cur_frm.script_manager.trigger("item_code", this.child.doctype, this.child.name); + const _item = { doc: item, quantity: 1, @@ -219,6 +228,12 @@ erpnext.POSCart = class POSCart { update_quantity(item) { this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) .text(item.quantity); + + $.each(cur_frm.doc["items"] || [], function(i, d) { + if (d.item_code == item.doc.item_code) { + frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); + } + }); } remove_item(item_code) { From 0f3d43147658647f169c956e8cc3fdcb07110162 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 14 Aug 2017 18:25:59 +0530 Subject: [PATCH 12/36] Update cart ui from cur_frm, Add number pad --- erpnext/public/css/pos.css | 42 +++ erpnext/public/less/pos.less | 44 +++ .../page/point_of_sale/point_of_sale.js | 334 ++++++++++++------ 3 files changed, 305 insertions(+), 115 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 399613d497..de1e097bb8 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -23,6 +23,9 @@ width: 40%; margin-left: 15px; } +.cart-wrapper { + margin-bottom: 10px; +} .cart-wrapper .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -30,6 +33,10 @@ height: 200px; overflow: auto; } +.cart-items input { + height: 22px; + font-size: 12px; +} .fields { display: flex; } @@ -63,3 +70,38 @@ 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; +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 9358f0a24c..de165144f1 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -35,6 +35,7 @@ } .cart-wrapper { + margin-bottom: 10px; .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -43,6 +44,11 @@ .cart-items { height: 200px; overflow: auto; + + input { + height: 22px; + font-size: @text-medium; + } } .fields { @@ -84,4 +90,42 @@ 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; + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c28a5bd8c6..19eb70ee81 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,3 +1,5 @@ +/* global Clusterize */ + frappe.pages['point-of-sale'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, @@ -5,11 +7,11 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { single_column: true }); - wrapper.pos = new erpnext.PointOfSale(wrapper); - cur_pos = wrapper.pos; + wrapper.pos = new PointOfSale(wrapper); + window.cur_pos = wrapper.pos; } -erpnext.PointOfSale = class PointOfSale { +class PointOfSale { constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -20,24 +22,25 @@ erpnext.PointOfSale = class PointOfSale { ]; frappe.require(assets, () => { - this.prepare().then(() => { - this.make(); - this.bind_events(); - }); + this.make(); }); } - prepare() { - this.set_online_status(); - this.prepare_menu(); - this.make_sales_invoice_frm() - return this.get_pos_profile(); - } - make() { - this.make_dom(); - this.make_cart(); - this.make_items(); + return frappe.run_serially([ + () => { + this.prepare_dom(); + this.prepare_menu(); + this.set_online_status(); + }, + () => this.make_sales_invoice_frm(), + () => this.setup_pos_profile(), + () => { + this.make_cart(); + this.make_items(); + this.bind_events(); + } + ]); } set_online_status() { @@ -54,7 +57,7 @@ erpnext.PointOfSale = class PointOfSale { }); } - make_dom() { + prepare_dom() { this.wrapper.append(`
@@ -68,29 +71,64 @@ erpnext.PointOfSale = class PointOfSale { } make_cart() { - this.cart = new erpnext.POSCart(this.wrapper.find('.cart-container')); - } - - make_items() { - this.items = new erpnext.POSItems({ - wrapper: this.wrapper.find('.item-container'), - pos_profile: this.pos_profile, + this.cart = new POSCart({ + wrapper: this.wrapper.find('.cart-container'), events: { - item_click: (item_code) => this.add_item_to_cart(item_code) + customer_change: (customer) => this.cur_frm.set_value('customer', customer), + increase_qty: (item_code) => { + this.add_item_to_cart(item_code); + }, + decrease_qty: (item_code) => { + this.add_item_to_cart(item_code, -1); + } } }); } - add_item_to_cart(item_code) { - const item = this.items.get(item_code); - this.cart.add_item(item); + make_items() { + this.items = new POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + item_click: (item_code) => { + if(!this.cur_frm.doc.customer) { + frappe.throw(__('Please select a customer')); + } + this.add_item_to_cart(item_code); + } + } + }); + } + + add_item_to_cart(item_code, qty = 1) { + + if(this.cart.exists(item_code)) { + // increase qty by 1 + this.cur_frm.doc.items.forEach((item) => { + if (item.item_code === item_code) { + frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty); + // update cart + this.cart.add_item(item); + } + }); + return; + } + + // add to cur_frm + const item = this.cur_frm.add_child('items', { item_code: item_code }); + this.cur_frm.script_manager + .trigger('item_code', item.doctype, item.name) + .then(() => { + // update cart + this.cart.add_item(item); + }); } bind_events() { } - get_pos_profile() { + setup_pos_profile() { return frappe.call({ method: 'erpnext.stock.get_item_details.get_pos_profile', args: { @@ -104,13 +142,14 @@ erpnext.PointOfSale = class PointOfSale { make_sales_invoice_frm() { const dt = 'Sales Invoice'; return new Promise(resolve => { - frappe.model.with_doctype(dt, function() { + frappe.model.with_doctype(dt, () => { const page = $('
'); const frm = new _f.Frm(dt, page, false); const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); frm.doc.items = []; - resolve(frm); + this.cur_frm = frm; + resolve(); }); }); } @@ -141,16 +180,18 @@ erpnext.PointOfSale = class PointOfSale { } } -erpnext.POSCart = class POSCart { - constructor(wrapper) { +class POSCart { + constructor({wrapper, events}) { this.wrapper = wrapper; - this.items = {}; + this.events = events; this.make(); + this.bind_events(); } make() { this.make_dom(); this.make_customer_field(); + this.make_numpad(); } make_dom() { @@ -172,7 +213,10 @@ erpnext.POSCart = class POSCart {
+
+
`); + this.$cart_items = this.wrapper.find('.cart-items'); } make_customer_field() { @@ -181,8 +225,9 @@ erpnext.POSCart = class POSCart { fieldtype: 'Link', label: 'Customer', options: 'Customer', + reqd: 1, onchange: (e) => { - cur_frm.set_value('customer', this.customer_field.value); + this.events.customer_change.apply(null, [this.customer_field.get_value()]); } }, parent: this.wrapper.find('.customer-field'), @@ -190,96 +235,113 @@ erpnext.POSCart = class POSCart { }); } - add_item(item) { - const { item_code } = item; - const _item = this.items[item_code]; - - if (_item) { - // exists, increase quantity - _item.quantity += 1; - this.update_quantity(_item); - } else { - // add it to this.items - item['qty'] = 1; - this.child = cur_frm.add_child('items', item) - cur_frm.script_manager.trigger("item_code", this.child.doctype, this.child.name); - - const _item = { - doc: item, - quantity: 1, - discount: 2, - rate: 2 + make_numpad() { + this.numpad = new NumberPad({ + wrapper: this.wrapper.find('.number-pad-container'), + onclick: (btn_value) => { + // on click + console.log(btn_value); } - Object.assign(this.items, { - [item_code]: _item - }); - this.add_item_to_cart(_item); - } + }); } - add_item_to_cart(item) { + add_item(item) { this.wrapper.find('.cart-items .empty-state').hide(); - const $item = $(this.get_item_html(item)) - $item.appendTo(this.wrapper.find('.cart-items')); - // $item.addClass('added'); - // this.wrapper.find('.cart-items').append(this.get_item_html(item)) - } - update_quantity(item) { - this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) - .text(item.quantity); - - $.each(cur_frm.doc["items"] || [], function(i, d) { - if (d.item_code == item.doc.item_code) { - frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); - } - }); - } - - remove_item(item_code) { - delete this.items[item_code]; - - // this.refresh(); - } - - refresh() { - const item_codes = Object.keys(this.items); - const html = item_codes - .map(item_code => this.get_item_html(item_code)) - .join(""); - this.wrapper.find('.cart-items').html(html); - } - - get_item_html(_item) { - - let item; - if (typeof _item === "object") { - item = _item; - } - else if (typeof _item === "string") { - item = this.items[_item]; + 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) { + $item.find('.quantity input').val(item.qty); + $item.find('.discount').text(item.discount_percentage); + $item.find('.rate').text(item.rate); + } else { + $item.remove(); + } + } + + 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}"]`); + const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + this.$cart_items.animate({ scrollTop }); + } + + get_item_html(item) { return ` -
+
- ${item.doc.item_name} + ${item.item_name}
- ${item.quantity} + ${get_quantity_html(item.qty)}
- ${item.discount} + ${item.discount_percentage}%
${item.rate}
`; + + function get_quantity_html(value) { + return ` +
+ + + + + + + + + +
+ `; + } + } + + bind_events() { + const events = this.events; + 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.increase_qty(item_code); + } else if(action === 'decrement') { + events.decrease_qty(item_code); + } + }); } } -erpnext.POSItems = class POSItems { +class POSItems { constructor({wrapper, pos_profile, events}) { this.wrapper = wrapper; this.pos_profile = pos_profile; @@ -439,16 +501,10 @@ erpnext.POSItems = class POSItems {
- ${!item_image ? - ` + ${!item_image ? ` ${frappe.get_abbr(item_title)} - ` : - '' - } - ${item_image ? - `${item_title}` : - '' - } + ` : '' } + ${item_image ? `${item_title}` : '' }
${item_price} @@ -509,12 +565,12 @@ erpnext.POSItems = class POSItems { "`tabItem`.`end_of_life`", "`tabItem`.`total_projected_qty`" ], + filters: [['disabled', '=', '0']], order_by: "`tabItem`.`modified` desc", page_length: page_length, start: start } - }) - .then(r => { + }).then(r => { const data = r.message; const items = frappe.utils.dict(data.keys, data.values); @@ -528,4 +584,52 @@ erpnext.POSItems = class POSItems { }); }); } +} + +class NumberPad { + constructor({wrapper, onclick}) { + this.wrapper = wrapper; + this.onclick = onclick; + this.make_dom(); + this.bind_events(); + } + + make_dom() { + const button_array = [ + [1, 2, 3, 'Qty'], + [4, 5, 6, 'Disc'], + [7, 8, 9, 'Price'], + ['Del', 0, '.', 'Pay'] + ]; + + this.wrapper.html(` +
+ ${button_array.map(get_row).join("")} +
+ `); + + function get_row(row) { + return '
' + row.map(get_col).join("") + '
'; + } + + function get_col(col) { + return `
${col}
`; + } + } + + bind_events() { + // bind click event + const me = this; + this.wrapper.on('click', '.num-col', function() { + const $btn = $(this); + me.highlight_button($btn); + me.onclick.apply(null, [$btn.attr('data-value')]); + }); + } + + highlight_button($btn) { + // const $btn = this.wrapper.find(`[data-value="${value}"]`); + $btn.addClass('highlight'); + setTimeout(() => $btn.removeClass('highlight'), 1000); + } } \ No newline at end of file From f03a73466cd15942c4d9b291a5e5bd128a174c1d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Aug 2017 12:15:19 +0530 Subject: [PATCH 13/36] Added modal for multi mode payment, provision to add write off and change amount --- .../page/point_of_sale/point_of_sale.js | 183 +++++++++++++++++- 1 file changed, 179 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 19eb70ee81..f4cc355624 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -106,9 +106,11 @@ class PointOfSale { // increase qty by 1 this.cur_frm.doc.items.forEach((item) => { if (item.item_code === item_code) { - frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty); + frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty) + .then(() => { + this.cart.add_item(item); + }) // update cart - this.cart.add_item(item); } }); return; @@ -149,6 +151,7 @@ class PointOfSale { frm.refresh(name); frm.doc.items = []; this.cur_frm = frm; + this.cur_frm.set_value('is_pos', 1) resolve(); }); }); @@ -240,11 +243,21 @@ class POSCart { wrapper: this.wrapper.find('.number-pad-container'), onclick: (btn_value) => { // on click + if (btn_value == 'Pay') { + this.make_payment() + } + console.log(btn_value); } }); } + make_payment() { + this.payment = new MakePayment({ + frm: cur_frm + }) + } + add_item(item) { this.wrapper.find('.cart-items .empty-state').hide(); @@ -268,6 +281,7 @@ class POSCart { $item.find('.rate').text(item.rate); } else { $item.remove(); + frappe.model.clear_doc(item.doctype, item.name) } } @@ -284,8 +298,8 @@ class POSCart { scroll_to_item(item_code) { const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); - const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); - this.$cart_items.animate({ scrollTop }); + // const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + // this.$cart_items.animate({ scrollTop }); } get_item_html(item) { @@ -632,4 +646,165 @@ class NumberPad { $btn.addClass('highlight'); setTimeout(() => $btn.removeClass('highlight'), 1000); } +} + +class MakePayment { + constructor({frm}) { + this.frm = frm + this.make(); + this.set_primary_action(); + this.show_total_amount(); + // this.show_outstanding_amount() + } + + make() { + const me = this; + this.update_flag() + + this.dialog = new frappe.ui.Dialog({ + title: __('Payment'), + fields: this.get_fields(), + width:800 + }); + + this.dialog.show(); + this.$body = this.dialog.body; + + this.numpad = new NumberPad({ + wrapper: $(this.$body).find('[data-fieldname = "numpad"]'), + onclick: (btn_value) => { + // on click + } + }); + } + + set_primary_action() { + this.dialog.set_primary_action(__("Submit"), function() { + //save form + }) + } + + get_fields() { + const me = this; + const total_amount = [ + { + fieldtype: 'HTML', + fieldname: "total_amount", + }, + { + fieldtype: 'Section Break', + label: __("Mode of Payments") + }, + ] + + const mode_of_paymen_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: (e) => { + const fieldname = $(e.target).attr('data-fieldname'); + const value = this.dialog.get_value(fieldname); + me.update_payment_value(fieldname, value) + } + } + }) + + const other_fields = [ + { + 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: (e) => { + 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: (e) => { + me.update_cur_frm_value('change_amount', () => { + frappe.flags.write_off_amount = false; + me.update_write_off_amount() + }) + } + }, + ] + + $.merge(total_amount, mode_of_paymen_fields) + return $.merge(total_amount, other_fields) + } + + update_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) + } + + update_write_off_amount() { + this.dialog.set_value("write_off_amount", this.frm.doc.write_off_amount) + } + + show_total_amount() { + const grand_total = format_currency(this.frm.doc.grand_total, this.frm.doc.currency) + const template = ` +

+ ${ __("Total Amount") }: + ${__(grand_total)} +

+ ` + this.total_amount_section = $(this.$body).find("[data-fieldname = 'total_amount']") + this.total_amount_section.append(template) + } } \ No newline at end of file From 78c81d9c6c51c44440cfcc128e3c6e850b07fa1d Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 21 Aug 2017 15:05:33 +0530 Subject: [PATCH 14/36] Refactor payment code --- .../page/point_of_sale/point_of_sale.js | 177 +++++++++--------- 1 file changed, 92 insertions(+), 85 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 41d7e3ce83..84ee8f5f17 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -9,7 +9,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { wrapper.pos = new PointOfSale(wrapper); window.cur_pos = wrapper.pos; -} +}; class PointOfSale { constructor(wrapper) { @@ -74,12 +74,20 @@ class PointOfSale { this.cart = new POSCart({ wrapper: this.wrapper.find('.cart-container'), events: { - customer_change: (customer) => this.cur_frm.set_value('customer', customer), + customer_change: (customer) => this.frm.set_value('customer', customer), increase_qty: (item_code) => { this.add_item_to_cart(item_code); }, decrease_qty: (item_code) => { this.add_item_to_cart(item_code, -1); + }, + on_numpad: (value) => { + if (value == 'Pay') { + if (!this.payment) { + this.make_payment_modal(); + } + this.payment.open_modal(); + } } } }); @@ -91,7 +99,7 @@ class PointOfSale { pos_profile: this.pos_profile, events: { item_click: (item_code) => { - if(!this.cur_frm.doc.customer) { + if(!this.frm.doc.customer) { frappe.throw(__('Please select a customer')); } this.add_item_to_cart(item_code); @@ -104,21 +112,25 @@ class PointOfSale { if(this.cart.exists(item_code)) { // increase qty by 1 - this.cur_frm.doc.items.forEach((item) => { + this.frm.doc.items.forEach((item) => { if (item.item_code === item_code) { - frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty) + const final_qty = item.qty + qty; + frappe.model.set_value(item.doctype, item.name, 'qty', final_qty) .then(() => { + if (final_qty === 0) { + frappe.model.clear_doc(item.doctype, item.name); + } + // update cart this.cart.add_item(item); - }) - // update cart + }); } }); return; } // add to cur_frm - const item = this.cur_frm.add_child('items', { item_code: item_code }); - this.cur_frm.script_manager + const item = this.frm.add_child('items', { item_code: item_code }); + this.frm.script_manager .trigger('item_code', item.doctype, item.name) .then(() => { // update cart @@ -126,6 +138,10 @@ class PointOfSale { }); } + make_payment_modal() { + this.payment = new Payment(this.frm); + } + bind_events() { } @@ -150,8 +166,8 @@ class PointOfSale { const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); frm.doc.items = []; - this.cur_frm = frm; - this.cur_frm.set_value('is_pos', 1) + this.frm = frm; + this.frm.set_value('is_pos', 1); resolve(); }); }); @@ -167,7 +183,7 @@ class PointOfSale { this.page.add_menu_item(__("New Sales Invoice"), function () { // - }) + }); this.page.add_menu_item(__("Sync Master Data"), function () { // @@ -229,7 +245,7 @@ class POSCart { label: 'Customer', options: 'Customer', reqd: 1, - onchange: (e) => { + onchange: () => { this.events.customer_change.apply(null, [this.customer_field.get_value()]); } }, @@ -243,19 +259,21 @@ class POSCart { wrapper: this.wrapper.find('.number-pad-container'), onclick: (btn_value) => { // on click - if (btn_value == 'Pay') { - this.make_payment() + if (['Qty', 'Disc', 'Price'].includes(btn_value)) { + this.set_input_active(btn_value); } - console.log(btn_value); + this.events.on_numpad.apply(null, [btn_value]); } }); } - make_payment() { - this.payment = new MakePayment({ - frm: cur_frm - }) + set_input_active(btn_value) { + if (!this.selected_item) return; + + if (btn_value === 'Qty') { + this.selected_item.find('.quantity input').css('border', '1px solid blue'); + } } add_item(item) { @@ -281,7 +299,6 @@ class POSCart { $item.find('.rate').text(item.rate); } else { $item.remove(); - frappe.model.clear_doc(item.doctype, item.name) } } @@ -352,6 +369,10 @@ class POSCart { events.decrease_qty(item_code); } }); + const me = this; + this.$cart_items.on('click', '.list-item', function() { + me.selected_item = $(this); + }); } } @@ -528,30 +549,6 @@ class POSItems {
`; - // const template = ` - - //
- //
- // ${item_name} - // Stock: ${item_stock} - //
- //
- //
- // ${item_image ? - // `${item_title}` : - // ` - // ${frappe.get_abbr(item_title)} - // ` - // } - //
- //
- //
- - // `; - return template; } @@ -601,24 +598,27 @@ class POSItems { } class NumberPad { - constructor({wrapper, onclick}) { + constructor({wrapper, onclick, button_array}) { this.wrapper = wrapper; this.onclick = onclick; + this.button_array = button_array; this.make_dom(); this.bind_events(); } make_dom() { - const button_array = [ - [1, 2, 3, 'Qty'], - [4, 5, 6, 'Disc'], - [7, 8, 9, 'Price'], - ['Del', 0, '.', 'Pay'] - ]; + if (!this.button_array) { + this.button_array = [ + [1, 2, 3, 'Qty'], + [4, 5, 6, 'Disc'], + [7, 8, 9, 'Price'], + ['Del', 0, '.', 'Pay'] + ]; + } this.wrapper.html(`
- ${button_array.map(get_row).join("")} + ${this.button_array.map(get_row).join("")}
`); @@ -648,30 +648,38 @@ class NumberPad { } } -class MakePayment { - constructor({frm}) { - this.frm = frm +class Payment { + constructor(frm) { + this.frm = frm; this.make(); this.set_primary_action(); - this.show_total_amount(); // this.show_outstanding_amount() } + open_modal() { + this.show_total_amount(); + this.dialog.show(); + } + make() { - const me = this; - this.update_flag() + this.set_flag(); this.dialog = new frappe.ui.Dialog({ title: __('Payment'), fields: this.get_fields(), - width:800 + width: 800 }); - this.dialog.show(); this.$body = this.dialog.body; this.numpad = new NumberPad({ - wrapper: $(this.$body).find('[data-fieldname = "numpad"]'), + wrapper: $(this.$body).find('[data-fieldname="numpad"]'), + button_array: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ['Del', 0, '.'], + ], onclick: (btn_value) => { // on click } @@ -680,24 +688,24 @@ class MakePayment { set_primary_action() { this.dialog.set_primary_action(__("Submit"), function() { - //save form - }) + // save form + }); } get_fields() { const me = this; - const total_amount = [ + let fields = [ { fieldtype: 'HTML', - fieldname: "total_amount", + fieldname: 'total_amount', }, { fieldtype: 'Section Break', - label: __("Mode of Payments") + label: __('Mode of Payments') }, - ] + ]; - const mode_of_paymen_fields = this.frm.doc.payments.map(p => { + fields = fields.concat(this.frm.doc.payments.map(p => { return { fieldtype: 'Currency', label: __(p.mode_of_payment), @@ -707,12 +715,12 @@ class MakePayment { onchange: (e) => { const fieldname = $(e.target).attr('data-fieldname'); const value = this.dialog.get_value(fieldname); - me.update_payment_value(fieldname, value) + me.update_payment_value(fieldname, value); } - } - }) + }; + })); - const other_fields = [ + fields = fields.concat([ { fieldtype: 'Column Break', }, @@ -729,11 +737,11 @@ class MakePayment { options: me.frm.doc.currency, fieldname: "write_off_amount", default: me.frm.doc.write_off_amount, - onchange: (e) => { + onchange: () => { me.update_cur_frm_value('write_off_amount', () => { frappe.flags.change_amount = false; me.update_change_amount() - }) + }); } }, { @@ -745,20 +753,19 @@ class MakePayment { options: me.frm.doc.currency, fieldname: "change_amount", default: me.frm.doc.change_amount, - onchange: (e) => { + onchange: () => { me.update_cur_frm_value('change_amount', () => { frappe.flags.write_off_amount = false; - me.update_write_off_amount() - }) + me.update_write_off_amount(); + }); } }, - ] + ]); - $.merge(total_amount, mode_of_paymen_fields) - return $.merge(total_amount, other_fields) + return fields; } - update_flag() { + set_flag() { frappe.flags.write_off_amount = true; frappe.flags.change_amount = true; } @@ -797,14 +804,14 @@ class MakePayment { } show_total_amount() { - const grand_total = format_currency(this.frm.doc.grand_total, this.frm.doc.currency) + const grand_total = format_currency(this.frm.doc.grand_total, this.frm.doc.currency); const template = `

${ __("Total Amount") }: ${__(grand_total)}

` - this.total_amount_section = $(this.$body).find("[data-fieldname = 'total_amount']") - this.total_amount_section.append(template) + this.total_amount_section = $(this.$body).find("[data-fieldname = 'total_amount']"); + this.total_amount_section.html(template); } } \ No newline at end of file From b73321c577854a3d7948484963357e7a698938b5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 23 Aug 2017 12:15:10 +0530 Subject: [PATCH 15/36] Item search refactor --- erpnext/patches.txt | 5 +- .../page/point_of_sale/point_of_sale.js | 120 +++++++++--------- .../page/point_of_sale/point_of_sale.py | 46 +++++++ 3 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 erpnext/selling/page/point_of_sale/point_of_sale.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 56af066c54..60caf0fec1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -431,7 +431,4 @@ erpnext.patches.v8_5.set_default_mode_of_payment erpnext.patches.v8_5.update_customer_group_in_POS_profile erpnext.patches.v8_6.update_timesheet_company_from_PO erpnext.patches.v8_6.set_write_permission_for_quotation_for_sales_manager -<<<<<<< HEAD -erpnext.patches.v8_5.remove_project_type_property_setter -======= ->>>>>>> Set write permission to sales manger for permlevel 1 in Quotation doctype +erpnext.patches.v8_5.remove_project_type_property_setter \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 84ee8f5f17..00f886512a 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -39,6 +39,7 @@ class PointOfSale { this.make_cart(); this.make_items(); this.bind_events(); + this.disable_text_box_and_button(); } ]); } @@ -72,6 +73,7 @@ class PointOfSale { make_cart() { this.cart = new POSCart({ + frm: this.frm, wrapper: this.wrapper.find('.cart-container'), events: { customer_change: (customer) => this.frm.set_value('customer', customer), @@ -93,6 +95,10 @@ class PointOfSale { }); } + disable_text_box_and_button() { + $(this.wrapper).find('input, button').prop("disabled", !(this.frm.doc.docstatus===0)); + } + make_items() { this.items = new POSItems({ wrapper: this.wrapper.find('.item-container'), @@ -108,21 +114,28 @@ class PointOfSale { }); } - add_item_to_cart(item_code, qty = 1) { + add_item_to_cart(item_code, qty = 1, barcode) { if(this.cart.exists(item_code)) { // increase qty by 1 this.frm.doc.items.forEach((item) => { if (item.item_code === item_code) { - const final_qty = item.qty + qty; - frappe.model.set_value(item.doctype, item.name, 'qty', final_qty) - .then(() => { - if (final_qty === 0) { - frappe.model.clear_doc(item.doctype, item.name); - } - // update cart - this.cart.add_item(item); - }); + if (barcode) { + value = barcode['serial_no'] ? + item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no']; + frappe.model.set_value(item.doctype, item.name, + Object.keys(barcode)[0], final_qty); + } else { + const final_qty = item.qty + qty; + frappe.model.set_value(item.doctype, item.name, 'qty', final_qty) + .then(() => { + if (final_qty === 0) { + frappe.model.clear_doc(item.doctype, item.name); + } + // update cart + this.cart.add_item(item); + }); + } } }); return; @@ -200,7 +213,8 @@ class PointOfSale { } class POSCart { - constructor({wrapper, events}) { + constructor({frm, wrapper, events}) { + this.frm = frm; this.wrapper = wrapper; this.events = events; this.make(); @@ -301,25 +315,9 @@ class POSCart { $item.remove(); } } - - 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}"]`); - // const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); - // this.$cart_items.animate({ scrollTop }); - } - + get_item_html(item) { + const rate = format_currency(item.rate, this.frm.doc.currency); return `
@@ -332,7 +330,7 @@ class POSCart { ${item.discount_percentage}%
- ${item.rate} + ${rate}
`; @@ -354,6 +352,23 @@ class POSCart { } } + 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}"]`); + // const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + // this.$cart_items.animate({ scrollTop }); + } + bind_events() { const events = this.events; this.$cart_items.on('click', @@ -381,6 +396,8 @@ class POSItems { this.wrapper = wrapper; this.pos_profile = pos_profile; this.items = {}; + this.currency = this.pos_profile.currency || + frappe.defaults.get_default('currency'); this.make_dom(); this.make_fields(); @@ -516,7 +533,8 @@ class POSItems { } get_item_html(item) { - const { item_code, item_name, image: item_image, item_stock=0, item_price=0} = item; + const price_list_rate = format_currency(item.price_list_rate, this.currency) + const { item_code, item_name, item_image, item_stock=0} = item; const item_title = item_name || item_code; const template = ` @@ -542,7 +560,7 @@ class POSItems { ${item_image ? `${item_title}` : '' }
- ${item_price} + ${price_list_rate}
@@ -552,38 +570,17 @@ class POSItems { return template; } - get_items(start = 0, page_length = 20) { + get_items(start = 10, page_length = 20) { + var me = this; return new Promise(res => { frappe.call({ - method: "frappe.desk.reportview.get", - type: "GET", + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", args: { - doctype: "Item", - fields: [ - "`tabItem`.`name`", - "`tabItem`.`owner`", - "`tabItem`.`docstatus`", - "`tabItem`.`modified`", - "`tabItem`.`modified_by`", - "`tabItem`.`item_name`", - "`tabItem`.`item_code`", - "`tabItem`.`disabled`", - "`tabItem`.`item_group`", - "`tabItem`.`stock_uom`", - "`tabItem`.`image`", - "`tabItem`.`variant_of`", - "`tabItem`.`has_variants`", - "`tabItem`.`end_of_life`", - "`tabItem`.`total_projected_qty`" - ], - filters: [['disabled', '=', '0']], - order_by: "`tabItem`.`modified` desc", - page_length: page_length, - start: start + 'price_list': this.pos_profile.selling_price_list, + 'item': me.search_field.$input.value || "" } }).then(r => { - const data = r.message; - const items = frappe.utils.dict(data.keys, data.values); + const items = r.message; // convert to key, value let items_dict = {}; @@ -687,8 +684,11 @@ class Payment { } set_primary_action() { + var me = this; + this.dialog.set_primary_action(__("Submit"), function() { - // save form + this.frm.doc.savesubmit() + this.dialog.hide() }); } 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 0000000000..045ef07a73 --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -0,0 +1,46 @@ +# 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 +from frappe import _ +from frappe.utils import nowdate +from erpnext.setup.utils import get_exchange_rate +from frappe.core.doctype.communication.email import make +from erpnext.stock.get_item_details import get_pos_profile +from erpnext.accounts.party import get_party_account_currency +from erpnext.controllers.accounts_controller import get_taxes_and_charges + +@frappe.whitelist() +def get_items(price_list, item=None): + condition = "" + order_by = "" + args = {"price_list": price_list} + + if item: + # search serial no + item_code = frappe.db.sql("""select name as serial_no, item_code + from `tabSerial No` where name=%s""", (item), as_dict=1) + if item_code: + item_code[0]["name"] = item_code[0]["item_code"] + return item_code + + # search barcode + item_code = frappe.db.sql("""select name, item_code from `tabItem` + where barcode=%s""", + (item), as_dict=1) + if item_code: + item_code[0]["barcode"] = item + return item_code + + # locate function is used to sort by closest match from the beginning of the value + return 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.has_variants = 0 and (i.item_code like %(item_code)s or i.item_name like %(item_code)s) + limit 24""", {'item_code': '%%%s%%'%(frappe.db.escape(item)), 'price_list': price_list} , as_dict=1) From 26df5b8c00f6a0226ce766d8f0a999c065e55d66 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 23 Aug 2017 13:12:12 +0530 Subject: [PATCH 16/36] Connect Numpad events with POSCart --- erpnext/public/css/pos.css | 20 +++ erpnext/public/less/pos.less | 26 ++++ .../page/point_of_sale/point_of_sale.js | 116 +++++++++++++++--- .../page/point_of_sale/point_of_sale.py | 5 +- 4 files changed, 145 insertions(+), 22 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index de1e097bb8..c6ec63796b 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -33,6 +33,19 @@ height: 200px; 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 input { height: 22px; font-size: 12px; @@ -105,3 +118,10 @@ input[type=number]::-webkit-outer-spin-button { text-align: center; line-height: 50px; } +.num-col.active { + background-color: #fffce7; +} +.num-col.brand-primary { + background-color: #5E64FF; + color: #ffffff; +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index de165144f1..6f49c66bc4 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -45,6 +45,23 @@ height: 200px; 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; + } + input { height: 22px; font-size: @text-medium; @@ -128,4 +145,13 @@ input[type=number]::-webkit-outer-spin-button { text-align: center; line-height: 50px; } + + &.active { + background-color: @light-yellow; + } + + &.brand-primary { + background-color: @brand-primary; + color: #ffffff; + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 00f886512a..8ed0878173 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -90,6 +90,9 @@ class PointOfSale { } this.payment.open_modal(); } + }, + on_select_change: () => { + this.cart.numpad.set_inactive(); } } }); @@ -109,6 +112,7 @@ class PointOfSale { frappe.throw(__('Please select a customer')); } this.add_item_to_cart(item_code); + this.cart && this.cart.unselect_all(); } } }); @@ -121,10 +125,10 @@ class PointOfSale { this.frm.doc.items.forEach((item) => { if (item.item_code === item_code) { if (barcode) { - value = barcode['serial_no'] ? + const value = barcode['serial_no'] ? item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no']; - frappe.model.set_value(item.doctype, item.name, - Object.keys(barcode)[0], final_qty); + frappe.model.set_value(item.doctype, item.name, + Object.keys(barcode)[0], value); } else { const final_qty = item.qty + qty; frappe.model.set_value(item.doctype, item.name, 'qty', final_qty) @@ -270,23 +274,45 @@ class POSCart { 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'], wrapper: this.wrapper.find('.number-pad-container'), onclick: (btn_value) => { // on click - if (['Qty', 'Disc', 'Price'].includes(btn_value)) { + if (['Qty', 'Disc', 'Rate'].includes(btn_value)) { + if (!this.selected_item) { + frappe.show_alert({ + indicator: 'red', + message: __('Please select an item in the cart first') + }); + return; + } + this.numpad.set_active(btn_value); this.set_input_active(btn_value); } - this.events.on_numpad.apply(null, [btn_value]); + this.events.on_numpad(btn_value); } }); } set_input_active(btn_value) { - if (!this.selected_item) return; + this.selected_item.removeClass('qty disc rate'); if (btn_value === 'Qty') { - this.selected_item.find('.quantity input').css('border', '1px solid blue'); + this.selected_item.addClass('qty'); + } else if (btn_value == 'Disc') { + this.selected_item.addClass('disc'); + } else if (btn_value == 'Rate') { + this.selected_item.addClass('rate'); } } @@ -315,7 +341,7 @@ class POSCart { $item.remove(); } } - + get_item_html(item) { const rate = format_currency(item.rate, this.frm.doc.currency); return ` @@ -365,12 +391,15 @@ class POSCart { scroll_to_item(item_code) { const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); - // const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); - // this.$cart_items.animate({ scrollTop }); + 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); @@ -384,10 +413,30 @@ class POSCart { events.decrease_qty(item_code); } }); - const me = this; + + // current item this.$cart_items.on('click', '.list-item', function() { me.selected_item = $(this); + me.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); + me.selected_item.addClass('current-item'); + me.events.on_select_change(); }); + + // 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; + // }); + } + + unselect_all() { + this.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); + this.selected_item = null; + this.events.on_select_change(); } } @@ -396,7 +445,7 @@ class POSItems { this.wrapper = wrapper; this.pos_profile = pos_profile; this.items = {}; - this.currency = this.pos_profile.currency || + this.currency = this.pos_profile.currency || frappe.defaults.get_default('currency'); this.make_dom(); @@ -595,10 +644,12 @@ class POSItems { } class NumberPad { - constructor({wrapper, onclick, button_array}) { + constructor({wrapper, onclick, button_array, add_class, disable_highlight}) { this.wrapper = wrapper; this.onclick = onclick; this.button_array = button_array; + this.add_class = add_class; + this.disable_highlight = disable_highlight; this.make_dom(); this.bind_events(); } @@ -606,10 +657,10 @@ class NumberPad { make_dom() { if (!this.button_array) { this.button_array = [ - [1, 2, 3, 'Qty'], - [4, 5, 6, 'Disc'], - [7, 8, 9, 'Price'], - ['Del', 0, '.', 'Pay'] + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ['', 0, ''] ]; } @@ -626,6 +677,15 @@ class NumberPad { function get_col(col) { return `
${col}
`; } + + 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() { @@ -633,16 +693,32 @@ class NumberPad { const me = this; this.wrapper.on('click', '.num-col', function() { const $btn = $(this); - me.highlight_button($btn); - me.onclick.apply(null, [$btn.attr('data-value')]); + const btn_value = $btn.attr('data-value'); + if (!me.disable_highlight.includes(btn_value)) { + me.highlight_button($btn); + } + me.onclick(btn_value); }); } + get_btn(btn_value) { + return this.wrapper.find(`.num-col[data-value="${btn_value}"]`); + } + highlight_button($btn) { - // const $btn = this.wrapper.find(`[data-value="${value}"]`); $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 { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 045ef07a73..9f3c289540 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -15,7 +15,6 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges def get_items(price_list, item=None): condition = "" order_by = "" - args = {"price_list": price_list} if item: # search serial no @@ -42,5 +41,7 @@ def get_items(price_list, item=None): ON (item_det.item_code=i.name or item_det.item_code=i.variant_of) where - i.has_variants = 0 and (i.item_code like %(item_code)s or i.item_name like %(item_code)s) + i.disabled = 0 and i.has_variants = 0 + and (i.item_code like %(item_code)s + or i.item_name like %(item_code)s) limit 24""", {'item_code': '%%%s%%'%(frappe.db.escape(item)), 'price_list': price_list} , as_dict=1) From 6d2f6c2e4d55c33b3f88a9b81b5824b3d2ae23ac Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 23 Aug 2017 16:25:16 +0530 Subject: [PATCH 17/36] Search improved, code refactor --- erpnext/public/css/pos.css | 7 + erpnext/public/less/pos.less | 9 + .../page/point_of_sale/point_of_sale.js | 204 ++++++++++++------ .../page/point_of_sale/point_of_sale.py | 42 ++-- 4 files changed, 179 insertions(+), 83 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index c6ec63796b..b19372b295 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -29,6 +29,9 @@ .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: 200px; overflow: auto; @@ -46,7 +49,11 @@ .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; } diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 6f49c66bc4..bcbd142dd1 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -39,6 +39,10 @@ .list-item__content:not(:first-child) { justify-content: flex-end; } + + .list-item--head .list-item__content:nth-child(2) { + flex: 1.5; + } } .cart-items { @@ -62,7 +66,12 @@ font-weight: bold; } + .list-item .quantity { + flex: 1.5; + } + input { + text-align: right; height: 22px; font-size: @text-medium; } diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 8ed0878173..90ccbfb1d2 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -76,12 +76,9 @@ class PointOfSale { frm: this.frm, wrapper: this.wrapper.find('.cart-container'), events: { - customer_change: (customer) => this.frm.set_value('customer', customer), - increase_qty: (item_code) => { - this.add_item_to_cart(item_code); - }, - decrease_qty: (item_code) => { - this.add_item_to_cart(item_code, -1); + 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') { @@ -111,37 +108,36 @@ class PointOfSale { if(!this.frm.doc.customer) { frappe.throw(__('Please select a customer')); } - this.add_item_to_cart(item_code); + this.update_item_in_cart(item_code, 'qty', '+1'); this.cart && this.cart.unselect_all(); } } }); } - add_item_to_cart(item_code, qty = 1, barcode) { + update_item_in_cart(item_code, field='qty', value=1) { if(this.cart.exists(item_code)) { - // increase qty by 1 - this.frm.doc.items.forEach((item) => { - if (item.item_code === item_code) { - if (barcode) { - const value = barcode['serial_no'] ? - item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no']; - frappe.model.set_value(item.doctype, item.name, - Object.keys(barcode)[0], value); - } else { - const final_qty = item.qty + qty; - frappe.model.set_value(item.doctype, item.name, 'qty', final_qty) - .then(() => { - if (final_qty === 0) { - frappe.model.clear_doc(item.doctype, item.name); - } - // update cart - this.cart.add_item(item); - }); - } - } - }); + const item = this.frm.doc.items.find(i => i.item_code === item_code); + + if (typeof value === 'string') { + // value can be of type '+1' or '-1' + value = item[field] + flt(value); + } + + this.update_item_in_frm(item, field, value) + .then(() => { + // update cart + this.cart.add_item(item); + }); + + // if (barcode) { + // const value = barcode['serial_no'] ? + // item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no']; + // frappe.model.set_value(item.doctype, item.name, + // Object.keys(barcode)[0], value); + // } else { + // } return; } @@ -155,6 +151,15 @@ class PointOfSale { }); } + update_item_in_frm(item, field, value) { + return frappe.model.set_value(item.doctype, item.name, field, value) + .then(() => { + if (field === 'qty' && value === 0) { + frappe.model.clear_doc(item.doctype, item.name); + } + }); + } + make_payment_modal() { this.payment = new Payment(this.frm); } @@ -249,6 +254,16 @@ class POSCart {
+
+
+
${__('Net Total')}
+
0.00
+
+
+
${__('Taxes')}
+
0.00
+
+
@@ -264,7 +279,7 @@ class POSCart { options: 'Customer', reqd: 1, onchange: () => { - this.events.customer_change.apply(null, [this.customer_field.get_value()]); + this.events.on_customer_change(this.customer_field.get_value()); } }, parent: this.wrapper.find('.customer-field'), @@ -283,20 +298,35 @@ class POSCart { add_class: { 'Pay': 'brand-primary' }, - disable_highlight: ['Qty', 'Disc', 'Rate'], + 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) { + frappe.show_alert({ + indicator: 'red', + message: __('Please select an item in the cart') + }); + return; + } if (['Qty', 'Disc', 'Rate'].includes(btn_value)) { - if (!this.selected_item) { + 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 an item in the cart first') + message: __('Please select a field to edit from numpad') }); return; } - this.numpad.set_active(btn_value); - this.set_input_active(btn_value); + + 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); @@ -307,12 +337,16 @@ class POSCart { 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'; } } @@ -391,6 +425,7 @@ class POSCart { 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 }); } @@ -408,18 +443,33 @@ class POSCart { const action = $btn.attr('data-action'); if(action === 'increment') { - events.increase_qty(item_code); + events.on_field_change(item_code, 'qty', '+1'); } else if(action === 'decrement') { - events.decrease_qty(item_code); + 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.selected_item = $(this); - me.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); - me.selected_item.addClass('current-item'); - me.events.on_select_change(); + console.log('cart item click'); + me.set_selected_item($(this)); }); // disable current item @@ -433,6 +483,13 @@ class POSCart { // }); } + 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; @@ -456,8 +513,9 @@ class POSItems { // bootstrap with 20 items this.get_items() - .then(items => { - this.items = items + .then((items, serial_no) => { + console.log(serial_no); + this.items = items; }) .then(() => this.render_items()); } @@ -538,6 +596,7 @@ class POSItems { const row_container = '
'; 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 @@ -547,6 +606,10 @@ class POSItems { 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); @@ -555,14 +618,10 @@ class POSItems { filter_items(search_term) { search_term = search_term.toLowerCase(); - const filtered_items = - Object.values(this.items) - .filter(item => { - return item.item_code.toLowerCase().includes(search_term) || - item.item_name.toLowerCase().includes(search_term) - }); - - this.render_items(filtered_items); + this.get_items({search_value: search_term}) + .then((items) => { + this.render_items(items); + }); } bind_events(events) { @@ -582,7 +641,7 @@ class POSItems { } get_item_html(item) { - const price_list_rate = format_currency(item.price_list_rate, this.currency) + const price_list_rate = format_currency(item.price_list_rate, this.currency); const { item_code, item_name, item_image, item_stock=0} = item; const item_title = item_name || item_code; @@ -619,39 +678,41 @@ class POSItems { return template; } - get_items(start = 10, page_length = 20) { - var me = this; + get_items({start = 0, page_length = 40, search_value=''}={}) { 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': me.search_field.$input.value || "" + search_value, } }).then(r => { - const items = r.message; + const { items, serial_no } = r.message; - // convert to key, value - let items_dict = {}; - items.map(item => { - items_dict[item.item_code] = item; - }); - - res(items_dict); + res(items, serial_no); }); }); } } class NumberPad { - constructor({wrapper, onclick, button_array, add_class, disable_highlight}) { + 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() { @@ -697,10 +758,23 @@ class NumberPad { if (!me.disable_highlight.includes(btn_value)) { me.highlight_button($btn); } + if (me.reset_btns.includes(btn_value)) { + me.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); }); } + get_value() { + return flt(this.value); + } + get_btn(btn_value) { return this.wrapper.find(`.num-col[data-value="${btn_value}"]`); } @@ -890,4 +964,4 @@ class Payment { this.total_amount_section = $(this.$body).find("[data-fieldname = 'total_amount']"); this.total_amount_section.html(template); } -} \ 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 index 9f3c289540..e6cfc719e0 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -12,28 +12,19 @@ from erpnext.accounts.party import get_party_account_currency from erpnext.controllers.accounts_controller import get_taxes_and_charges @frappe.whitelist() -def get_items(price_list, item=None): +def get_items(start, page_length, price_list, search_value=""): condition = "" - order_by = "" + serial_no = "" + item_code = search_value - if item: + if search_value: # search serial no - item_code = frappe.db.sql("""select name as serial_no, item_code - from `tabSerial No` where name=%s""", (item), as_dict=1) - if item_code: - item_code[0]["name"] = item_code[0]["item_code"] - return item_code - - # search barcode - item_code = frappe.db.sql("""select name, item_code from `tabItem` - where barcode=%s""", - (item), as_dict=1) - if item_code: - item_code[0]["barcode"] = item - return item_code + 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 # locate function is used to sort by closest match from the beginning of the value - return frappe.db.sql("""select i.name as item_code, i.item_name, i.image as item_image, + 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 @@ -44,4 +35,19 @@ def get_items(price_list, item=None): i.disabled = 0 and i.has_variants = 0 and (i.item_code like %(item_code)s or i.item_name like %(item_code)s) - limit 24""", {'item_code': '%%%s%%'%(frappe.db.escape(item)), 'price_list': price_list} , as_dict=1) + limit {start}, {page_length}""".format(start=start, page_length=page_length), + { + '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 + }) + + return res \ No newline at end of file From e5f6b4d64053589a94c70e28426a32054c87e5aa Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Aug 2017 15:27:55 +0530 Subject: [PATCH 18/36] Added print, make new invoice from POS functionality --- .../page/point_of_sale/point_of_sale.js | 214 ++++++++++++++---- .../page/point_of_sale/point_of_sale.py | 15 +- 2 files changed, 178 insertions(+), 51 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 90ccbfb1d2..dc2b2659c5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -33,14 +33,12 @@ class PointOfSale { this.prepare_menu(); this.set_online_status(); }, - () => this.make_sales_invoice_frm(), () => this.setup_pos_profile(), () => { - this.make_cart(); this.make_items(); this.bind_events(); - this.disable_text_box_and_button(); - } + }, + () => this.make_new_invoice(), ]); } @@ -96,7 +94,19 @@ class PointOfSale { } disable_text_box_and_button() { - $(this.wrapper).find('input, button').prop("disabled", !(this.frm.doc.docstatus===0)); + let disabled = this.frm.doc.docstatus == 1 ? true: false; + let pointer_events = this.frm.doc.docstatus == 1 ? "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(); + if(this.frm.doc.docstatus === 1) { + this.set_primary_action() + } } make_items() { @@ -110,6 +120,9 @@ class PointOfSale { } this.update_item_in_cart(item_code, 'qty', '+1'); this.cart && this.cart.unselect_all(); + }, + update_cart: (item, field, value) => { + this.update_item_in_cart(item, field, value) } } }); @@ -125,11 +138,18 @@ class PointOfSale { value = item[field] + flt(value); } + if (field === 'serial_no') { + value = item.serial_no + '\n' + value; + } + this.update_item_in_frm(item, field, value) .then(() => { // update cart this.cart.add_item(item); - }); + }) + .then(() => { + this.show_taxes_and_totals(); + }) // if (barcode) { // const value = barcode['serial_no'] ? @@ -148,6 +168,7 @@ class PointOfSale { .then(() => { // update cart this.cart.add_item(item); + this.show_taxes_and_totals(); }); } @@ -161,7 +182,49 @@ class PointOfSale { } make_payment_modal() { - this.payment = new Payment(this.frm); + this.payment = new Payment({ + frm: this.frm, + events: { + submit_form: () => { + this.submit_sales_invoice() + } + } + }); + } + + submit_sales_invoice() { + var me = this; + this.frm.savesubmit(); + // frappe.confirm(__("Permanently Submit {0}?", [this.frm.doc.name]), function() { +// return frappe.call({ +// method: 'erpnext.selling.page.point_of_sale.point_of_sale.submit_invoice', +// freeze: true, +// args: { +// doc: me.frm.doc +// } +// }).then(r => { +// if(r.message) { +// me.frm.doc = r.message; +// me.frm.meta.default_print_format = 'POS Invoice'; +// frappe.show_alert({ +// indicator: 'green', +// message: __(`Sales invoice ${r.message.name} created succesfully`) +// }); +// +// me.frm.msgbox = frappe.msgprint( +// ` +// ${__('Print')} +// +// ${__('New')}` +// ); +// $(me.frm.msgbox.wrapper).find('.new_doc').click(function() { +// me.frm.msgbox.hide() +// me.make_new_invoice() +// }) +// me.disable_text_box_and_button(); +// } +// }); +// }) } bind_events() { @@ -179,15 +242,26 @@ class PointOfSale { }); } + make_new_invoice() { + return frappe.run_serially([ + () => this.make_sales_invoice_frm(), + () => { + this.make_cart(); + this.disable_text_box_and_button(); + } + ]); + } + make_sales_invoice_frm() { - const dt = 'Sales Invoice'; + this.dt = 'Sales Invoice'; return new Promise(resolve => { - frappe.model.with_doctype(dt, () => { + frappe.model.with_doctype(this.dt, () => { const page = $('
'); - const frm = new _f.Frm(dt, page, false); - const name = frappe.model.make_new_doc_and_get_name(dt, true); + const frm = new _f.Frm(this.dt, page, false); + const name = frappe.model.make_new_doc_and_get_name(this.dt, true); frm.refresh(name); frm.doc.items = []; + this.doc = frm.doc; this.frm = frm; this.frm.set_value('is_pos', 1); resolve(); @@ -196,6 +270,7 @@ class PointOfSale { } prepare_menu() { + var me = this; this.page.clear_menu(); // for mobile @@ -203,12 +278,8 @@ class PointOfSale { // }).addClass('visible-xs'); - this.page.add_menu_item(__("New Sales Invoice"), function () { - // - }); - - this.page.add_menu_item(__("Sync Master Data"), function () { - // + this.page.add_menu_item(__("Email"), function () { + me.frm.email_doc(); }); this.page.add_menu_item(__("Sync Offline Invoices"), function () { @@ -219,6 +290,39 @@ class PointOfSale { frappe.set_route('List', 'POS Profile'); }); } + + set_primary_action() { + var me = this; + this.page.set_secondary_action(__("Print"), function () { + me.frm.print_preview.printit(true) + }) + + this.page.set_primary_action(__("New"), function () { + me.make_new_invoice() + }) + } + + show_taxes_and_totals() { + let tax_template = ''; + let currency = this.frm.doc.currency; + const taxes_wrapper = $(this.wrapper).find('.taxes'); + + this.frm.refresh_field('taxes') + $(this.wrapper).find('.net_total').html(format_currency(this.frm.doc.net_total, this.currency)) + console.log(this.frm.doc.taxes[0].tax_amount) + $.each(this.frm.doc.taxes, function(index, data) { + console.log(data.tax_amount) + tax_template += ` +
+
${data.description}
+
${fmt_money(data.tax_amount, currency)}
+
` + }) + + taxes_wrapper.empty() + console.log(tax_template) + taxes_wrapper.html(tax_template) + } } class POSCart { @@ -237,36 +341,39 @@ class POSCart { } make_dom() { + $(this.wrapper).find('.pos-cart').empty() this.wrapper.append(` -
-
-
-
-
-
${__('Item Name')}
-
${__('Quantity')}
-
${__('Discount')}
-
${__('Rate')}
+
+
+
+
+
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
+
+
+
+ No Items added to cart +
+
-
-
- No Items added to cart +
+
+
${__('Net Total')}
+
0.00
+
+
+
${__('Taxes')}
+
0.00
-
-
-
${__('Net Total')}
-
0.00
-
-
-
${__('Taxes')}
-
0.00
-
+
-
-
`); this.$cart_items = this.wrapper.find('.cart-items'); } @@ -304,7 +411,7 @@ class POSCart { wrapper: this.wrapper.find('.number-pad-container'), onclick: (btn_value) => { // on click - if (!this.selected_item) { + if (!this.selected_item && btn_value !== 'Pay') { frappe.show_alert({ indicator: 'red', message: __('Please select an item in the cart') @@ -502,6 +609,7 @@ class POSItems { this.wrapper = wrapper; this.pos_profile = pos_profile; this.items = {}; + this.events = events; this.currency = this.pos_profile.currency || frappe.defaults.get_default('currency'); @@ -509,12 +617,11 @@ class POSItems { this.make_fields(); this.init_clusterize(); - this.bind_events(events); + this.bind_events(); // bootstrap with 20 items this.get_items() .then((items, serial_no) => { - console.log(serial_no); this.items = items; }) .then(() => this.render_items()); @@ -621,14 +728,19 @@ class POSItems { this.get_items({search_value: search_term}) .then((items) => { this.render_items(items); + if(this.serial_no) { + this.events.update_cart(items[0].item_code, + 'serial_no', this.serial_no) + } }); } - bind_events(events) { + bind_events() { + var me = this; this.wrapper.on('click', '.pos-item-wrapper', function(e) { const $item = $(this); const item_code = $item.attr('data-item-code'); - events.item_click.apply(null, [item_code]); + me.events.item_click.apply(null, [item_code]); }); } @@ -691,7 +803,8 @@ class POSItems { }).then(r => { const { items, serial_no } = r.message; - res(items, serial_no); + this.serial_no = serial_no || ""; + res(items); }); }); } @@ -796,8 +909,9 @@ class NumberPad { } class Payment { - constructor(frm) { + constructor({frm, events}) { this.frm = frm; + this.events = events; this.make(); this.set_primary_action(); // this.show_outstanding_amount() @@ -837,9 +951,9 @@ class Payment { var me = this; this.dialog.set_primary_action(__("Submit"), function() { - this.frm.doc.savesubmit() - this.dialog.hide() - }); + me.dialog.hide() + me.events.submit_form() + }) } get_fields() { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index e6cfc719e0..73546a05c9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -50,4 +50,17 @@ def get_items(start, page_length, price_list, search_value=""): 'serial_no': serial_no }) - return res \ No newline at end of file + 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 From 5d9196960e5120d688f19e6fbf6b7ccc7c2cae26 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 24 Aug 2017 17:09:34 +0530 Subject: [PATCH 19/36] new form improvements --- .../page/point_of_sale/point_of_sale.js | 193 ++++++++++-------- 1 file changed, 107 insertions(+), 86 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index dc2b2659c5..06db851479 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -93,20 +93,22 @@ class PointOfSale { }); } - disable_text_box_and_button() { - let disabled = this.frm.doc.docstatus == 1 ? true: false; - let pointer_events = this.frm.doc.docstatus == 1 ? "none":"inherit"; + 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('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.wrapper.find('.cart-container').css('pointer-events', pointer_events); + this.wrapper.find('.item-container').css('pointer-events', pointer_events); this.page.clear_actions(); - if(this.frm.doc.docstatus === 1) { - this.set_primary_action() - } } make_items() { @@ -193,38 +195,27 @@ class PointOfSale { } submit_sales_invoice() { - var me = this; - this.frm.savesubmit(); - // frappe.confirm(__("Permanently Submit {0}?", [this.frm.doc.name]), function() { -// return frappe.call({ -// method: 'erpnext.selling.page.point_of_sale.point_of_sale.submit_invoice', -// freeze: true, -// args: { -// doc: me.frm.doc -// } -// }).then(r => { -// if(r.message) { -// me.frm.doc = r.message; -// me.frm.meta.default_print_format = 'POS Invoice'; -// frappe.show_alert({ -// indicator: 'green', -// message: __(`Sales invoice ${r.message.name} created succesfully`) -// }); -// -// me.frm.msgbox = frappe.msgprint( -// ` -// ${__('Print')} -// -// ${__('New')}` -// ); -// $(me.frm.msgbox.wrapper).find('.new_doc').click(function() { -// me.frm.msgbox.hide() -// me.make_new_invoice() -// }) -// me.disable_text_box_and_button(); -// } -// }); -// }) + + 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() { @@ -246,27 +237,42 @@ class PointOfSale { return frappe.run_serially([ () => this.make_sales_invoice_frm(), () => { - this.make_cart(); - this.disable_text_box_and_button(); + if (this.cart) { + this.cart.frm = this.frm; + this.cart.reset(); + } else { + this.make_cart(); + } + this.toggle_editing(true); } ]); } make_sales_invoice_frm() { - this.dt = 'Sales Invoice'; + const doctype = 'Sales Invoice'; return new Promise(resolve => { - frappe.model.with_doctype(this.dt, () => { - const page = $('
'); - const frm = new _f.Frm(this.dt, page, false); - const name = frappe.model.make_new_doc_and_get_name(this.dt, true); - frm.refresh(name); - frm.doc.items = []; - this.doc = frm.doc; - this.frm = frm; - this.frm.set_value('is_pos', 1); + 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 = $('
'); + 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() { @@ -291,15 +297,16 @@ class PointOfSale { }); } - set_primary_action() { - var me = this; - this.page.set_secondary_action(__("Print"), function () { - me.frm.print_preview.printit(true) - }) + set_form_action() { + if(this.frm.doc.docstatus !== 1) return; - this.page.set_primary_action(__("New"), function () { - me.make_new_invoice() - }) + this.page.set_secondary_action(__("Print"), () => { + this.frm.print_preview.printit(true); + }); + + this.page.set_primary_action(__("New"), () => { + this.make_new_invoice(); + }); } show_taxes_and_totals() { @@ -307,21 +314,18 @@ class PointOfSale { let currency = this.frm.doc.currency; const taxes_wrapper = $(this.wrapper).find('.taxes'); - this.frm.refresh_field('taxes') - $(this.wrapper).find('.net_total').html(format_currency(this.frm.doc.net_total, this.currency)) - console.log(this.frm.doc.taxes[0].tax_amount) + this.frm.refresh_field('taxes'); + $(this.wrapper).find('.net-total').html(format_currency(this.frm.doc.net_total, this.currency)) $.each(this.frm.doc.taxes, function(index, data) { - console.log(data.tax_amount) tax_template += `
${data.description}
${fmt_money(data.tax_amount, currency)}
-
` - }) +
`; + }); taxes_wrapper.empty() - console.log(tax_template) - taxes_wrapper.html(tax_template) + taxes_wrapper.html(tax_template); } } @@ -341,7 +345,6 @@ class POSCart { } make_dom() { - $(this.wrapper).find('.pos-cart').empty() this.wrapper.append(`
@@ -361,14 +364,7 @@ class POSCart {
-
-
${__('Net Total')}
-
0.00
-
-
-
${__('Taxes')}
-
0.00
-
+ ${this.get_taxes_and_totals()}
@@ -376,6 +372,28 @@ class POSCart {
`); 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'); + } + + 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_taxes_and_totals() { + return ` +
+
${__('Net Total')}
+
0.00
+
+
+
${__('Taxes')}
+
0.00
+
`; } make_customer_field() { @@ -458,7 +476,7 @@ class POSCart { } add_item(item) { - this.wrapper.find('.cart-items .empty-state').hide(); + this.$empty_state.hide(); if (this.exists(item.item_code)) { // update quantity @@ -575,7 +593,6 @@ class POSCart { // current item this.$cart_items.on('click', '.list-item', function() { - console.log('cart item click'); me.set_selected_item($(this)); }); @@ -872,7 +889,7 @@ class NumberPad { me.highlight_button($btn); } if (me.reset_btns.includes(btn_value)) { - me.value = ''; + me.reset_value(); } else { if (btn_value === me.del_btn) { me.value = me.value.substr(0, me.value.length - 1); @@ -884,6 +901,10 @@ class NumberPad { }); } + reset_value() { + this.value = ''; + } + get_value() { return flt(this.value); } @@ -951,9 +972,9 @@ class Payment { var me = this; this.dialog.set_primary_action(__("Submit"), function() { - me.dialog.hide() - me.events.submit_form() - }) + me.dialog.hide(); + me.events.submit_form(); + }); } get_fields() { From 5e2d2059fee18f0a3bf20b15d633be661b1cafc0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 27 Aug 2017 08:54:40 +0530 Subject: [PATCH 20/36] Added email, edit on form view from POS, pay using numpad functionality --- .../page/point_of_sale/point_of_sale.js | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 06db851479..dbe47956c5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -284,12 +284,9 @@ class PointOfSale { // }).addClass('visible-xs'); - this.page.add_menu_item(__("Email"), function () { - me.frm.email_doc(); - }); - - this.page.add_menu_item(__("Sync Offline Invoices"), function () { - // + this.page.add_menu_item(__("Form View"), function () { + var doc = 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 () { @@ -307,6 +304,10 @@ class PointOfSale { this.page.set_primary_action(__("New"), () => { this.make_new_invoice(); }); + + this.page.add_menu_item(__("Email"), function () { + me.frm.email_doc(); + }); } show_taxes_and_totals() { @@ -934,20 +935,22 @@ class Payment { this.frm = frm; this.events = events; this.make(); + this.bind_events(); this.set_primary_action(); - // this.show_outstanding_amount() } open_modal() { - this.show_total_amount(); 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: __('Payment'), + title: title, fields: this.get_fields(), width: 800 }); @@ -963,11 +966,21 @@ class Payment { ['Del', 0, '.'], ], onclick: (btn_value) => { - // on click + if(this.fieldname) { + this.dialog.set_value(this.fieldname, flt(this.numpad.value)) + } } }); } + bind_events() { + var me = this; + $(this.dialog.body).find('.input-with-feedback').focusin(function() { + me.numpad.value = ''; + me.fieldname = $(this).prop('dataset').fieldname; + }) + } + set_primary_action() { var me = this; @@ -979,18 +992,8 @@ class Payment { get_fields() { const me = this; - let fields = [ - { - fieldtype: 'HTML', - fieldname: 'total_amount', - }, - { - fieldtype: 'Section Break', - label: __('Mode of Payments') - }, - ]; - fields = fields.concat(this.frm.doc.payments.map(p => { + let fields = this.frm.doc.payments.map(p => { return { fieldtype: 'Currency', label: __(p.mode_of_payment), @@ -998,12 +1001,11 @@ class Payment { fieldname: p.mode_of_payment, default: p.amount, onchange: (e) => { - const fieldname = $(e.target).attr('data-fieldname'); - const value = this.dialog.get_value(fieldname); - me.update_payment_value(fieldname, value); + const value = this.dialog.get_value(this.fieldname); + me.update_payment_value(this.fieldname, value); } }; - })); + }); fields = fields.concat([ { @@ -1045,6 +1047,28 @@ class Payment { }); } }, + { + 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; @@ -1082,21 +1106,15 @@ class Payment { 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_total_amount() { - const grand_total = format_currency(this.frm.doc.grand_total, this.frm.doc.currency); - const template = ` -

- ${ __("Total Amount") }: - ${__(grand_total)} -

- ` - this.total_amount_section = $(this.$body).find("[data-fieldname = 'total_amount']"); - this.total_amount_section.html(template); + 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) } } From d733b7d7d088fbdc235ba9c03e45ea11905ef843 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sun, 27 Aug 2017 13:56:33 +0530 Subject: [PATCH 21/36] Styling and code fixes --- erpnext/public/css/pos.css | 41 +++++- erpnext/public/less/pos.less | 57 +++++++- .../page/point_of_sale/point_of_sale.js | 127 +++++++++++++----- 3 files changed, 192 insertions(+), 33 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index b19372b295..10355f456c 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -1,6 +1,16 @@ +[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; @@ -33,7 +43,7 @@ flex: 1.5; } .cart-items { - height: 200px; + height: 150px; overflow: auto; } .cart-items .list-item.current-item { @@ -132,3 +142,32 @@ input[type=number]::-webkit-outer-spin-button { 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/less/pos.less b/erpnext/public/less/pos.less index bcbd142dd1..b699a55f85 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -1,10 +1,25 @@ @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; @@ -46,7 +61,7 @@ } .cart-items { - height: 200px; + height: 150px; overflow: auto; .list-item.current-item { @@ -163,4 +178,44 @@ input[type=number]::-webkit-outer-spin-button { 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/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index dbe47956c5..236e923255 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -150,8 +150,9 @@ class PointOfSale { this.cart.add_item(item); }) .then(() => { - this.show_taxes_and_totals(); - }) + this.cart.update_taxes_and_totals(); + this.cart.update_grand_total(); + }); // if (barcode) { // const value = barcode['serial_no'] ? @@ -170,7 +171,8 @@ class PointOfSale { .then(() => { // update cart this.cart.add_item(item); - this.show_taxes_and_totals(); + this.cart.update_taxes_and_totals(); + this.cart.update_grand_total(); }); } @@ -306,28 +308,9 @@ class PointOfSale { }); this.page.add_menu_item(__("Email"), function () { - me.frm.email_doc(); + this.frm.email_doc(); }); } - - show_taxes_and_totals() { - let tax_template = ''; - let currency = this.frm.doc.currency; - const taxes_wrapper = $(this.wrapper).find('.taxes'); - - this.frm.refresh_field('taxes'); - $(this.wrapper).find('.net-total').html(format_currency(this.frm.doc.net_total, this.currency)) - $.each(this.frm.doc.taxes, function(index, data) { - tax_template += ` -
-
${data.description}
-
${fmt_money(data.tax_amount, currency)}
-
`; - }); - - taxes_wrapper.empty() - taxes_wrapper.html(tax_template); - } } class POSCart { @@ -363,9 +346,15 @@ class POSCart { No Items added to cart
-
-
- ${this.get_taxes_and_totals()} +
+ ${this.get_taxes_and_totals()} +
+
+ ${this.get_discount_amount()} +
+
+ ${this.get_grand_total()} +
@@ -375,6 +364,13 @@ class POSCart { 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() { @@ -385,6 +381,35 @@ class POSCart { this.customer_field.set_value(""); } + get_grand_total() { + return ` +
+
${__('Grand Total')}
+
0.00
+
+ `; + } + + get_discount_amount() { + const get_currency_symbol = window.get_currency_symbol; + + return ` +
+
${__('Discount')}
+
+ + +
+
+ `; + } + get_taxes_and_totals() { return `
@@ -394,7 +419,47 @@ class POSCart {
${__('Taxes')}
0.00
-
`; +
+ `; + } + + 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 ` +
+ ${tax.description} + + ${format_currency(tax.tax_amount, currency)} + +
+ `; + }).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() { @@ -965,9 +1030,9 @@ class Payment { [7, 8, 9], ['Del', 0, '.'], ], - onclick: (btn_value) => { + onclick: () => { if(this.fieldname) { - this.dialog.set_value(this.fieldname, flt(this.numpad.value)) + this.dialog.set_value(this.fieldname, this.numpad.get_value()); } } }); @@ -976,9 +1041,9 @@ class Payment { bind_events() { var me = this; $(this.dialog.body).find('.input-with-feedback').focusin(function() { - me.numpad.value = ''; + me.numpad.reset_value(); me.fieldname = $(this).prop('dataset').fieldname; - }) + }); } set_primary_action() { From 3ae9e91bcd68605ec776dc941246fbd9d82ab5c0 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sun, 27 Aug 2017 14:04:23 +0530 Subject: [PATCH 22/36] Add memoization in search --- erpnext/selling/page/point_of_sale/point_of_sale.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 236e923255..a3ea04e201 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -808,12 +808,23 @@ class POSItems { filter_items(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; + } + this.get_items({search_value: search_term}) .then((items) => { + this.search_index[search_term] = items; + this.render_items(items); if(this.serial_no) { this.events.update_cart(items[0].item_code, - 'serial_no', this.serial_no) + 'serial_no', this.serial_no); } }); } From ba3f0e6b7060bf874130bc568951ac8617c7ee4a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Aug 2017 17:19:28 +0530 Subject: [PATCH 23/36] Added serial no, batch no, item group functionality --- .../doctype/pos_profile/pos_profile.json | 8 +- .../accounts/doctype/pos_settings/__init__.py | 0 .../doctype/pos_settings/pos_settings.js | 8 + .../doctype/pos_settings/pos_settings.json | 93 +++++++++++ .../doctype/pos_settings/pos_settings.py | 16 ++ .../doctype/pos_settings/test_pos_settings.js | 23 +++ .../doctype/sales_invoice/sales_invoice.py | 2 +- .../js/utils/serial_no_batch_selector.js | 13 +- .../page/point_of_sale/point_of_sale.js | 154 ++++++++++++------ .../page/point_of_sale/point_of_sale.py | 6 +- erpnext/stock/get_item_details.py | 12 +- 11 files changed, 269 insertions(+), 66 deletions(-) create mode 100644 erpnext/accounts/doctype/pos_settings/__init__.py create mode 100644 erpnext/accounts/doctype/pos_settings/pos_settings.js create mode 100644 erpnext/accounts/doctype/pos_settings/pos_settings.json create mode 100644 erpnext/accounts/doctype/pos_settings/pos_settings.py create mode 100644 erpnext/accounts/doctype/pos_settings/test_pos_settings.js diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 6991da2888..c4e6dabc17 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -822,7 +822,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 +836,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 +851,7 @@ "collapsible": 0, "columns": 0, "default": "Grand Total", - "depends_on": "apply_discount", + "depends_on": "", "fieldname": "apply_discount_on", "fieldtype": "Select", "hidden": 0, @@ -1291,7 +1291,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-28 03:40:03.253088", + "modified": "2017-08-27 16:39:00.713225", "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 0000000000..e69de29bb2 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 0000000000..fab766bb4b --- /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(frm) { + + } +}); 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 0000000000..ab3976e1ba --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -0,0 +1,93 @@ +{ + "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, + "fieldname": "type_of_pos", + "fieldtype": "Select", + "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": "Type of POS", + "length": 0, + "no_copy": 0, + "options": "Online\nOffline", + "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-28 16:46:41.732676", + "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 0000000000..4a71775a70 --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class POSSettings(Document): + def validate(self): + link = 'point-of-sale' if self.type_of_pos == 'Online' else 'pos' + desktop_icon = frappe.db.get_value('Desktop Icon', {'module_name': 'POS'}, 'name') + if desktop_icon: + doc = frappe.get_doc('Desktop Icon', desktop_icon) + doc.link = link + doc.save() \ 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 0000000000..639c94ed10 --- /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 9dfacbdb5e..0b6926f287 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -303,7 +303,7 @@ class SalesInvoice(SellingController): 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/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 08630e5998..ed5a0f6b8d 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 @@ erpnext.SerialNoBatchSelector = Class.extend({ } }); + if(this.show_dialog) { + let d = this.item; + this.dialog.set_value('serial_no', d.serial_no); + } + this.dialog.show(); }, @@ -140,6 +146,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ 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/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index a3ea04e201..4096ed436e 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -120,6 +120,7 @@ class PointOfSale { if(!this.frm.doc.customer) { frappe.throw(__('Please select a customer')); } + this.update_item_in_cart(item_code, 'qty', '+1'); this.cart && this.cart.unselect_all(); }, @@ -131,53 +132,76 @@ class PointOfSale { } 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); - if (typeof value === 'string') { + 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 === 'serial_no') { + value = item.serial_no + '\n'+ value; } - this.update_item_in_frm(item, field, value) - .then(() => { - // update cart - this.cart.add_item(item); - }) - .then(() => { - this.cart.update_taxes_and_totals(); - this.cart.update_grand_total(); - }); - - // if (barcode) { - // const value = barcode['serial_no'] ? - // item.serial_no + '\n' + barcode['serial_no'] : barcode['batch_no']; - // frappe.model.set_value(item.doctype, item.name, - // Object.keys(barcode)[0], value); - // } else { - // } + 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', { item_code: item_code }); + const item = this.frm.add_child('items', args); this.frm.script_manager .trigger('item_code', item.doctype, item.name) .then(() => { // update cart - this.cart.add_item(item); - this.cart.update_taxes_and_totals(); - this.cart.update_grand_total(); + this.update_cart_data(item) }); } + select_batch_and_serial_no(item) { + let dialog = new erpnext.SerialNoBatchSelector({ + frm: this.frm, + item: item, + warehouse_details: { + type: "Warehouse", + name: item.warehouse + }, + callback: (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) { - return frappe.model.set_value(item.doctype, item.name, 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); @@ -307,7 +331,7 @@ class PointOfSale { this.make_new_invoice(); }); - this.page.add_menu_item(__("Email"), function () { + this.page.add_menu_item(__("Email"), () => { this.frm.email_doc(); }); } @@ -398,11 +422,11 @@ class POSCart {
${__('Discount')}
@@ -561,7 +585,7 @@ class POSCart { if(item.qty > 0) { $item.find('.quantity input').val(item.qty); $item.find('.discount').text(item.discount_percentage); - $item.find('.rate').text(item.rate); + $item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency)); } else { $item.remove(); } @@ -671,6 +695,28 @@ class POSCart { // 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) { @@ -749,21 +795,19 @@ class POSItems { this.search_field.$input.on('input', (e) => { const search_term = e.target.value; - this.filter_items(search_term); + this.filter_items({ search_term }); }); - - // Item group field this.item_group_field = frappe.ui.form.make_control({ df: { - fieldtype: 'Select', + fieldtype: 'Link', label: 'Item Group', - options: [ - 'All Item Groups', - 'Raw Materials', - 'Finished Goods' - ], - default: 'All Item Groups' + options: 'Item Group', + default: 'All Item Groups', + onchange: () => { + console.log("in the item_group") + this.filter_items({ item_group: this.item_group_field.get_value() }) + }, }, parent: this.wrapper.find('.item-group-field'), render_input: true @@ -805,21 +849,24 @@ class POSItems { this.clusterize.update(row_items); } - filter_items(search_term) { - search_term = search_term.toLowerCase(); + 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; + // 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; + } } - this.get_items({search_value: search_term}) + this.get_items({search_value: search_term, item_group }) .then((items) => { - this.search_index[search_term] = items; + if (search_term) { + this.search_index[search_term] = items; + } this.render_items(items); if(this.serial_no) { @@ -884,7 +931,7 @@ class POSItems { return template; } - get_items({start = 0, page_length = 40, search_value=''}={}) { + 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", @@ -892,7 +939,8 @@ class POSItems { start, page_length, 'price_list': this.pos_profile.selling_price_list, - search_value, + item_group, + search_value } }).then(r => { const { items, serial_no } = r.message; diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 73546a05c9..d34fc544e9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -12,7 +12,7 @@ from erpnext.accounts.party import get_party_account_currency from erpnext.controllers.accounts_controller import get_taxes_and_charges @frappe.whitelist() -def get_items(start, page_length, price_list, search_value=""): +def get_items(start, page_length, price_list, item_group, search_value=""): condition = "" serial_no = "" item_code = search_value @@ -23,6 +23,7 @@ def get_items(start, page_length, price_list, search_value=""): if serial_no_data: serial_no, item_code = serial_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 @@ -33,9 +34,10 @@ def get_items(start, page_length, price_list, search_value=""): (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) - limit {start}, {page_length}""".format(start=start, page_length=page_length), + 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 diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 80ef70805a..8d084dcb8c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -79,7 +79,7 @@ def get_item_details(args): 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 @@ def get_gross_profit(out): 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 @@ def get_serial_no(args): 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 From 655f86d5e2d9a6b8c5d936f5986cc8c2a0c01b3d Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 29 Aug 2017 14:27:01 +0530 Subject: [PATCH 24/36] UI fixes --- erpnext/public/css/erpnext.css | 20 ++++++++-------- erpnext/public/css/pos.css | 3 ++- erpnext/public/less/erpnext.less | 22 +++++++++--------- erpnext/public/less/pos.less | 3 ++- .../page/point_of_sale/point_of_sale.js | 23 ++++++++++--------- 5 files changed, 37 insertions(+), 34 deletions(-) diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css index 0660b39208..13fdcf1501 100644 --- a/erpnext/public/css/erpnext.css +++ b/erpnext/public/css/erpnext.css @@ -308,16 +308,6 @@ body[data-route="pos"] .item-list .image-field .placeholder-text { 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"] .btn-more { 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 index 10355f456c..f66abc8081 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -80,7 +80,8 @@ .pos-item-wrapper { display: flex; flex-direction: column; - justify-content: space-between; + position: relative; + width: 25%; } .image-view-container { display: block; diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less index 6c616c9e32..de46c53df8 100644 --- a/erpnext/public/less/erpnext.less +++ b/erpnext/public/less/erpnext.less @@ -364,17 +364,6 @@ body[data-route="pos"] { .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 @@ 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; } \ No newline at end of file diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index b699a55f85..9653a82658 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -108,7 +108,8 @@ .pos-item-wrapper { display: flex; flex-direction: column; - justify-content: space-between; + position: relative; + width: 25%; } .image-view-container { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 4096ed436e..cc1e3f164d 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -39,6 +39,7 @@ class PointOfSale { this.bind_events(); }, () => this.make_new_invoice(), + () => this.page.set_title(__('Point of Sale')) ]); } @@ -584,7 +585,7 @@ class POSCart { const $item = this.$cart_items.find(`[data-item-code="${item.item_code}"]`); if(item.qty > 0) { $item.find('.quantity input').val(item.qty); - $item.find('.discount').text(item.discount_percentage); + $item.find('.discount').text(item.discount_percentage + '%'); $item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency)); } else { $item.remove(); @@ -700,23 +701,23 @@ class POSCart { 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') - }) - }) + 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) + '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() - }) - }) + this.update_taxes_and_totals(); + this.update_grand_total(); + }); + }); } set_selected_item($item) { From c70bbacd347a1e83082a49f3b1f7614e30823448 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 29 Aug 2017 15:27:17 +0530 Subject: [PATCH 25/36] Add online, offline option, fix search --- .../doctype/pos_settings/pos_settings.json | 11 +-- .../doctype/pos_settings/pos_settings.py | 8 +- erpnext/accounts/page/pos/pos.js | 12 ++- .../page/point_of_sale/point_of_sale.js | 78 ++++++++++++------- .../page/point_of_sale/point_of_sale.py | 11 +++ 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json index ab3976e1ba..cdd1865b75 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.json +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -18,8 +18,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "type_of_pos", - "fieldtype": "Select", + "default": "0", + "fieldname": "is_online", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -27,10 +28,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Type of POS", + "label": "Online", "length": 0, "no_copy": 0, - "options": "Online\nOffline", + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -54,7 +55,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-08-28 16:46:41.732676", + "modified": "2017-08-29 14:34:36.166049", "modified_by": "Administrator", "module": "Accounts", "name": "POS Settings", diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py index 4a71775a70..c978a4e9ad 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.py +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -7,10 +7,4 @@ import frappe from frappe.model.document import Document class POSSettings(Document): - def validate(self): - link = 'point-of-sale' if self.type_of_pos == 'Online' else 'pos' - desktop_icon = frappe.db.get_value('Desktop Icon', {'module_name': 'POS'}, 'name') - if desktop_icon: - doc = frappe.get_doc('Desktop Icon', desktop_icon) - doc.link = link - doc.save() \ No newline at end of file + pass \ No newline at end of file diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 599372411a..3d5d5f4d90 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -8,8 +8,16 @@ frappe.pages['pos'].on_page_load = function (wrapper) { single_column: true }); - wrapper.pos = new erpnext.pos.PointOfSale(wrapper); - cur_pos = wrapper.pos; + 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/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index cc1e3f164d..b523a4952c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -7,8 +7,16 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { single_column: true }); - wrapper.pos = new PointOfSale(wrapper); - window.cur_pos = wrapper.pos; + 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 PointOfSale(wrapper); + window.cur_pos = wrapper.pos; + } else { + // offline + frappe.set_route('pos'); + } + }); }; class PointOfSale { @@ -117,16 +125,12 @@ class PointOfSale { wrapper: this.wrapper.find('.item-container'), pos_profile: this.pos_profile, events: { - item_click: (item_code) => { + update_cart: (item, field, value) => { if(!this.frm.doc.customer) { frappe.throw(__('Please select a customer')); } - - this.update_item_in_cart(item_code, 'qty', '+1'); + this.update_item_in_cart(item, field, value); this.cart && this.cart.unselect_all(); - }, - update_cart: (item, field, value) => { - this.update_item_in_cart(item, field, value) } } }); @@ -146,7 +150,7 @@ class PointOfSale { } if(field === 'qty' && (item.serial_no || item.batch_no)) { - this.select_batch_and_serial_no(item) + this.select_batch_and_serial_no(item); } else { this.update_item_in_frm(item, field, value) .then(() => { @@ -257,6 +261,13 @@ class PointOfSale { } }).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') + }; + } }); } @@ -307,9 +318,9 @@ class PointOfSale { this.page.clear_menu(); // for mobile - this.page.add_menu_item(__("Pay"), function () { - // - }).addClass('visible-xs'); + // this.page.add_menu_item(__("Pay"), function () { + // + // }).addClass('visible-xs'); this.page.add_menu_item(__("Form View"), function () { var doc = frappe.model.sync(me.frm.doc); @@ -319,6 +330,10 @@ class PointOfSale { 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() { @@ -740,8 +755,7 @@ class POSItems { this.pos_profile = pos_profile; this.items = {}; this.events = events; - this.currency = this.pos_profile.currency || - frappe.defaults.get_default('currency'); + this.currency = this.pos_profile.currency; this.make_dom(); this.make_fields(); @@ -751,10 +765,11 @@ class POSItems { // bootstrap with 20 items this.get_items() - .then((items, serial_no) => { + .then(({ items }) => { + this.all_items = items; this.items = items; - }) - .then(() => this.render_items()); + this.render_items(items); + }); } make_dom() { @@ -795,8 +810,11 @@ class POSItems { }); this.search_field.$input.on('input', (e) => { - const search_term = e.target.value; - this.filter_items({ search_term }); + 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({ @@ -861,18 +879,26 @@ class POSItems { this.render_items(items); return; } + } else { + return this.render_items(this.all_items); } this.get_items({search_value: search_term, item_group }) - .then((items) => { + .then(({ items, serial_no, batch_no }) => { if (search_term) { this.search_index[search_term] = items; } this.render_items(items); - if(this.serial_no) { + if(serial_no) { this.events.update_cart(items[0].item_code, - 'serial_no', this.serial_no); + '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(''); } }); } @@ -882,7 +908,7 @@ class POSItems { this.wrapper.on('click', '.pos-item-wrapper', function(e) { const $item = $(this); const item_code = $item.attr('data-item-code'); - me.events.item_click.apply(null, [item_code]); + me.events.update_cart(item_code, 'qty', '+1'); }); } @@ -944,10 +970,10 @@ class POSItems { search_value } }).then(r => { - const { items, serial_no } = r.message; + // const { items, serial_no, batch_no } = r.message; - this.serial_no = serial_no || ""; - res(items); + // this.serial_no = serial_no || ""; + res(r.message); }); }); } diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index d34fc544e9..4e2b1b1343 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -15,6 +15,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges def get_items(start, page_length, price_list, item_group, search_value=""): condition = "" serial_no = "" + batch_no = "" item_code = search_value if search_value: @@ -23,6 +24,11 @@ def get_items(start, page_length, price_list, item_group, search_value=""): 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, @@ -52,6 +58,11 @@ def get_items(start, page_length, price_list, item_group, search_value=""): 'serial_no': serial_no }) + if batch_no: + res.update({ + 'batch_no': batch_no + }) + return res @frappe.whitelist() From 70aaff46be966c6cee2dfac366680872a35765e3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Aug 2017 19:17:02 +0530 Subject: [PATCH 26/36] minor fix in item group search --- erpnext/selling/page/point_of_sale/point_of_sale.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index b523a4952c..c4bd59076b 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -824,8 +824,10 @@ class POSItems { options: 'Item Group', default: 'All Item Groups', onchange: () => { - console.log("in the item_group") - this.filter_items({ item_group: this.item_group_field.get_value() }) + 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'), @@ -879,7 +881,7 @@ class POSItems { this.render_items(items); return; } - } else { + } else if (item_group == "All Item Groups") { return this.render_items(this.all_items); } From 1116f96aeea2b6b69f5c4c0717b7c959de6195d1 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 30 Aug 2017 12:53:08 +0530 Subject: [PATCH 27/36] Grand Total Value width --- erpnext/selling/page/point_of_sale/point_of_sale.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c4bd59076b..4ce1ca1a33 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -424,8 +424,8 @@ class POSCart { get_grand_total() { return `
-
${__('Grand Total')}
-
0.00
+
${__('Grand Total')}
+
0.00
`; } From 6163a397dfec881993a86e7eb00100145f6add32 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 30 Aug 2017 12:54:16 +0530 Subject: [PATCH 28/36] Disable eslint in lib --- erpnext/public/js/pos/clusterize.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js index 6d331ba761..075c9ca4ae 100644 --- a/erpnext/public/js/pos/clusterize.js +++ b/erpnext/public/js/pos/clusterize.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*! Clusterize.js - v0.17.6 - 2017-03-05 * http://NeXTs.github.com/Clusterize.js/ * Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */ From 7e752c4ebf3277b10ee17b72b61b129f6cf2c3d4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Aug 2017 18:56:26 +0530 Subject: [PATCH 29/36] Added patch, fixed codacy issue --- .../doctype/pos_settings/pos_settings.js | 2 +- .../doctype/pos_settings/pos_settings.json | 4 +- .../doctype/pos_settings/pos_settings.py | 1 - erpnext/patches.txt | 2 +- .../v8_7/set_offline_in_pos_settings.py | 12 +++++ .../js/utils/serial_no_batch_selector.js | 2 +- .../page/point_of_sale/point_of_sale.js | 52 +++++++++---------- .../page/point_of_sale/point_of_sale.py | 10 +--- 8 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 erpnext/patches/v8_7/set_offline_in_pos_settings.py diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js index fab766bb4b..1a14618513 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.js +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('POS Settings', { - refresh: function(frm) { + refresh: function() { } }); diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json index cdd1865b75..a04558da26 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.json +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -18,7 +18,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", + "default": "1", "fieldname": "is_online", "fieldtype": "Check", "hidden": 0, @@ -55,7 +55,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-08-29 14:34:36.166049", + "modified": "2017-08-30 18:34:58.960276", "modified_by": "Administrator", "module": "Accounts", "name": "POS Settings", diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py index c978a4e9ad..736d36eea9 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.py +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -3,7 +3,6 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe from frappe.model.document import Document class POSSettings(Document): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 35958e34f5..4e25a9469f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -435,4 +435,4 @@ erpnext.patches.v8_5.remove_project_type_property_setter 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 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 0000000000..64a3a7c806 --- /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/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index ed5a0f6b8d..3e2414e665 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -146,7 +146,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ this.map_row_values(this.item, this.values, 'serial_no', 'qty'); } refresh_field("items"); - this.callback && this.callback(this.item) + this.callback && this.callback(this.item); }, map_row_values: function(row, values, number, qty_field, warehouse) { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 4ce1ca1a33..6bbb267271 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,7 +1,7 @@ /* global Clusterize */ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ + frappe.ui.make_app_page({ parent: wrapper, title: 'Point of Sale', single_column: true @@ -156,7 +156,7 @@ class PointOfSale { .then(() => { // update cart this.update_cart_data(item); - }) + }); } return; } @@ -172,12 +172,12 @@ class PointOfSale { .trigger('item_code', item.doctype, item.name) .then(() => { // update cart - this.update_cart_data(item) + this.update_cart_data(item); }); } select_batch_and_serial_no(item) { - let dialog = new erpnext.SerialNoBatchSelector({ + new erpnext.SerialNoBatchSelector({ frm: this.frm, item: item, warehouse_details: { @@ -189,9 +189,9 @@ class PointOfSale { .then(() => { // update cart this.update_cart_data(item); - }) + }); } - }, true) + }, true); } update_cart_data(item) { @@ -202,7 +202,7 @@ class PointOfSale { update_item_in_frm(item, field, value) { if (field) { - frappe.model.set_value(item.doctype, item.name, field, value) + frappe.model.set_value(item.doctype, item.name, field, value); } return this.frm.script_manager @@ -219,7 +219,7 @@ class PointOfSale { frm: this.frm, events: { submit_form: () => { - this.submit_sales_invoice() + this.submit_sales_invoice(); } } }); @@ -323,7 +323,7 @@ class PointOfSale { // }).addClass('visible-xs'); this.page.add_menu_item(__("Form View"), function () { - var doc = frappe.model.sync(me.frm.doc); + frappe.model.sync(me.frm.doc); frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name); }); @@ -376,7 +376,7 @@ class POSCart {
-
${__('Item Name')}
+
${__('Item Name')}
${__('Quantity')}
${__('Discount')}
${__('Rate')}
@@ -610,8 +610,8 @@ class POSCart { get_item_html(item) { const rate = format_currency(item.rate, this.frm.doc.currency); return ` -
-
+
+
${item.item_name}
@@ -824,9 +824,9 @@ class POSItems { options: 'Item Group', default: 'All Item Groups', onchange: () => { - const item_group = this.item_group_field.get_value() + const item_group = this.item_group_field.get_value(); if (item_group) { - this.filter_items({ item_group: item_group }) + this.filter_items({ item_group: item_group }); } }, }, @@ -907,7 +907,7 @@ class POSItems { bind_events() { var me = this; - this.wrapper.on('click', '.pos-item-wrapper', function(e) { + 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'); @@ -1100,7 +1100,7 @@ class Payment { this.set_flag(); let title = __('Total Amount {0}', - [format_currency(this.frm.doc.grand_total, this.frm.doc.currency)]) + [format_currency(this.frm.doc.grand_total, this.frm.doc.currency)]); this.dialog = new frappe.ui.Dialog({ title: title, @@ -1153,7 +1153,7 @@ class Payment { options: me.frm.doc.currency, fieldname: p.mode_of_payment, default: p.amount, - onchange: (e) => { + onchange: () => { const value = this.dialog.get_value(this.fieldname); me.update_payment_value(this.fieldname, value); } @@ -1180,7 +1180,7 @@ class Payment { onchange: () => { me.update_cur_frm_value('write_off_amount', () => { frappe.flags.change_amount = false; - me.update_change_amount() + me.update_change_amount(); }); } }, @@ -1237,8 +1237,8 @@ class Payment { const value = this.dialog.get_value(fieldname); this.frm.set_value(fieldname, value) .then(() => { - callback() - }) + callback(); + }); } frappe.flags[fieldname] = true; @@ -1252,22 +1252,22 @@ class Payment { .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() + 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) + 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) + 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.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 4e2b1b1343..8ed288b6e9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -3,17 +3,9 @@ from __future__ import unicode_literals import frappe, json -from frappe import _ -from frappe.utils import nowdate -from erpnext.setup.utils import get_exchange_rate -from frappe.core.doctype.communication.email import make -from erpnext.stock.get_item_details import get_pos_profile -from erpnext.accounts.party import get_party_account_currency -from erpnext.controllers.accounts_controller import get_taxes_and_charges @frappe.whitelist() def get_items(start, page_length, price_list, item_group, search_value=""): - condition = "" serial_no = "" batch_no = "" item_code = search_value @@ -42,7 +34,7 @@ def get_items(start, page_length, price_list, item_group, search_value=""): 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.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)), From 3e4392776735caa844be53926be2e8d7c41b4b06 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 31 Aug 2017 11:16:14 +0530 Subject: [PATCH 30/36] Test cases --- .../page/point_of_sale/point_of_sale.js | 2 + .../page/point_of_sale/test_point_of_sale.js | 38 +++++++++++++++++++ erpnext/tests/ui/tests.txt | 1 + 3 files changed, 41 insertions(+) create mode 100644 erpnext/selling/page/point_of_sale/test_point_of_sale.js diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 6bbb267271..0e6ecdd7d0 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -507,8 +507,10 @@ class POSCart { 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()); } 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 0000000000..a178e5c2c2 --- /dev/null +++ b/erpnext/selling/page/point_of_sale/test_point_of_sale.js @@ -0,0 +1,38 @@ +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'), + () => frappe.timeout(0.2), + () => cur_frm.set_value('customer', 'Test Customer'), + () => frappe.timeout(2), + () => frappe.click_link('Test FG Item 2'), + () => frappe.timeout(0.2), + () => frappe.click_element(`.cart-items [title="_Test FG Item 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/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 653aeecc66..e76c67ea8f 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -50,6 +50,7 @@ erpnext/schools/doctype/room/test_room.js 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 From 6ab630371d0c6bedd3b15959253bf973750cc2d3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 1 Sep 2017 15:28:44 +0530 Subject: [PATCH 31/36] Fixed serial no trigger not working from dialog box --- .../doctype/pos_profile/pos_profile.js | 25 +++++++++-- .../doctype/pos_profile/pos_profile.json | 43 ++++++++++++++++--- .../point_of_sale/point_of_sale.json | 2 +- erpnext/public/js/controllers/transaction.js | 13 ++++-- .../page/point_of_sale/point_of_sale.js | 36 ++++++++-------- 5 files changed, 87 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 03d84e9ad6..97bbc1227f 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -8,10 +8,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { 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", "onload", function(frm) { }); 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 c4e6dabc17..187454ef33 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", @@ -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, @@ -969,6 +968,38 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "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, @@ -1291,7 +1322,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-08-27 16:39:00.713225", + "modified": "2017-09-01 15:55:14.890452", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", 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 28c853cc48..4e69cad06b 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": "\n\n

\n\t{{ company }}
\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}
\n

\n

\n\t{{ __(\"Customer\") }}: {{ customer }}
\n

\n\n

\n\t{{ __(\"Date\") }}: {{ dateutil.global_date_format(posting_date) }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{% for item in items %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endfor %}\n\t\n
{{ __(\"Item\") }}{{ __(\"Qty\") }}{{ __(\"Amount\") }}
\n\t\t\t\t{{ item.item_name }}\n\t\t\t{{ format_number(item.qty, null,precision(\"difference\")) }}
@ {{ format_currency(item.rate, currency) }}
{{ format_currency(item.amount, currency) }}
\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endif %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t
\n\t\t\t\t{{ row.description }}\n\t\t\t\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t
\n\n\n
\n

{{ terms }}

\n

{{ __(\"Thank you, please visit again.\") }}

", "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/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3a8ddb5927..f2c3d6057c 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 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ 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); + }); } } } @@ -1102,7 +1106,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, }); -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, @@ -1111,6 +1115,7 @@ erpnext.show_serial_batch_selector = function(frm, d) { type: "Warehouse", name: d.warehouse }, - }); + callback: callback + }, show_dialog); }); } diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 0e6ecdd7d0..354cdea1c2 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -139,6 +139,7 @@ class PointOfSale { 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' @@ -168,29 +169,28 @@ class PointOfSale { // 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(() => { - // update cart - this.update_cart_data(item); + 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) { - new erpnext.SerialNoBatchSelector({ - frm: this.frm, - item: item, - warehouse_details: { - type: "Warehouse", - name: item.warehouse - }, - callback: (item) => { - this.update_item_in_frm(item) - .then(() => { - // update cart - this.update_cart_data(item); - }); - } + erpnext.show_serial_batch_selector(this.frm, item, () => { + this.update_item_in_frm(item) + .then(() => { + // update cart + this.update_cart_data(item); + }); }, true); } @@ -308,7 +308,6 @@ class PointOfSale { frm.doc.items = []; frm.set_value('is_pos', 1); frm.meta.default_print_format = 'POS Invoice'; - return frm; } } @@ -340,6 +339,9 @@ class PointOfSale { 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); }); From 62b1dc777e1136014ca338f53085880516075f75 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 5 Sep 2017 17:10:24 +0530 Subject: [PATCH 32/36] Namespace PointOfSale class --- erpnext/selling/page/point_of_sale/point_of_sale.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 354cdea1c2..92ae3e89ae 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,4 +1,5 @@ /* global Clusterize */ +frappe.provide('erpnext.pos'); frappe.pages['point-of-sale'].on_page_load = function(wrapper) { frappe.ui.make_app_page({ @@ -10,7 +11,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { 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 PointOfSale(wrapper); + wrapper.pos = new erpnext.pos.PointOfSale(wrapper); window.cur_pos = wrapper.pos; } else { // offline @@ -19,7 +20,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { }); }; -class PointOfSale { +erpnext.pos.PointOfSale = class PointOfSale { constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; From 4bcaeb312c4ff2eb7d0387af4c9a467a68e6bc17 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 5 Sep 2017 17:29:45 +0530 Subject: [PATCH 33/36] fix codacy --- erpnext/selling/page/point_of_sale/point_of_sale.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 92ae3e89ae..c52b55a6cb 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -354,7 +354,7 @@ erpnext.pos.PointOfSale = class PointOfSale { this.frm.email_doc(); }); } -} +}; class POSCart { constructor({frm, wrapper, events}) { From 7419c4b57776001547b092086878bbef848b41cf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 6 Sep 2017 12:37:38 +0530 Subject: [PATCH 34/36] Fixed test cases --- .../page/point_of_sale/test_point_of_sale.js | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) 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 index a178e5c2c2..d5053b5cd1 100644 --- a/erpnext/selling/page/point_of_sale/test_point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/test_point_of_sale.js @@ -1,3 +1,21 @@ +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(); @@ -5,13 +23,13 @@ QUnit.test("test:Point of Sales", function(assert) { frappe.run_serially([ () => frappe.set_route('point-of-sale'), () => frappe.timeout(2), - () => frappe.set_control('customer', 'Test Customer'), + () => frappe.set_control('customer', 'Test Customer 1'), () => frappe.timeout(0.2), - () => cur_frm.set_value('customer', 'Test Customer'), + () => cur_frm.set_value('customer', 'Test Customer 1'), () => frappe.timeout(2), - () => frappe.click_link('Test FG Item 2'), + () => frappe.click_link('Test Product 2'), () => frappe.timeout(0.2), - () => frappe.click_element(`.cart-items [title="_Test FG Item 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), From c72d08e8b976159b64031d64ded8207182b459b8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 6 Sep 2017 14:16:51 +0530 Subject: [PATCH 35/36] Added indicator to show stock is available or not --- erpnext/selling/page/point_of_sale/point_of_sale.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c52b55a6cb..9cd2a49912 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -603,10 +603,16 @@ class POSCart { 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(); } @@ -614,8 +620,9 @@ class POSCart { 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 ` -
+
${item.item_name}
@@ -929,7 +936,7 @@ class POSItems { get_item_html(item) { const price_list_rate = format_currency(item.price_list_rate, this.currency); - const { item_code, item_name, item_image, item_stock=0} = item; + const { item_code, item_name, item_image} = item; const item_title = item_name || item_code; const template = ` @@ -939,7 +946,6 @@ class POSItems { ${item_title} -

(${__(item_stock)})

From d4e57a38a4c7b652586b9bd6a3bd7d7d36d2da63 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 7 Sep 2017 19:16:38 +0530 Subject: [PATCH 36/36] Fixed multiple ui test cases --- erpnext/accounts/page/pos/test_pos.js | 22 +++++------------- .../doctype/sales_order/test_sales_order.js | 23 ------------------- .../sales_order/tests/test_sales_order.js | 18 +++++++++------ .../{ => tests}/test_point_of_sale.js | 22 ++---------------- .../point_of_sale/tests/test_pos_settings.js | 17 ++++++++++++++ erpnext/tests/ui/tests.txt | 3 ++- 6 files changed, 38 insertions(+), 67 deletions(-) delete mode 100644 erpnext/selling/doctype/sales_order/test_sales_order.js rename erpnext/selling/page/point_of_sale/{ => tests}/test_point_of_sale.js (70%) create mode 100644 erpnext/selling/page/point_of_sale/tests/test_pos_settings.js diff --git a/erpnext/accounts/page/pos/test_pos.js b/erpnext/accounts/page/pos/test_pos.js index bc5edc9f2a..8913a9e1cc 100644 --- a/erpnext/accounts/page/pos/test_pos.js +++ b/erpnext/accounts/page/pos/test_pos.js @@ -1,16 +1,15 @@ -QUnit.test("test:POS Profile", function(assert) { - assert.expect(1); +QUnit.test("test:Sales Invoice", function(assert) { + assert.expect(3); let done = assert.async(); frappe.run_serially([ () => { return frappe.tests.make("POS Profile", [ {naming_series: "SINV"}, - {company: "Test Company"}, {country: "India"}, {currency: "INR"}, - {write_off_account: "Write Off - TC"}, - {write_off_cost_center: "Main - TC"}, + {write_off_account: "Write Off - FT"}, + {write_off_cost_center: "Main - FT"}, {payments: [ [ {"default": 1}, @@ -24,19 +23,10 @@ QUnit.test("test:POS Profile", function(assert) { () => { assert.equal(cur_frm.doc.payments[0].default, 1, "Default mode of payment tested"); }, - () => done() - ]); -}); - -QUnit.test("test:Sales Invoice", function(assert) { - assert.expect(2); - let done = assert.async(); - - frappe.run_serially([ + () => frappe.timeout(1), () => { return frappe.tests.make("Sales Invoice", [ {customer: "Test Customer 2"}, - {company: "Test Company"}, {is_pos: 1}, {posting_date: frappe.datetime.get_today()}, {due_date: frappe.datetime.get_today()}, @@ -44,7 +34,7 @@ QUnit.test("test:Sales Invoice", function(assert) { [ {"item_code": "Test Product 1"}, {"qty": 5}, - {"warehouse":'Stores - TC'} + {"warehouse":'Stores - FT'} ]] } ]); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.js b/erpnext/selling/doctype/sales_order/test_sales_order.js deleted file mode 100644 index 57ed19b696..0000000000 --- a/erpnext/selling/doctype/sales_order/test_sales_order.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Sales Order", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Sales Order', [ - // insert a new Sales Order - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js index 3eceb89ca2..6568d5cad0 100644 --- a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js @@ -1,7 +1,7 @@ QUnit.module('Sales Order'); QUnit.test("test sales order", function(assert) { - assert.expect(8); + assert.expect(10); let done = assert.async(); frappe.run_serially([ () => { @@ -12,7 +12,7 @@ QUnit.test("test sales order", function(assert) { {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)}, {'qty': 5}, {'item_code': 'Test Product 4'}, - {'uom': 'unit'}, + {'uom': 'Nos'}, {'margin_type': 'Percentage'}, {'discount_percentage': 10}, ] @@ -33,7 +33,7 @@ QUnit.test("test sales order", function(assert) { {additional_discount_percentage:10} ]); }, - () => cur_frm.save(), + () => frappe.timeout(1), () => { // get_item_details assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct"); @@ -42,15 +42,19 @@ QUnit.test("test sales order", function(assert) { // get tax account head details assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct"); // calculate totals - assert.ok(cur_frm.doc.items[0].price_list_rate==1000, "Item 1 price_list_rate"); - assert.ok(cur_frm.doc.total== 4500, "total correct "); - assert.ok(cur_frm.doc.rounded_total== 4414.5, "rounded total correct "); - + assert.ok(cur_frm.doc.items[0].price_list_rate==90, "Item 1 price_list_rate"); + assert.ok(cur_frm.doc.total== 405, "total correct "); + assert.ok(cur_frm.doc.net_total== 364.5, "net total correct "); + assert.ok(cur_frm.doc.grand_total== 397.30, "grand total correct "); + assert.ok(cur_frm.doc.rounded_total== 397.30, "rounded total correct "); }, + () => cur_frm.save(), + () => frappe.timeout(1), () => cur_frm.print_doc(), () => frappe.timeout(1), () => { assert.ok($('.btn-print-print').is(':visible'), "Print Format Available"); + frappe.timeout(1); assert.ok($(".section-break+ .section-break .column-break:nth-child(1) .data-field:nth-child(1) .value").text().includes("Billing Street 1"), "Print Preview Works As Expected"); }, () => cur_frm.print_doc(), diff --git a/erpnext/selling/page/point_of_sale/test_point_of_sale.js b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js similarity index 70% rename from erpnext/selling/page/point_of_sale/test_point_of_sale.js rename to erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js index d5053b5cd1..c70d076c70 100644 --- a/erpnext/selling/page/point_of_sale/test_point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js @@ -1,21 +1,3 @@ -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(); @@ -29,7 +11,7 @@ QUnit.test("test:Point of Sales", function(assert) { () => frappe.timeout(2), () => frappe.click_link('Test Product 2'), () => frappe.timeout(0.2), - () => frappe.click_element(`.cart-items [title="Test Product 2"]`), + () => frappe.click_element(`.cart-items [data-item-code="Test Product 2"]`), () => frappe.timeout(0.2), () => frappe.click_element(`.number-pad [data-value="Rate"]`), () => frappe.timeout(0.2), @@ -49,7 +31,7 @@ QUnit.test("test:Point of Sales", function(assert) { () => frappe.timeout(0.2), () => frappe.click_button('Submit'), () => frappe.click_button('Yes'), - () => frappe.timeout(5), + () => frappe.timeout(3), () => assert.ok(cur_frm.doc.docstatus==1, "Sales invoice created successfully"), () => done() ]); diff --git a/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js b/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js new file mode 100644 index 0000000000..d9b8cf8274 --- /dev/null +++ b/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js @@ -0,0 +1,17 @@ +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(10), + () => assert.ok(cur_frm.doc.is_online==1, "Enabled online"), + () => frappe.timeout(2), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 9bb520fc67..4b62dd6b96 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -50,7 +50,8 @@ erpnext/schools/doctype/room/test_room.js 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/selling/page/point_of_sale/tests/test_pos_settings.js +erpnext/selling/page/point_of_sale/tests/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