From 2144e02d3c5b377eda9dd7c09279ab91a54e821c Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 17:27:09 +0100 Subject: [PATCH] - Added multiple barcode feature per item --- erpnext/accounts/doctype/sales_invoice/pos.py | 149 ++++++++++++------ erpnext/accounts/page/pos/pos.js | 13 +- erpnext/controllers/accounts_controller.py | 2 +- erpnext/controllers/queries.py | 2 +- erpnext/public/js/controllers/transaction.js | 3 + .../page/point_of_sale/point_of_sale.py | 2 +- erpnext/stock/doctype/item/item.json | 92 +++++++---- erpnext/stock/doctype/item/item.py | 10 +- .../doctype/stock_settings/stock_settings.py | 2 +- erpnext/stock/get_item_details.py | 4 +- 10 files changed, 176 insertions(+), 103 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index 63db16ca55..4be16ba2b3 100644 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -2,54 +2,64 @@ # 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 + +import json + +import frappe from erpnext.accounts.party import get_party_account_currency from erpnext.controllers.accounts_controller import get_taxes_and_charges +from erpnext.setup.utils import get_exchange_rate +from erpnext.stock.get_item_details import get_pos_profile +from frappe import _ +from frappe.core.doctype.communication.email import make +from frappe.utils import nowdate + @frappe.whitelist() def get_pos_data(): - doc = frappe.new_doc('Sales Invoice') - doc.is_pos = 1; - pos_profile = get_pos_profile(doc.company) or {} - if not pos_profile: - frappe.throw(_("POS Profile is required to use Point-of-Sale")) - if not doc.company: doc.company = pos_profile.get('company') - doc.update_stock = pos_profile.get('update_stock') + doc = frappe.new_doc('Sales Invoice') + doc.is_pos = 1 + pos_profile = get_pos_profile(doc.company) or {} + if not pos_profile: + frappe.throw(_("POS Profile is required to use Point-of-Sale")) - if pos_profile.get('name'): - pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name')) - pos_profile.validate() + if not doc.company: + doc.company = pos_profile.get('company') - company_data = get_company_data(doc.company) - update_pos_profile_data(doc, pos_profile, company_data) - update_multi_mode_option(doc, pos_profile) - default_print_format = pos_profile.get('print_format') or "Point of Sale" - print_template = frappe.db.get_value('Print Format', default_print_format, 'html') - customers = get_customers_list(pos_profile) + doc.update_stock = pos_profile.get('update_stock') + + if pos_profile.get('name'): + pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name')) + pos_profile.validate() + + company_data = get_company_data(doc.company) + update_pos_profile_data(doc, pos_profile, company_data) + update_multi_mode_option(doc, pos_profile) + default_print_format = pos_profile.get('print_format') or "Point of Sale" + print_template = frappe.db.get_value('Print Format', default_print_format, 'html') + items_list = get_items_list(pos_profile) + customers = get_customers_list(pos_profile) + + return { + 'doc': doc, + 'default_customer': pos_profile.get('customer'), + 'items': items_list, + 'item_groups': get_item_groups(pos_profile), + 'customers': customers, + 'address': get_customers_address(customers), + 'contacts': get_contacts(customers), + 'serial_no_data': get_serial_no_data(pos_profile, doc.company), + 'batch_no_data': get_batch_no_data(), + 'barcode_data': get_barcode_data(items_list), + 'tax_data': get_item_tax_data(), + 'price_list_data': get_price_list_data(doc.selling_price_list), + 'bin_data': get_bin_data(pos_profile), + 'pricing_rules': get_pricing_rule_data(doc), + 'print_template': print_template, + 'pos_profile': pos_profile, + 'meta': get_meta() + } - return { - 'doc': doc, - 'default_customer': pos_profile.get('customer'), - 'items': get_items_list(pos_profile), - 'item_groups': get_item_groups(pos_profile), - 'customers': customers, - 'address': get_customers_address(customers), - 'contacts': get_contacts(customers), - 'serial_no_data': get_serial_no_data(pos_profile, doc.company), - 'batch_no_data': get_batch_no_data(), - 'tax_data': get_item_tax_data(), - 'price_list_data': get_price_list_data(doc.selling_price_list), - 'bin_data': get_bin_data(pos_profile), - 'pricing_rules': get_pricing_rule_data(doc), - 'print_template': print_template, - 'pos_profile': pos_profile, - 'meta': get_meta() - } def get_meta(): doctype_meta = { @@ -57,14 +67,16 @@ def get_meta(): 'invoice': frappe.get_meta('Sales Invoice') } - for row in frappe.get_all('DocField', fields = ['fieldname', 'options'], - filters = {'parent': 'Sales Invoice', 'fieldtype': 'Table'}): + for row in frappe.get_all('DocField', fields=['fieldname', 'options'], + filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): doctype_meta[row.fieldname] = frappe.get_meta(row.options) return doctype_meta + def get_company_data(company): - return frappe.get_all('Company', fields = ["*"], filters= {'name': company})[0] + return frappe.get_all('Company', fields=["*"], filters={'name': company})[0] + def update_pos_profile_data(doc, pos_profile, company_data): doc.campaign = pos_profile.get('campaign') @@ -93,15 +105,18 @@ def update_pos_profile_data(doc, pos_profile, company_data): doc.apply_discount_on = pos_profile.get('apply_discount_on') or 'Grand Total' doc.customer_group = pos_profile.get('customer_group') or get_root('Customer Group') doc.territory = pos_profile.get('territory') or get_root('Territory') - doc.terms = frappe.db.get_value('Terms and Conditions', pos_profile.get('tc_name'), 'terms') or doc.terms or '' + doc.terms = frappe.db.get_value('Terms and Conditions', pos_profile.get( + 'tc_name'), 'terms') or doc.terms or '' doc.offline_pos_name = '' + def get_root(table): root = frappe.db.sql(""" select name from `tab%(table)s` having - min(lft)"""%{'table': table}, as_dict=1) + min(lft)""" % {'table': table}, as_dict=1) return root[0].name + def update_multi_mode_option(doc, pos_profile): from frappe.model import default_fields @@ -123,15 +138,18 @@ def update_multi_mode_option(doc, pos_profile): doc.append('payments', payment_mode) + def get_mode_of_payment(doc): return frappe.db.sql(""" select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa, `tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s""", {'company': doc.company}, as_dict=1) + def update_tax_table(doc): taxes = get_taxes_and_charges('Sales Taxes and Charges Template', doc.taxes_and_charges) for tax in taxes: doc.append('taxes', tax) + def get_items_list(pos_profile): cond = "1=1" item_groups = [] @@ -139,12 +157,12 @@ def get_items_list(pos_profile): # Get items based on the item groups defined in the POS profile for d in pos_profile.get('item_groups'): item_groups.extend([d.name for d in get_child_nodes('Item Group', d.item_group)]) - cond = "item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "item_group in (%s)" % (', '.join(['%s'] * len(item_groups))) - return frappe.db.sql(""" + return frappe.db.sql(""" select name, item_code, item_name, description, item_group, expense_account, has_batch_no, - has_serial_no, expense_account, selling_cost_center, stock_uom, image, + has_serial_no, expense_account, selling_cost_center, stock_uom, image, default_warehouse, is_stock_item, barcode, brand from tabItem @@ -152,6 +170,7 @@ def get_items_list(pos_profile): disabled = 0 and has_variants = 0 and is_sales_item = 1 and {cond} """.format(cond=cond), tuple(item_groups), as_dict=1) + def get_item_groups(pos_profile): item_group_dict = {} item_groups = frappe.db.sql("""Select name, @@ -161,6 +180,7 @@ def get_item_groups(pos_profile): item_group_dict[data.name] = [data.lft, data.rgt] return item_group_dict + def get_customers_list(pos_profile={}): cond = "1=1" customer_groups = [] @@ -168,12 +188,13 @@ def get_customers_list(pos_profile={}): # Get customers based on the customer groups defined in the POS profile for d in pos_profile.get('customer_groups'): customer_groups.extend([d.name for d in get_child_nodes('Customer Group', d.customer_group)]) - cond = "customer_group in (%s)"%(', '.join(['%s']*len(customer_groups))) + cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups))) return frappe.db.sql(""" select name, customer_name, customer_group, territory, customer_pos_id from tabCustomer where disabled = 0 and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {} + def get_customers_address(customers): customer_address = {} if isinstance(customers, basestring): @@ -192,26 +213,29 @@ def get_customers_address(customers): return customer_address + def get_contacts(customers): customer_contact = {} if isinstance(customers, basestring): customers = [frappe._dict({'name': customers})] for data in customers: - contact = frappe.db.sql(""" select email_id, phone, mobile_no from `tabContact` + contact = frappe.db.sql(""" select email_id, phone, mobile_no from `tabContact` where is_primary_contact =1 and name in (select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s and parenttype = 'Contact')""", data.name, as_dict=1) - if contact: + if contact: customer_contact[data.name] = contact[0] return customer_contact + def get_child_nodes(group_type, root): lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1) + def get_serial_no_data(pos_profile, company): # get itemwise serial no data # example {'Nokia Lumia 1020': {'SN0001': 'Pune'}} @@ -232,6 +256,7 @@ def get_serial_no_data(pos_profile, company): return itemwise_serial_no + def get_batch_no_data(): # get itemwise batch no data # exmaple: {'LED-GRE': [Batch001, Batch002]} @@ -248,6 +273,26 @@ def get_batch_no_data(): return itemwise_batch + +def get_barcode_data(items_list): + # get itemwise batch no data + # exmaple: {'LED-GRE': [Batch001, Batch002]} + # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse + + itemwise_barcode = {} + for item in items_list: + barcodes = frappe.db.sql(""" + select barcode from `tabItem Barcode` where parent = '{0}' + """.format(item.item_code), as_dict=1) + + for barcode in barcodes: + if item.item_code not in itemwise_barcode: + itemwise_barcode.setdefault(item.item_code, []) + itemwise_barcode[item.item_code].append(barcode) + + return itemwise_barcode + + def get_item_tax_data(): # get default tax of an item # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} @@ -425,7 +470,7 @@ def make_contact(args,customer): def make_address(args, customer): if not args.get('address_line1'): return - + name = args.get('name') if not name: diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 558dd8dc1c..b2449fd74c 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -296,6 +296,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ this.customers = r.message.customers; this.serial_no_data = r.message.serial_no_data; this.batch_no_data = r.message.batch_no_data; + this.barcode_data = r.message.barcode_data; this.tax_data = r.message.tax_data; this.contacts = r.message.contacts; this.address = r.message.address || {}; @@ -410,7 +411,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ }); this.serach_item.make_input(); - + this.serach_item.$input.on("keypress", function (event) { clearTimeout(me.last_search_timeout); @@ -418,7 +419,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ if((me.serach_item.$input.val() != "") || (event.which == 13)) { me.items = me.get_items(); me.make_item_list(); - } + } }, 400); }); @@ -1105,9 +1106,9 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ search_status = false; me.item_serial_no[item.item_code] = [me.serach_item.$input.val(), me.serial_no_data[item.item_code][me.serach_item.$input.val()]] return true - } else if (item.barcode == me.serach_item.$input.val()) { + } else if (in_list(me.barcode_data[item.item_code], me.serach_item.$input.val())) { search_status = false; - return item.barcode == me.serach_item.$input.val(); + return true; } else if (reg.test(item.item_code.toLowerCase()) || (item.description && reg.test(item.description.toLowerCase())) || reg.test(item.item_name.toLowerCase()) || reg.test(item.item_group.toLowerCase())) { return true @@ -1512,8 +1513,8 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ me.print_document(html) }) } - - if (this.frm.doc.docstatus == 1) { + + if (this.frm.doc.docstatus == 1) { this.page.add_menu_item(__("Email"), function () { me.email_prompt() }) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fb97c91f1f..2427b30f63 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled from erpnext.exceptions import InvalidCurrency -force_item_fields = ("item_group", "barcode", "brand", "stock_uom") +force_item_fields = ("item_group", "barcodes", "brand", "stock_uom") class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index a71a08e7fc..d2df30d84d 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -165,8 +165,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals and (tabItem.`{key}` LIKE %(txt)s or tabItem.item_group LIKE %(txt)s or tabItem.item_name LIKE %(txt)s - or tabItem.barcode LIKE %(txt)s or tabItem.description LIKE %(txt)s) + or tabItem.name IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s) {fcond} {mcond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3492ad954d..ed81ff71a6 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -273,6 +273,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ // barcode cleared, remove item d.item_code = ""; } + console.log(d.barcode) this.item_code(doc, cdt, cdn, true); }, @@ -281,6 +282,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var item = frappe.get_doc(cdt, cdn); var update_stock = 0, show_batch_dialog = 0; + console.log(from_barcode) if(['Sales Invoice'].includes(this.frm.doc.doctype)) { update_stock = cint(me.frm.doc.update_stock); show_batch_dialog = update_stock; @@ -294,6 +296,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!from_barcode) { item.barcode = null; } + console.log(item) if(item.item_code || item.barcode || item.serial_no) { if(!this.validate_company_and_party()) { this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove(); 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 d98a01706e..8801696de2 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -28,7 +28,7 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p batch_no, item_code = batch_no_data if not serial_no and not batch_no: - barcode_data = frappe.db.get_value('Item', {'barcode': search_value}, ['name', 'barcode']) + barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['parent', 'barcode']) if barcode_data: item_code, barcode = barcode_data diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index a97a24fe9e..efc52af412 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -172,35 +172,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "barcode", - "fieldtype": "Data", - "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": "Barcode", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "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, @@ -700,6 +671,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_barcodes", + "fieldtype": "Section Break", + "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": "Barcodes", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "barcodes", + "fieldtype": "Table", + "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": "Barcodes", + "length": 0, + "no_copy": 0, + "options": "Item Barcode", + "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, @@ -3453,7 +3485,7 @@ "issingle": 0, "istable": 0, "max_attachments": 1, - "modified": "2017-12-08 07:20:25.932499", + "modified": "2017-12-10 16:56:30.775568", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index df90ad9f54..e2ab8382e3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -85,7 +85,7 @@ class Item(WebsiteGenerator): self.check_for_active_boms() self.fill_customer_code() self.check_item_tax() - self.validate_barcode() + # self.validate_barcode() self.validate_warehouse_for_reorder() self.update_bom_item_desc() self.synced_with_hub = 0 @@ -465,14 +465,6 @@ class Item(WebsiteGenerator): else: check_list.append(d.tax_type) - def validate_barcode(self): - if self.barcode: - duplicate = frappe.db.sql("""select name from tabItem where barcode = %s - and name != %s""", (self.barcode, self.name)) - if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format(self.barcode, duplicate[0][0])) - - def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' warehouse = [] diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 89ece33b08..23a18f4e31 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -25,7 +25,7 @@ class StockSettings(Document): frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) # show/hide barcode field - frappe.make_property_setter({'fieldname': 'barcode', 'property': 'hidden', + frappe.make_property_setter({'fieldname': 'barcodes', 'property': 'hidden', 'value': 0 if self.show_barcode_field else 1}) self.cant_change_valuation_method() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 0c9d0c74bd..d107e73051 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -120,7 +120,7 @@ def process_args(args): @frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: - item_code = frappe.db.get_value("Item", {"barcode": barcode}) + item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"]) if not item_code: frappe.throw(_("No Item with Barcode {0}").format(barcode)) elif serial_no: @@ -268,7 +268,7 @@ def get_basic_details(args, item): if not out[d[1]] or (company and args.company != company): out[d[1]] = frappe.db.get_value("Company", args.company, d[2]) if d[2] else None - for fieldname in ("item_name", "item_group", "barcode", "brand", "stock_uom"): + for fieldname in ("item_name", "item_group", "barcodes", "brand", "stock_uom"): out[fieldname] = item.get(fieldname) return out