From ba3f0e6b7060bf874130bc568951ac8617c7ee4a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Aug 2017 17:19:28 +0530 Subject: [PATCH] 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