From 2144e02d3c5b377eda9dd7c09279ab91a54e821c Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 17:27:09 +0100 Subject: [PATCH 01/22] - 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 From 9b7e52cafba74d23d181b577934767ad60d2b146 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 18:11:48 +0100 Subject: [PATCH 02/22] - added barcode validation against Item Barcode --- erpnext/stock/doctype/item/item.py | 1423 ++++++++++++++-------------- 1 file changed, 730 insertions(+), 693 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index e2ab8382e3..8871ba385c 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -2,736 +2,770 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe -import erpnext -import json -import itertools -from frappe import msgprint, _ -from frappe.utils import (cstr, flt, cint, getdate, now_datetime, formatdate, - strip, get_timestamp, random_string) -from frappe.utils.html_utils import clean_html -from frappe.website.website_generator import WebsiteGenerator -from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups -from frappe.website.render import clear_cache -from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow -from erpnext.controllers.item_variant import (get_variant, copy_attributes_to_variant, - make_variant_item_code, validate_item_variant_attributes, ItemVariantExistsError) -class DuplicateReorderRows(frappe.ValidationError): pass -class StockExistsForTemplate(frappe.ValidationError): pass +import itertools +import json + +import erpnext +import frappe +from erpnext.controllers.item_variant import (ItemVariantExistsError, + copy_attributes_to_variant, + get_variant, + make_variant_item_code, + validate_item_variant_attributes) +from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, + invalidate_cache_for) +from frappe import _, msgprint +from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, + now_datetime, random_string, strip) +from frappe.utils.html_utils import clean_html +from frappe.website.doctype.website_slideshow.website_slideshow import \ + get_slideshow +from frappe.website.render import clear_cache +from frappe.website.website_generator import WebsiteGenerator + + +class DuplicateReorderRows(frappe.ValidationError): + pass + + +class StockExistsForTemplate(frappe.ValidationError): + pass + class Item(WebsiteGenerator): - website = frappe._dict( - page_title_field = "item_name", - condition_field = "show_in_website", - template = "templates/generators/item.html", - no_cache = 1 - ) + website = frappe._dict( + page_title_field="item_name", + condition_field="show_in_website", + template="templates/generators/item.html", + no_cache=1 + ) - def onload(self): - super(Item, self).onload() + def onload(self): + super(Item, self).onload() - self.set_onload('stock_exists', self.stock_ledger_created()) - if self.is_fixed_asset: - asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) - self.set_onload("asset_exists", True if asset else False) + self.set_onload('stock_exists', self.stock_ledger_created()) + if self.is_fixed_asset: + asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) + self.set_onload("asset_exists", True if asset else False) - def autoname(self): - if frappe.db.get_default("item_naming_by")=="Naming Series": - if self.variant_of: - if not self.item_code: - template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name") - self.item_code = make_variant_item_code(self.variant_of, template_item_name, self) - else: - from frappe.model.naming import make_autoname - self.item_code = make_autoname(self.naming_series+'.#####') - elif not self.item_code: - msgprint(_("Item Code is mandatory because Item is not automatically numbered"), raise_exception=1) + def autoname(self): + if frappe.db.get_default("item_naming_by") == "Naming Series": + if self.variant_of: + if not self.item_code: + template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name") + self.item_code = make_variant_item_code(self.variant_of, template_item_name, self) + else: + from frappe.model.naming import make_autoname + self.item_code = make_autoname(self.naming_series + '.#####') + elif not self.item_code: + msgprint(_("Item Code is mandatory because Item is not automatically numbered"), raise_exception=1) - self.item_code = strip(self.item_code) - self.name = self.item_code + self.item_code = strip(self.item_code) + self.name = self.item_code - def before_insert(self): - if not self.description: - self.description = self.item_name + def before_insert(self): + if not self.description: + self.description = self.item_name - if self.is_sales_item and not self.get('is_item_from_hub'): - self.publish_in_hub = 1 + if self.is_sales_item and not self.get('is_item_from_hub'): + self.publish_in_hub = 1 - def after_insert(self): - '''set opening stock and item price''' - if self.standard_rate: - self.add_price() + def after_insert(self): + '''set opening stock and item price''' + if self.standard_rate: + self.add_price() - if self.opening_stock: - self.set_opening_stock() + if self.opening_stock: + self.set_opening_stock() - def validate(self): - self.get_doc_before_save() + def validate(self): + self.get_doc_before_save() - super(Item, self).validate() + super(Item, self).validate() - if not self.item_name: - self.item_name = self.item_code + if not self.item_name: + self.item_name = self.item_code - if not self.description: - self.description = self.item_name + if not self.description: + self.description = self.item_name - self.validate_uom() - self.validate_description() - self.add_default_uom_in_conversion_factor_table() - self.validate_conversion_factor() - self.validate_item_type() - self.check_for_active_boms() - self.fill_customer_code() - self.check_item_tax() - # self.validate_barcode() - self.validate_warehouse_for_reorder() - self.update_bom_item_desc() - self.synced_with_hub = 0 + self.validate_uom() + self.validate_description() + self.add_default_uom_in_conversion_factor_table() + self.validate_conversion_factor() + self.validate_item_type() + self.check_for_active_boms() + self.fill_customer_code() + self.check_item_tax() + self.validate_barcode() + self.validate_warehouse_for_reorder() + self.update_bom_item_desc() + self.synced_with_hub = 0 - self.validate_has_variants() - self.validate_stock_exists_for_template_item() - self.validate_attributes() - self.validate_variant_attributes() - self.validate_website_image() - self.make_thumbnail() - self.validate_fixed_asset() - self.validate_retain_sample() + self.validate_has_variants() + self.validate_stock_exists_for_template_item() + self.validate_attributes() + self.validate_variant_attributes() + self.validate_website_image() + self.make_thumbnail() + self.validate_fixed_asset() + self.validate_retain_sample() - if not self.get("__islocal"): - self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") - self.old_website_item_groups = frappe.db.sql_list("""select item_group + if not self.get("__islocal"): + self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") + self.old_website_item_groups = frappe.db.sql_list("""select item_group from `tabWebsite Item Group` where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) - def on_update(self): - invalidate_cache_for_item(self) - self.validate_name_with_item_group() - self.update_variants() - self.update_item_price() - self.update_template_item() - - def validate_description(self): - '''Clean HTML description if set''' - if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): - self.description = clean_html(self.description) - - def add_price(self, price_list=None): - '''Add a new price''' - if not price_list: - price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) - if price_list: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list, - "item_code": self.name, - "currency": erpnext.get_default_currency(), - "price_list_rate": self.standard_rate - }) - item_price.insert() - - def set_opening_stock(self): - '''set opening stock''' - if not self.is_stock_item or self.has_serial_no or self.has_batch_no: - return - - if not self.valuation_rate and self.standard_rate: - self.valuation_rate = self.standard_rate - - if not self.valuation_rate: - frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) - - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - # default warehouse, or Stores - default_warehouse = (self.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse') - or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) - - if default_warehouse: - stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, - qty=self.opening_stock, rate=self.valuation_rate) - - stock_entry.add_comment("Comment", _("Opening Stock")) - - def make_route(self): - if not self.route: - return cstr(frappe.db.get_value('Item Group', self.item_group, - 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) - - def validate_website_image(self): - """Validate if the website image is a public file""" - auto_set_website_image = False - if not self.website_image and self.image: - auto_set_website_image = True - self.website_image = self.image - - if not self.website_image: - return - - # find if website image url exists as public - file_doc = frappe.get_all("File", filters={ - "file_url": self.website_image - }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) - - - if file_doc: - file_doc = file_doc[0] - - if not file_doc: - if not auto_set_website_image: - frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") - .format(self.website_image, self.name)) - - self.website_image = None - - elif file_doc.is_private: - if not auto_set_website_image: - frappe.msgprint(_("Website Image should be a public file or website URL")) - - self.website_image = None - - def make_thumbnail(self): - """Make a thumbnail of `website_image`""" - import requests.exceptions - - if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): - self.thumbnail = None - - if self.website_image and not self.thumbnail: - file_doc = None - - try: - file_doc = frappe.get_doc("File", { - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }) - except frappe.DoesNotExistError: - pass - # cleanup - frappe.local.message_log.pop() - - except requests.exceptions.HTTPError: - frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) - self.website_image = None - - except requests.exceptions.SSLError: - frappe.msgprint(_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) - self.website_image = None - - # for CSV import - if self.website_image and not file_doc: - try: - file_doc = frappe.get_doc({ - "doctype": "File", - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }).insert() - - except IOError: - self.website_image = None - - if file_doc: - if not file_doc.thumbnail_url: - file_doc.make_thumbnail() - - self.thumbnail = file_doc.thumbnail_url - - def validate_fixed_asset(self): - if self.is_fixed_asset: - if self.is_stock_item: - frappe.throw(_("Fixed Asset Item must be a non-stock item.")) - - if not self.asset_category: - frappe.throw(_("Asset Category is mandatory for Fixed Asset item")) - - if self.stock_ledger_created(): - frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created.")) - - if not self.is_fixed_asset: - asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) - if asset: - frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) - - def validate_retain_sample(self): - if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): - frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")); - if self.retain_sample and not self.has_batch_no: - frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(self.item_code)) - - def get_context(self, context): - context.show_search=True - context.search_link = '/product_search' - - context.parents = get_parent_item_groups(self.item_group) - - self.set_variant_context(context) - self.set_attribute_context(context) - self.set_disabled_attributes(context) - - return context - - def set_variant_context(self, context): - if self.has_variants: - context.no_cache = True - - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all("Item", - filters={"variant_of": self.name, "show_variant_in_website": 1}, - order_by="name asc") - - variant = frappe.form_dict.variant - if not variant and context.variants: - # the case when the item is opened for the first time from its list - variant = context.variants[0] - - if variant: - context.variant = frappe.get_doc("Item", variant) - - for fieldname in ("website_image", "web_long_description", "description", - "website_specifications"): - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] - - context[fieldname] = value - - if self.slideshow: - if context.variant and context.variant.slideshow: - context.update(get_slideshow(context.variant)) - else: - context.update(get_slideshow(self)) - - def set_attribute_context(self, context): - if self.has_variants: - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} - - # load attributes - for v in context.variants: - v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], filters={"parent": v.name}) - - for attr in v.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.attribute_value not in values: - values.append(attr.attribute_value) - - if v.name==context.variant.name: - context.selected_attributes[attr.attribute] = attr.attribute_value - - # filter attributes, order based on attribute table - for attr in self.attributes: - values = context.attribute_values.setdefault(attr.attribute, []) - - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) - - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): - - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) - - context.variant_info = json.dumps(context.variants) - - def set_disabled_attributes(self, context): - """Disable selection options of attribute combinations that do not result in a variant""" - if not self.attributes or not self.has_variants: - return - - context.disabled_attributes = {} - attributes = [attr.attribute for attr in self.attributes] - - def find_variant(combination): - for variant in context.variants: - if len(variant.attributes) < len(attributes): - continue - - if "combination" not in variant: - ref_combination = [] - - for attr in variant.attributes: - idx = attributes.index(attr.attribute) - ref_combination.insert(idx, attr.attribute_value) - - variant["combination"] = ref_combination - - if not (set(combination) - set(variant["combination"])): - # check if the combination is a subset of a variant combination - # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] - return True - - for i, attr in enumerate(self.attributes): - if i==0: - continue - - combination_source = [] - - # loop through previous attributes - for prev_attr in self.attributes[:i]: - combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) - - combination_source.append(context.attribute_values[attr.attribute]) - - for combination in itertools.product(*combination_source): - if not find_variant(combination): - context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) - - def add_default_uom_in_conversion_factor_table(self): - uom_conv_list = [d.uom for d in self.get("uoms")] - if self.stock_uom not in uom_conv_list: - ch = self.append('uoms', {}) - ch.uom = self.stock_uom - ch.conversion_factor = 1 - - to_remove = [] - for d in self.get("uoms"): - if d.conversion_factor == 1 and d.uom != self.stock_uom: - to_remove.append(d) - - [self.remove(d) for d in to_remove] - - def update_template_tables(self): - template = frappe.get_doc("Item", self.variant_of) - - # add item taxes from template - for d in template.get("taxes"): - self.append("taxes", {"tax_type": d.tax_type, "tax_rate": d.tax_rate}) - - # copy re-order table if empty - if not self.get("reorder_levels"): - for d in template.get("reorder_levels"): - n = {} - for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): - n[k] = d.get(k) - self.append("reorder_levels", n) - - def validate_conversion_factor(self): - check_list = [] - for d in self.get('uoms'): - if cstr(d.uom) in check_list: - frappe.throw(_("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) - else: - check_list.append(cstr(d.uom)) - - if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: - frappe.throw(_("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) - - def validate_item_type(self): - if self.has_serial_no == 1 and self.is_stock_item == 0: - msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) - - if self.has_serial_no == 0 and self.serial_no_series: - self.serial_no_series = None - - - def check_for_active_boms(self): - if self.default_bom: - bom_item = frappe.db.get_value("BOM", self.default_bom, "item") - if bom_item not in (self.name, self.variant_of): - frappe.throw(_("Default BOM ({0}) must be active for this item or its template").format(bom_item)) - - def fill_customer_code(self): - """ Append all the customer codes and insert into "customer_code" field of item table """ - cust_code=[] - for d in self.get('customer_items'): - cust_code.append(d.ref_code) - self.customer_code=','.join(cust_code) - - def check_item_tax(self): - """Check whether Tax Rate is not entered twice for same Tax Type""" - check_list=[] - for d in self.get('taxes'): - if d.tax_type: - account_type = frappe.db.get_value("Account", d.tax_type, "account_type") - - if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account']: - frappe.throw(_("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(d.idx)) - else: - if d.tax_type in check_list: - frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) - else: - check_list.append(d.tax_type) - - def validate_warehouse_for_reorder(self): - '''Validate Reorder level table for duplicate and conditional mandatory''' - warehouse = [] - for d in self.get("reorder_levels"): - if not d.warehouse_group: - d.warehouse_group = d.warehouse - if d.get("warehouse") and d.get("warehouse") not in warehouse: - warehouse += [d.get("warehouse")] - else: - frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) - - if d.warehouse_reorder_level and not d.warehouse_reorder_qty: - frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) - - def stock_ledger_created(self): - if not hasattr(self, '_stock_ledger_created'): - self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` + def on_update(self): + invalidate_cache_for_item(self) + self.validate_name_with_item_group() + self.update_variants() + self.update_item_price() + self.update_template_item() + + def validate_description(self): + '''Clean HTML description if set''' + if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): + self.description = clean_html(self.description) + + def add_price(self, price_list=None): + '''Add a new price''' + if not price_list: + price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') + or frappe.db.get_value('Price List', _('Standard Selling'))) + if price_list: + item_price = frappe.get_doc({ + "doctype": "Item Price", + "price_list": price_list, + "item_code": self.name, + "currency": erpnext.get_default_currency(), + "price_list_rate": self.standard_rate + }) + item_price.insert() + + def set_opening_stock(self): + '''set opening stock''' + if not self.is_stock_item or self.has_serial_no or self.has_batch_no: + return + + if not self.valuation_rate and self.standard_rate: + self.valuation_rate = self.standard_rate + + if not self.valuation_rate: + frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) + + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + # default warehouse, or Stores + default_warehouse = (self.default_warehouse + or frappe.db.get_single_value('Stock Settings', 'default_warehouse') + or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) + + if default_warehouse: + stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, + qty=self.opening_stock, rate=self.valuation_rate) + + stock_entry.add_comment("Comment", _("Opening Stock")) + + def make_route(self): + if not self.route: + return cstr(frappe.db.get_value('Item Group', self.item_group, + 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + + def validate_website_image(self): + """Validate if the website image is a public file""" + auto_set_website_image = False + if not self.website_image and self.image: + auto_set_website_image = True + self.website_image = self.image + + if not self.website_image: + return + + # find if website image url exists as public + file_doc = frappe.get_all("File", filters={ + "file_url": self.website_image + }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) + + if file_doc: + file_doc = file_doc[0] + + if not file_doc: + if not auto_set_website_image: + frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") + .format(self.website_image, self.name)) + + self.website_image = None + + elif file_doc.is_private: + if not auto_set_website_image: + frappe.msgprint(_("Website Image should be a public file or website URL")) + + self.website_image = None + + def make_thumbnail(self): + """Make a thumbnail of `website_image`""" + import requests.exceptions + + if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): + self.thumbnail = None + + if self.website_image and not self.thumbnail: + file_doc = None + + try: + file_doc = frappe.get_doc("File", { + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }) + except frappe.DoesNotExistError: + pass + # cleanup + frappe.local.message_log.pop() + + except requests.exceptions.HTTPError: + frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) + self.website_image = None + + except requests.exceptions.SSLError: + frappe.msgprint( + _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) + self.website_image = None + + # for CSV import + if self.website_image and not file_doc: + try: + file_doc = frappe.get_doc({ + "doctype": "File", + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }).insert() + + except IOError: + self.website_image = None + + if file_doc: + if not file_doc.thumbnail_url: + file_doc.make_thumbnail() + + self.thumbnail = file_doc.thumbnail_url + + def validate_fixed_asset(self): + if self.is_fixed_asset: + if self.is_stock_item: + frappe.throw(_("Fixed Asset Item must be a non-stock item.")) + + if not self.asset_category: + frappe.throw(_("Asset Category is mandatory for Fixed Asset item")) + + if self.stock_ledger_created(): + frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created.")) + + if not self.is_fixed_asset: + asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) + if asset: + frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) + + def validate_retain_sample(self): + if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): + frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) + if self.retain_sample and not self.has_batch_no: + frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( + self.item_code)) + + def get_context(self, context): + context.show_search = True + context.search_link = '/product_search' + + context.parents = get_parent_item_groups(self.item_group) + + self.set_variant_context(context) + self.set_attribute_context(context) + self.set_disabled_attributes(context) + + return context + + def set_variant_context(self, context): + if self.has_variants: + context.no_cache = True + + # load variants + # also used in set_attribute_context + context.variants = frappe.get_all("Item", + filters={"variant_of": self.name, "show_variant_in_website": 1}, + order_by="name asc") + + variant = frappe.form_dict.variant + if not variant and context.variants: + # the case when the item is opened for the first time from its list + variant = context.variants[0] + + if variant: + context.variant = frappe.get_doc("Item", variant) + + for fieldname in ("website_image", "web_long_description", "description", + "website_specifications"): + if context.variant.get(fieldname): + value = context.variant.get(fieldname) + if isinstance(value, list): + value = [d.as_dict() for d in value] + + context[fieldname] = value + + if self.slideshow: + if context.variant and context.variant.slideshow: + context.update(get_slideshow(context.variant)) + else: + context.update(get_slideshow(self)) + + def set_attribute_context(self, context): + if self.has_variants: + attribute_values_available = {} + context.attribute_values = {} + context.selected_attributes = {} + + # load attributes + for v in context.variants: + v.attributes = frappe.get_all("Item Variant Attribute", + fields=["attribute", "attribute_value"], filters={"parent": v.name}) + + for attr in v.attributes: + values = attribute_values_available.setdefault(attr.attribute, []) + if attr.attribute_value not in values: + values.append(attr.attribute_value) + + if v.name == context.variant.name: + context.selected_attributes[attr.attribute] = attr.attribute_value + + # filter attributes, order based on attribute table + for attr in self.attributes: + values = context.attribute_values.setdefault(attr.attribute, []) + + if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): + for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): + values.append(val) + + else: + # get list of values defined (for sequence) + for attr_value in frappe.db.get_all("Item Attribute Value", + fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): + + if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): + values.append(attr_value.attribute_value) + + context.variant_info = json.dumps(context.variants) + + def set_disabled_attributes(self, context): + """Disable selection options of attribute combinations that do not result in a variant""" + if not self.attributes or not self.has_variants: + return + + context.disabled_attributes = {} + attributes = [attr.attribute for attr in self.attributes] + + def find_variant(combination): + for variant in context.variants: + if len(variant.attributes) < len(attributes): + continue + + if "combination" not in variant: + ref_combination = [] + + for attr in variant.attributes: + idx = attributes.index(attr.attribute) + ref_combination.insert(idx, attr.attribute_value) + + variant["combination"] = ref_combination + + if not (set(combination) - set(variant["combination"])): + # check if the combination is a subset of a variant combination + # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] + return True + + for i, attr in enumerate(self.attributes): + if i == 0: + continue + + combination_source = [] + + # loop through previous attributes + for prev_attr in self.attributes[:i]: + combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) + + combination_source.append(context.attribute_values[attr.attribute]) + + for combination in itertools.product(*combination_source): + if not find_variant(combination): + context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) + + def add_default_uom_in_conversion_factor_table(self): + uom_conv_list = [d.uom for d in self.get("uoms")] + if self.stock_uom not in uom_conv_list: + ch = self.append('uoms', {}) + ch.uom = self.stock_uom + ch.conversion_factor = 1 + + to_remove = [] + for d in self.get("uoms"): + if d.conversion_factor == 1 and d.uom != self.stock_uom: + to_remove.append(d) + + [self.remove(d) for d in to_remove] + + def update_template_tables(self): + template = frappe.get_doc("Item", self.variant_of) + + # add item taxes from template + for d in template.get("taxes"): + self.append("taxes", {"tax_type": d.tax_type, "tax_rate": d.tax_rate}) + + # copy re-order table if empty + if not self.get("reorder_levels"): + for d in template.get("reorder_levels"): + n = {} + for k in ("warehouse", "warehouse_reorder_level", + "warehouse_reorder_qty", "material_request_type"): + n[k] = d.get(k) + self.append("reorder_levels", n) + + def validate_conversion_factor(self): + check_list = [] + for d in self.get('uoms'): + if cstr(d.uom) in check_list: + frappe.throw( + _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) + else: + check_list.append(cstr(d.uom)) + + if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: + frappe.throw( + _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) + + def validate_item_type(self): + if self.has_serial_no == 1 and self.is_stock_item == 0: + msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) + + if self.has_serial_no == 0 and self.serial_no_series: + self.serial_no_series = None + + def check_for_active_boms(self): + if self.default_bom: + bom_item = frappe.db.get_value("BOM", self.default_bom, "item") + if bom_item not in (self.name, self.variant_of): + frappe.throw( + _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) + + def fill_customer_code(self): + """ Append all the customer codes and insert into "customer_code" field of item table """ + cust_code = [] + for d in self.get('customer_items'): + cust_code.append(d.ref_code) + self.customer_code = ','.join(cust_code) + + def check_item_tax(self): + """Check whether Tax Rate is not entered twice for same Tax Type""" + check_list = [] + for d in self.get('taxes'): + if d.tax_type: + account_type = frappe.db.get_value("Account", d.tax_type, "account_type") + + if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account']: + frappe.throw( + _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(d.idx)) + else: + if d.tax_type in check_list: + frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) + else: + check_list.append(d.tax_type) + + def validate_barcode(self): + if len(self.barcodes) > 0: + for barcode in self.barcodes: + duplicate = frappe.db.sql("""select name from `tabItem Barcode` where barcode = %s and parent != %s""", (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 = [] + for d in self.get("reorder_levels"): + if not d.warehouse_group: + d.warehouse_group = d.warehouse + if d.get("warehouse") and d.get("warehouse") not in warehouse: + warehouse += [d.get("warehouse")] + else: + frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") + .format(d.idx, d.warehouse), DuplicateReorderRows) + + if d.warehouse_reorder_level and not d.warehouse_reorder_qty: + frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) + + def stock_ledger_created(self): + if not hasattr(self, '_stock_ledger_created'): + self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` where item_code = %s limit 1""", self.name)) - return self._stock_ledger_created + return self._stock_ledger_created - def validate_name_with_item_group(self): - # causes problem with tree build - if frappe.db.exists("Item Group", self.name): - frappe.throw(_("An Item Group exists with same name, please change the item name or rename the item group")) + def validate_name_with_item_group(self): + # causes problem with tree build + if frappe.db.exists("Item Group", self.name): + frappe.throw( + _("An Item Group exists with same name, please change the item name or rename the item group")) - def update_item_price(self): - frappe.db.sql("""update `tabItem Price` set item_name=%s, + def update_item_price(self): + frappe.db.sql("""update `tabItem Price` set item_name=%s, item_description=%s, modified=NOW() where item_code=%s""", - (self.item_name, self.description, self.name)) + (self.item_name, self.description, self.name)) - def on_trash(self): - super(Item, self).on_trash() - frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code) - frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name) - for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}): - frappe.delete_doc("Item", variant_of.name) + def on_trash(self): + super(Item, self).on_trash() + frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code) + frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name) + for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}): + frappe.delete_doc("Item", variant_of.name) - def before_rename(self, old_name, new_name, merge=False): - if self.item_name==old_name: - frappe.db.set_value("Item", old_name, "item_name", new_name) + def before_rename(self, old_name, new_name, merge=False): + if self.item_name == old_name: + frappe.db.set_value("Item", old_name, "item_name", new_name) - if merge: - # Validate properties before merging - if not frappe.db.exists("Item", new_name): - frappe.throw(_("Item {0} does not exist").format(new_name)) + if merge: + # Validate properties before merging + if not frappe.db.exists("Item", new_name): + frappe.throw(_("Item {0} does not exist").format(new_name)) - field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] - new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] - if new_properties != [cstr(self.get(fld)) for fld in field_list]: - frappe.throw(_("To merge, following properties must be same for both items") - + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) + field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] + new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] + if new_properties != [cstr(self.get(fld)) for fld in field_list]: + frappe.throw(_("To merge, following properties must be same for both items") + + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) - def after_rename(self, old_name, new_name, merge): - if self.route: - invalidate_cache_for_item(self) - clear_cache(self.route) + def after_rename(self, old_name, new_name, merge): + if self.route: + invalidate_cache_for_item(self) + clear_cache(self.route) - frappe.db.set_value("Item", new_name, "item_code", new_name) + frappe.db.set_value("Item", new_name, "item_code", new_name) - if merge: - self.set_last_purchase_rate(new_name) - self.recalculate_bin_qty(new_name) + if merge: + self.set_last_purchase_rate(new_name) + self.recalculate_bin_qty(new_name) - for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): - for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` + for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): + for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` where ifnull(item_wise_tax_detail, '') != ''""".format(dt), as_dict=1): - item_wise_tax_detail = json.loads(d.item_wise_tax_detail) - if old_name in item_wise_tax_detail: - item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] - item_wise_tax_detail.pop(old_name) + item_wise_tax_detail = json.loads(d.item_wise_tax_detail) + if old_name in item_wise_tax_detail: + item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] + item_wise_tax_detail.pop(old_name) - frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + frappe.db.set_value(dt, d.name, "item_wise_tax_detail", + json.dumps(item_wise_tax_detail), update_modified=False) - def set_last_purchase_rate(self, new_name): - last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) - frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) + def set_last_purchase_rate(self, new_name): + last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) + frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) + def recalculate_bin_qty(self, new_name): + from erpnext.stock.stock_balance import repost_stock + frappe.db.auto_commit_on_many_writes = 1 + existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse + repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse from tabBin where item_code=%s""", new_name) - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) + # Delete all existing bins to avoid duplicate bins for the same item and warehouse + frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) - for warehouse in repost_stock_for_warehouses: - repost_stock(new_name, warehouse) + for warehouse in repost_stock_for_warehouses: + repost_stock(new_name, warehouse) - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.auto_commit_on_many_writes = 0 - def copy_specification_from_item_group(self): - self.set("website_specifications", []) - if self.item_group: - for label, desc in frappe.db.get_values("Item Website Specification", - {"parent": self.item_group}, ["label", "description"]): - row = self.append("website_specifications") - row.label = label - row.description = desc + def copy_specification_from_item_group(self): + self.set("website_specifications", []) + if self.item_group: + for label, desc in frappe.db.get_values("Item Website Specification", + {"parent": self.item_group}, ["label", "description"]): + row = self.append("website_specifications") + row.label = label + row.description = desc - def update_bom_item_desc(self): - if self.is_new(): return + def update_bom_item_desc(self): + if self.is_new(): + return - if self.db_get('description') != self.description: - frappe.db.sql(""" + if self.db_get('description') != self.description: + frappe.db.sql(""" update `tabBOM` set description = %s where item = %s and docstatus < 2 """, (self.description, self.name)) - frappe.db.sql(""" + frappe.db.sql(""" update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2 """, (self.description, self.name)) - frappe.db.sql(""" + frappe.db.sql(""" update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2 """, (self.description, self.name)) - def update_template_item(self): - """Set Show in Website for Template Item if True for its Variant""" - if self.variant_of and self.show_in_website: - self.show_variant_in_website = 1 - self.show_in_website = 0 + def update_template_item(self): + """Set Show in Website for Template Item if True for its Variant""" + if self.variant_of and self.show_in_website: + self.show_variant_in_website = 1 + self.show_in_website = 0 - if self.show_variant_in_website: - # show template - template_item = frappe.get_doc("Item", self.variant_of) + if self.show_variant_in_website: + # show template + template_item = frappe.get_doc("Item", self.variant_of) - if not template_item.show_in_website: - template_item.show_in_website = 1 - template_item.flags.dont_update_variants = True - template_item.flags.ignore_permissions = True - template_item.save() + if not template_item.show_in_website: + template_item.show_in_website = 1 + template_item.flags.dont_update_variants = True + template_item.flags.ignore_permissions = True + template_item.save() - def update_variants(self): - if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): - return - if self.has_variants: - updated = [] - variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name }) - for d in variants: - variant = frappe.get_doc("Item", d) - copy_attributes_to_variant(self, variant) - variant.save() - updated.append(d.item_code) - if updated: - frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) + def update_variants(self): + if self.flags.dont_update_variants or \ + frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + return + if self.has_variants: + updated = [] + variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) + for d in variants: + variant = frappe.get_doc("Item", d) + copy_attributes_to_variant(self, variant) + variant.save() + updated.append(d.item_code) + if updated: + frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) - def validate_has_variants(self): - if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): - if frappe.db.exists("Item", {"variant_of": self.name}): - frappe.throw(_("Item has variants.")) + def validate_has_variants(self): + if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): + if frappe.db.exists("Item", {"variant_of": self.name}): + frappe.throw(_("Item has variants.")) - def validate_stock_exists_for_template_item(self): - if self.stock_ledger_created() and self._doc_before_save: - if (self._doc_before_save.has_variants != self.has_variants - or self._doc_before_save.variant_of != self.variant_of): - frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + def validate_stock_exists_for_template_item(self): + if self.stock_ledger_created() and self._doc_before_save: + if (self._doc_before_save.has_variants != self.has_variants + or self._doc_before_save.variant_of != self.variant_of): + frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name), + StockExistsForTemplate) - if self.has_variants or self.variant_of: - if not self.is_child_table_same('attributes'): - frappe.throw(_('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) + if self.has_variants or self.variant_of: + if not self.is_child_table_same('attributes'): + frappe.throw( + _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) - def validate_uom(self): - if not self.get("__islocal"): - check_stock_uom_with_bin(self.name, self.stock_uom) - if self.has_variants: - for d in frappe.db.get_all("Item", filters= {"variant_of": self.name}): - check_stock_uom_with_bin(d.name, self.stock_uom) - if self.variant_of: - template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") - if template_uom != self.stock_uom: - frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + def validate_uom(self): + if not self.get("__islocal"): + check_stock_uom_with_bin(self.name, self.stock_uom) + if self.has_variants: + for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): + check_stock_uom_with_bin(d.name, self.stock_uom) + if self.variant_of: + template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") + if template_uom != self.stock_uom: + frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") + .format(self.stock_uom, template_uom)) - def validate_attributes(self): - if (self.has_variants or self.variant_of) and self.variant_based_on=='Item Attribute': - attributes = [] - if not self.attributes: - frappe.throw(_("Attribute table is mandatory")) - for d in self.attributes: - if d.attribute in attributes: - frappe.throw(_("Attribute {0} selected multiple times in Attributes Table".format(d.attribute))) - else: - attributes.append(d.attribute) + def validate_attributes(self): + if (self.has_variants or self.variant_of) and self.variant_based_on == 'Item Attribute': + attributes = [] + if not self.attributes: + frappe.throw(_("Attribute table is mandatory")) + for d in self.attributes: + if d.attribute in attributes: + frappe.throw( + _("Attribute {0} selected multiple times in Attributes Table".format(d.attribute))) + else: + attributes.append(d.attribute) - def validate_variant_attributes(self): - if self.variant_of and self.variant_based_on=='Item Attribute': - args = {} - for d in self.attributes: - if not d.attribute_value: - frappe.throw(_("Please specify Attribute Value for attribute {0}").format(d.attribute)) - args[d.attribute] = d.attribute_value + def validate_variant_attributes(self): + if self.variant_of and self.variant_based_on == 'Item Attribute': + args = {} + for d in self.attributes: + if not d.attribute_value: + frappe.throw(_("Please specify Attribute Value for attribute {0}").format(d.attribute)) + args[d.attribute] = d.attribute_value - variant = get_variant(self.variant_of, args, self.name) - if variant: - frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + variant = get_variant(self.variant_of, args, self.name) + if variant: + frappe.throw(_("Item variant {0} exists with same attributes") + .format(variant), ItemVariantExistsError) + + validate_item_variant_attributes(self, args) - validate_item_variant_attributes(self, args) def get_timeline_data(doctype, name): - '''returns timeline data based on stock ledger entry''' - out = {} - items = dict(frappe.db.sql('''select posting_date, count(*) + '''returns timeline data based on stock ledger entry''' + out = {} + items = dict(frappe.db.sql('''select posting_date, count(*) from `tabStock Ledger Entry` where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) group by posting_date''', name)) - for date, count in items.iteritems(): - timestamp = get_timestamp(date) - out.update({ timestamp: count }) + for date, count in items.iteritems(): + timestamp = get_timestamp(date) + out.update({timestamp: count}) + + return out - return out def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1): - if (not end_of_life) or (disabled is None): - end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) + if (not end_of_life) or (disabled is None): + end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) - if end_of_life and end_of_life!="0000-00-00" and getdate(end_of_life) <= now_datetime().date(): - msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) - _msgprint(msg, verbose) + if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): + msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) + _msgprint(msg, verbose) + + if disabled: + _msgprint(_("Item {0} is disabled").format(item_code), verbose) - if disabled: - _msgprint(_("Item {0} is disabled").format(item_code), verbose) def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): - if not is_stock_item: - is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") + if not is_stock_item: + is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") - if is_stock_item != 1: - msg = _("Item {0} is not a stock Item").format(item_code) + if is_stock_item != 1: + msg = _("Item {0} is not a stock Item").format(item_code) + + _msgprint(msg, verbose) - _msgprint(msg, verbose) def validate_cancelled_item(item_code, docstatus=None, verbose=1): - if docstatus is None: - docstatus = frappe.db.get_value("Item", item_code, "docstatus") + if docstatus is None: + docstatus = frappe.db.get_value("Item", item_code, "docstatus") + + if docstatus == 2: + msg = _("Item {0} is cancelled").format(item_code) + _msgprint(msg, verbose) - if docstatus == 2: - msg = _("Item {0} is cancelled").format(item_code) - _msgprint(msg, verbose) def _msgprint(msg, verbose): - if verbose: - msgprint(msg, raise_exception=True) - else: - raise frappe.ValidationError(msg) + if verbose: + msgprint(msg, raise_exception=True) + else: + raise frappe.ValidationError(msg) def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): - """returns last purchase details in stock uom""" - # get last purchase order item details - last_purchase_order = frappe.db.sql("""\ + """returns last purchase details in stock uom""" + # get last purchase order item details + last_purchase_order = frappe.db.sql("""\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, po_item.discount_percentage, po_item.base_rate @@ -741,8 +775,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by po.transaction_date desc, po.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql("""\ + # get last purchase receipt item details + last_purchase_receipt = frappe.db.sql("""\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, pr_item.base_rate @@ -752,76 +786,79 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by pr.posting_date desc, pr.posting_time desc, pr.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date \ - or "1900-01-01") - purchase_receipt_date = getdate(last_purchase_receipt and \ - last_purchase_receipt[0].posting_date or "1900-01-01") + purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date + or "1900-01-01") + purchase_receipt_date = getdate(last_purchase_receipt and + last_purchase_receipt[0].posting_date or "1900-01-01") - if (purchase_order_date > purchase_receipt_date) or \ - (last_purchase_order and not last_purchase_receipt): - # use purchase order - last_purchase = last_purchase_order[0] - purchase_date = purchase_order_date + if (purchase_order_date > purchase_receipt_date) or \ + (last_purchase_order and not last_purchase_receipt): + # use purchase order + last_purchase = last_purchase_order[0] + purchase_date = purchase_order_date - elif (purchase_receipt_date > purchase_order_date) or \ - (last_purchase_receipt and not last_purchase_order): - # use purchase receipt - last_purchase = last_purchase_receipt[0] - purchase_date = purchase_receipt_date + elif (purchase_receipt_date > purchase_order_date) or \ + (last_purchase_receipt and not last_purchase_order): + # use purchase receipt + last_purchase = last_purchase_receipt[0] + purchase_date = purchase_receipt_date - else: - return frappe._dict() + else: + return frappe._dict() - conversion_factor = flt(last_purchase.conversion_factor) - out = frappe._dict({ - "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, - "base_rate": flt(last_purchase.base_rate) / conversion_factor, - "discount_percentage": flt(last_purchase.discount_percentage), - "purchase_date": purchase_date - }) + conversion_factor = flt(last_purchase.conversion_factor) + out = frappe._dict({ + "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, + "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "discount_percentage": flt(last_purchase.discount_percentage), + "purchase_date": purchase_date + }) - conversion_rate = flt(conversion_rate) or 1.0 - out.update({ - "price_list_rate": out.base_price_list_rate / conversion_rate, - "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate - }) + conversion_rate = flt(conversion_rate) or 1.0 + out.update({ + "price_list_rate": out.base_price_list_rate / conversion_rate, + "rate": out.base_rate / conversion_rate, + "base_rate": out.base_rate + }) + + return out - return out def invalidate_cache_for_item(doc): - invalidate_cache_for(doc, doc.item_group) + invalidate_cache_for(doc, doc.item_group) - website_item_groups = list(set((doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype":"Website Item Group"}) if d.item_group])) + website_item_groups = list(set((doc.get("old_website_item_groups") or []) + + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) - for item_group in website_item_groups: - invalidate_cache_for(doc, item_group) + for item_group in website_item_groups: + invalidate_cache_for(doc, item_group) + + if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: + invalidate_cache_for(doc, doc.old_item_group) - if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: - invalidate_cache_for(doc, doc.old_item_group) def check_stock_uom_with_bin(item, stock_uom): - if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): - return + if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): + return - matched=True - ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + matched = True + ref_uom = frappe.db.get_value("Stock Ledger Entry", + {"item_code": item}, "stock_uom") - if ref_uom: - if cstr(ref_uom) != cstr(stock_uom): - matched = False - else: - bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) - for bin in bin_list: - if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 \ - or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): - matched = False - break + if ref_uom: + if cstr(ref_uom) != cstr(stock_uom): + matched = False + else: + bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) + for bin in bin_list: + if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 + or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): + matched = False + break - if matched and bin_list: - frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) + if matched and bin_list: + frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) - if not matched: - frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) + if not matched: + frappe.throw( + _("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) From a04c756913c1987b1eb2fa730f42bc37dbc8d083 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 18:54:08 +0100 Subject: [PATCH 03/22] - validate EAN or UPC-A code - made Item Barcode Doctpye standard - added stdnum to requirements --- erpnext/stock/doctype/item/item.py | 12 ++- .../stock/doctype/item_barcode/__init__.py | 0 .../doctype/item_barcode/item_barcode.json | 102 ++++++++++++++++++ .../doctype/item_barcode/item_barcode.py | 10 ++ requirements.txt | 1 + 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 erpnext/stock/doctype/item_barcode/__init__.py create mode 100644 erpnext/stock/doctype/item_barcode/item_barcode.json create mode 100644 erpnext/stock/doctype/item_barcode/item_barcode.py diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8871ba385c..8ccca7aa88 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -483,12 +483,18 @@ class Item(WebsiteGenerator): check_list.append(d.tax_type) def validate_barcode(self): + from stdnum import ean if len(self.barcodes) > 0: - for barcode in self.barcodes: - duplicate = frappe.db.sql("""select name from `tabItem Barcode` where barcode = %s and parent != %s""", (barcode, self.name)) + for item_barcode in self.barcodes: + duplicate = frappe.db.sql("""select name from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) if duplicate: frappe.throw(_("Barcode {0} already used in Item {1}").format( - self.barcode, duplicate[0][0])) + item_barcode.barcode, duplicate[0][0])) + + if item_barcode.barcode_type: + if not ean.is_valid(item_barcode.barcode): + frappe.throw(_("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type)) def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' diff --git a/erpnext/stock/doctype/item_barcode/__init__.py b/erpnext/stock/doctype/item_barcode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json new file mode 100644 index 0000000000..76cd061a2b --- /dev/null +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -0,0 +1,102 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-12-09 18:54:50.562438", + "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": "barcode", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Barcode", + "length": 0, + "no_copy": 1, + "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": 1 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "barcode_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Barcode Type", + "length": 0, + "no_copy": 0, + "options": "\nEAN\nUPC-A", + "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": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-12-10 18:39:10.566172", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Barcode", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "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/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py new file mode 100644 index 0000000000..29d47f9968 --- /dev/null +++ b/erpnext/stock/doctype/item_barcode/item_barcode.py @@ -0,0 +1,10 @@ +# -*- 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 ItemBarcode(Document): + pass diff --git a/requirements.txt b/requirements.txt index a6cfaf2ce4..7b5d8da616 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ frappe unidecode pygithub googlemaps +python-stdnum From 01953135c55e50833effa78e3805d5f093371d6a Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 18:54:19 +0100 Subject: [PATCH 04/22] - code clean --- erpnext/stock/doctype/item/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8ccca7aa88..a6af44bd73 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -486,7 +486,7 @@ class Item(WebsiteGenerator): from stdnum import ean if len(self.barcodes) > 0: for item_barcode in self.barcodes: - duplicate = frappe.db.sql("""select name from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) if duplicate: frappe.throw(_("Barcode {0} already used in Item {1}").format( item_barcode.barcode, duplicate[0][0])) From ff6d0a526068b4f9565a1fda5865eb1573df3835 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 19:36:42 +0100 Subject: [PATCH 05/22] - clean code - console logs --- erpnext/public/js/controllers/transaction.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ed81ff71a6..3492ad954d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -273,7 +273,6 @@ 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); }, @@ -282,7 +281,6 @@ 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; @@ -296,7 +294,6 @@ 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(); From 8f8a485e074c5cfa6a0d0da32f2aff69a5e13ed2 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 19:46:01 +0100 Subject: [PATCH 06/22] - autoname field:barcode --- erpnext/stock/doctype/item_barcode/item_barcode.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index 76cd061a2b..e592981160 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -3,6 +3,7 @@ "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, + "autoname": "field:barcode", "beta": 0, "creation": "2017-12-09 18:54:50.562438", "custom": 0, @@ -84,7 +85,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-12-10 18:39:10.566172", + "modified": "2017-12-10 19:45:08.634821", "modified_by": "Administrator", "module": "Stock", "name": "Item Barcode", From 4c848ee14f51e8666ad4abe9fdcb7186e8461730 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 20:06:56 +0100 Subject: [PATCH 07/22] - codacy fix --- erpnext/stock/doctype/item_barcode/item_barcode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py index 29d47f9968..e85f93b71e 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.py +++ b/erpnext/stock/doctype/item_barcode/item_barcode.py @@ -3,8 +3,9 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe + from frappe.model.document import Document + class ItemBarcode(Document): - pass + pass From 2a098ada7c9a61ffe2b74ac13990eb12f1c0587e Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 20:33:20 +0100 Subject: [PATCH 08/22] - bug fix --- erpnext/accounts/doctype/sales_invoice/pos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index 4be16ba2b3..e37dbbf11c 100644 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -163,7 +163,7 @@ def get_items_list(pos_profile): 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, - default_warehouse, is_stock_item, barcode, brand + default_warehouse, is_stock_item, brand from tabItem where From ff7d73e418cd5ae29fb6f93e3e0e20ba12c900b1 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 20:46:46 +0100 Subject: [PATCH 09/22] - query fix --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index d2df30d84d..93d6e427e6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -166,7 +166,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals or tabItem.item_group LIKE %(txt)s or tabItem.item_name LIKE %(txt)s or tabItem.description LIKE %(txt)s) - or tabItem.name IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s) + or tabItem.item_code 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), From 43372d3333ab80d8348b4d375cbbb7dd6e74a149 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 10 Dec 2017 20:56:48 +0100 Subject: [PATCH 10/22] - make barcode mandatory if child row is added - check valid barcode only if exists --- erpnext/stock/doctype/item/item.py | 17 +++++++++-------- .../doctype/item_barcode/item_barcode.json | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index a6af44bd73..683679303b 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -486,15 +486,16 @@ class Item(WebsiteGenerator): from stdnum import ean if len(self.barcodes) > 0: for item_barcode in self.barcodes: - duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) - if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format( - item_barcode.barcode, duplicate[0][0])) + if item_barcode.barcode: + duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + if duplicate: + frappe.throw(_("Barcode {0} already used in Item {1}").format( + item_barcode.barcode, duplicate[0][0])) - if item_barcode.barcode_type: - if not ean.is_valid(item_barcode.barcode): - frappe.throw(_("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type)) + if item_barcode.barcode_type: + if not ean.is_valid(item_barcode.barcode): + frappe.throw(_("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type)) def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index e592981160..c8a3a897b3 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -38,7 +38,7 @@ "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 1 @@ -85,7 +85,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-12-10 19:45:08.634821", + "modified": "2017-12-10 20:55:23.814039", "modified_by": "Administrator", "module": "Stock", "name": "Item Barcode", From dd37ceb70758e07e2591273a9c933e20ef4b4cc3 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Mon, 11 Dec 2017 09:54:18 +0100 Subject: [PATCH 11/22] patch to migrade barcodes into Item Barcode --- erpnext/patches.txt | 1 + .../v10_0/item_barcode_childtable_migrate.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v10_0/item_barcode_childtable_migrate.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fe5ceda8ad..772ae55a78 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -479,3 +479,4 @@ erpnext.patches.v9_2.rename_net_weight_in_item_master erpnext.patches.v9_2.delete_process_payroll erpnext.patches.v10_0.add_agriculture_domain erpnext.patches.v10_0.add_non_profit_domain +erpnext.patches.v10_0item_barcode_childtable_migrate diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py new file mode 100644 index 0000000000..d36012ddd1 --- /dev/null +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -0,0 +1,15 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + + +def execute(): + items_barcode = frappe.db.sql("SELECT name, barcode FROM tabItem WHERE barcode IS NOT NULL", as_dict=1) + + for item in items_barcode: + doc = frappe.get_doc("Item", item.name) + doc.append("barcodes", {"barcode": item.get("barcode")}) + doc.save() From da40999ed858246fa799f889138e183fd54cd867 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Mon, 11 Dec 2017 10:01:45 +0100 Subject: [PATCH 12/22] - fix patch --- erpnext/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 772ae55a78..25a4c88259 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -479,4 +479,4 @@ erpnext.patches.v9_2.rename_net_weight_in_item_master erpnext.patches.v9_2.delete_process_payroll erpnext.patches.v10_0.add_agriculture_domain erpnext.patches.v10_0.add_non_profit_domain -erpnext.patches.v10_0item_barcode_childtable_migrate +erpnext.patches.v10_0.item_barcode_childtable_migrate From 91166662887aaa5c13002137dffdd803631f145f Mon Sep 17 00:00:00 2001 From: joezsweet Date: Wed, 10 Jan 2018 23:11:13 +0100 Subject: [PATCH 13/22] fix patch --- erpnext/patches/v10_0/item_barcode_childtable_migrate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index d36012ddd1..4194c873e6 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -10,6 +10,7 @@ def execute(): items_barcode = frappe.db.sql("SELECT name, barcode FROM tabItem WHERE barcode IS NOT NULL", as_dict=1) for item in items_barcode: - doc = frappe.get_doc("Item", item.name) - doc.append("barcodes", {"barcode": item.get("barcode")}) - doc.save() + doc = frappe.get_doc("Item", item.get("name")) + if item.get("barcode"): + doc.append("barcodes", {"barcode": item.get("barcode")}) + doc.save() From b6fb8d214317c35a2c4b03b935c7c7cf6ad1a9d5 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Thu, 11 Jan 2018 08:44:59 +0100 Subject: [PATCH 14/22] fix patch, reload doc --- erpnext/patches/v10_0/item_barcode_childtable_migrate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index 4194c873e6..47aca4ce6f 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -8,7 +8,8 @@ import frappe def execute(): items_barcode = frappe.db.sql("SELECT name, barcode FROM tabItem WHERE barcode IS NOT NULL", as_dict=1) - + frappe.reload_doc("stock", "doctype", "item") + for item in items_barcode: doc = frappe.get_doc("Item", item.get("name")) if item.get("barcode"): From 1be684fd87becdd0362cc2ca2a3bcbb3c82cf81f Mon Sep 17 00:00:00 2001 From: joezsweet Date: Thu, 11 Jan 2018 09:03:26 +0100 Subject: [PATCH 15/22] codici fix --- erpnext/patches/v10_0/item_barcode_childtable_migrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index 47aca4ce6f..e4547aa8d0 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -9,7 +9,6 @@ import frappe def execute(): items_barcode = frappe.db.sql("SELECT name, barcode FROM tabItem WHERE barcode IS NOT NULL", as_dict=1) frappe.reload_doc("stock", "doctype", "item") - for item in items_barcode: doc = frappe.get_doc("Item", item.get("name")) if item.get("barcode"): From 3aaed03dfba7f044b3b5e99fec5161c1e5121c2a Mon Sep 17 00:00:00 2001 From: joezsweet Date: Thu, 11 Jan 2018 11:31:31 +0100 Subject: [PATCH 16/22] reload item_barcode --- erpnext/patches/v10_0/item_barcode_childtable_migrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index e4547aa8d0..2e25336b76 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -9,6 +9,7 @@ import frappe def execute(): items_barcode = frappe.db.sql("SELECT name, barcode FROM tabItem WHERE barcode IS NOT NULL", as_dict=1) frappe.reload_doc("stock", "doctype", "item") + frappe.reload_doc("stock", "doctype", "item_barcode") for item in items_barcode: doc = frappe.get_doc("Item", item.get("name")) if item.get("barcode"): From 2f7ab065fb11c6f2a00e31901439997796c96914 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 5 Feb 2018 11:54:32 +0100 Subject: [PATCH 17/22] - removed barcodes from force_item_fields --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1401d37143..f2259d56a3 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", "barcodes", "brand", "stock_uom") +force_item_fields = ("item_group", "brand", "stock_uom") class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): From 5f473611bd6ed57703716244a054d3fb5ba9cd23 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 5 Feb 2018 12:05:11 +0100 Subject: [PATCH 18/22] space to tab --- erpnext/stock/doctype/item/item.py | 1420 ++++++++++++++-------------- 1 file changed, 710 insertions(+), 710 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 683679303b..6d4f850692 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -9,770 +9,770 @@ import json import erpnext import frappe from erpnext.controllers.item_variant import (ItemVariantExistsError, - copy_attributes_to_variant, - get_variant, - make_variant_item_code, - validate_item_variant_attributes) + copy_attributes_to_variant, + get_variant, + make_variant_item_code, + validate_item_variant_attributes) from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, - invalidate_cache_for) + invalidate_cache_for) from frappe import _, msgprint from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, - now_datetime, random_string, strip) + now_datetime, random_string, strip) from frappe.utils.html_utils import clean_html from frappe.website.doctype.website_slideshow.website_slideshow import \ - get_slideshow + get_slideshow from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator class DuplicateReorderRows(frappe.ValidationError): - pass + pass class StockExistsForTemplate(frappe.ValidationError): - pass + pass class Item(WebsiteGenerator): - website = frappe._dict( - page_title_field="item_name", - condition_field="show_in_website", - template="templates/generators/item.html", - no_cache=1 - ) + website = frappe._dict( + page_title_field="item_name", + condition_field="show_in_website", + template="templates/generators/item.html", + no_cache=1 + ) - def onload(self): - super(Item, self).onload() + def onload(self): + super(Item, self).onload() - self.set_onload('stock_exists', self.stock_ledger_created()) - if self.is_fixed_asset: - asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) - self.set_onload("asset_exists", True if asset else False) + self.set_onload('stock_exists', self.stock_ledger_created()) + if self.is_fixed_asset: + asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) + self.set_onload("asset_exists", True if asset else False) - def autoname(self): - if frappe.db.get_default("item_naming_by") == "Naming Series": - if self.variant_of: - if not self.item_code: - template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name") - self.item_code = make_variant_item_code(self.variant_of, template_item_name, self) - else: - from frappe.model.naming import make_autoname - self.item_code = make_autoname(self.naming_series + '.#####') - elif not self.item_code: - msgprint(_("Item Code is mandatory because Item is not automatically numbered"), raise_exception=1) + def autoname(self): + if frappe.db.get_default("item_naming_by") == "Naming Series": + if self.variant_of: + if not self.item_code: + template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name") + self.item_code = make_variant_item_code(self.variant_of, template_item_name, self) + else: + from frappe.model.naming import make_autoname + self.item_code = make_autoname(self.naming_series + '.#####') + elif not self.item_code: + msgprint(_("Item Code is mandatory because Item is not automatically numbered"), raise_exception=1) - self.item_code = strip(self.item_code) - self.name = self.item_code + self.item_code = strip(self.item_code) + self.name = self.item_code - def before_insert(self): - if not self.description: - self.description = self.item_name + def before_insert(self): + if not self.description: + self.description = self.item_name - if self.is_sales_item and not self.get('is_item_from_hub'): - self.publish_in_hub = 1 + if self.is_sales_item and not self.get('is_item_from_hub'): + self.publish_in_hub = 1 - def after_insert(self): - '''set opening stock and item price''' - if self.standard_rate: - self.add_price() + def after_insert(self): + '''set opening stock and item price''' + if self.standard_rate: + self.add_price() - if self.opening_stock: - self.set_opening_stock() + if self.opening_stock: + self.set_opening_stock() - def validate(self): - self.get_doc_before_save() + def validate(self): + self.get_doc_before_save() - super(Item, self).validate() + super(Item, self).validate() - if not self.item_name: - self.item_name = self.item_code + if not self.item_name: + self.item_name = self.item_code - if not self.description: - self.description = self.item_name + if not self.description: + self.description = self.item_name - self.validate_uom() - self.validate_description() - self.add_default_uom_in_conversion_factor_table() - self.validate_conversion_factor() - self.validate_item_type() - self.check_for_active_boms() - self.fill_customer_code() - self.check_item_tax() - self.validate_barcode() - self.validate_warehouse_for_reorder() - self.update_bom_item_desc() - self.synced_with_hub = 0 + self.validate_uom() + self.validate_description() + self.add_default_uom_in_conversion_factor_table() + self.validate_conversion_factor() + self.validate_item_type() + self.check_for_active_boms() + self.fill_customer_code() + self.check_item_tax() + self.validate_barcode() + self.validate_warehouse_for_reorder() + self.update_bom_item_desc() + self.synced_with_hub = 0 - self.validate_has_variants() - self.validate_stock_exists_for_template_item() - self.validate_attributes() - self.validate_variant_attributes() - self.validate_website_image() - self.make_thumbnail() - self.validate_fixed_asset() - self.validate_retain_sample() + self.validate_has_variants() + self.validate_stock_exists_for_template_item() + self.validate_attributes() + self.validate_variant_attributes() + self.validate_website_image() + self.make_thumbnail() + self.validate_fixed_asset() + self.validate_retain_sample() - if not self.get("__islocal"): - self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") - self.old_website_item_groups = frappe.db.sql_list("""select item_group + if not self.get("__islocal"): + self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") + self.old_website_item_groups = frappe.db.sql_list("""select item_group from `tabWebsite Item Group` where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) - def on_update(self): - invalidate_cache_for_item(self) - self.validate_name_with_item_group() - self.update_variants() - self.update_item_price() - self.update_template_item() - - def validate_description(self): - '''Clean HTML description if set''' - if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): - self.description = clean_html(self.description) - - def add_price(self, price_list=None): - '''Add a new price''' - if not price_list: - price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) - if price_list: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list, - "item_code": self.name, - "currency": erpnext.get_default_currency(), - "price_list_rate": self.standard_rate - }) - item_price.insert() - - def set_opening_stock(self): - '''set opening stock''' - if not self.is_stock_item or self.has_serial_no or self.has_batch_no: - return - - if not self.valuation_rate and self.standard_rate: - self.valuation_rate = self.standard_rate - - if not self.valuation_rate: - frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) - - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - # default warehouse, or Stores - default_warehouse = (self.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse') - or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) - - if default_warehouse: - stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, - qty=self.opening_stock, rate=self.valuation_rate) - - stock_entry.add_comment("Comment", _("Opening Stock")) - - def make_route(self): - if not self.route: - return cstr(frappe.db.get_value('Item Group', self.item_group, - 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) - - def validate_website_image(self): - """Validate if the website image is a public file""" - auto_set_website_image = False - if not self.website_image and self.image: - auto_set_website_image = True - self.website_image = self.image - - if not self.website_image: - return - - # find if website image url exists as public - file_doc = frappe.get_all("File", filters={ - "file_url": self.website_image - }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) - - if file_doc: - file_doc = file_doc[0] - - if not file_doc: - if not auto_set_website_image: - frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") - .format(self.website_image, self.name)) - - self.website_image = None - - elif file_doc.is_private: - if not auto_set_website_image: - frappe.msgprint(_("Website Image should be a public file or website URL")) - - self.website_image = None - - def make_thumbnail(self): - """Make a thumbnail of `website_image`""" - import requests.exceptions - - if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): - self.thumbnail = None - - if self.website_image and not self.thumbnail: - file_doc = None - - try: - file_doc = frappe.get_doc("File", { - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }) - except frappe.DoesNotExistError: - pass - # cleanup - frappe.local.message_log.pop() - - except requests.exceptions.HTTPError: - frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) - self.website_image = None - - except requests.exceptions.SSLError: - frappe.msgprint( - _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) - self.website_image = None - - # for CSV import - if self.website_image and not file_doc: - try: - file_doc = frappe.get_doc({ - "doctype": "File", - "file_url": self.website_image, - "attached_to_doctype": "Item", - "attached_to_name": self.name - }).insert() - - except IOError: - self.website_image = None - - if file_doc: - if not file_doc.thumbnail_url: - file_doc.make_thumbnail() - - self.thumbnail = file_doc.thumbnail_url - - def validate_fixed_asset(self): - if self.is_fixed_asset: - if self.is_stock_item: - frappe.throw(_("Fixed Asset Item must be a non-stock item.")) - - if not self.asset_category: - frappe.throw(_("Asset Category is mandatory for Fixed Asset item")) - - if self.stock_ledger_created(): - frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created.")) - - if not self.is_fixed_asset: - asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) - if asset: - frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) - - def validate_retain_sample(self): - if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): - frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) - if self.retain_sample and not self.has_batch_no: - frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( - self.item_code)) - - def get_context(self, context): - context.show_search = True - context.search_link = '/product_search' - - context.parents = get_parent_item_groups(self.item_group) - - self.set_variant_context(context) - self.set_attribute_context(context) - self.set_disabled_attributes(context) - - return context - - def set_variant_context(self, context): - if self.has_variants: - context.no_cache = True - - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all("Item", - filters={"variant_of": self.name, "show_variant_in_website": 1}, - order_by="name asc") - - variant = frappe.form_dict.variant - if not variant and context.variants: - # the case when the item is opened for the first time from its list - variant = context.variants[0] - - if variant: - context.variant = frappe.get_doc("Item", variant) - - for fieldname in ("website_image", "web_long_description", "description", - "website_specifications"): - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] - - context[fieldname] = value - - if self.slideshow: - if context.variant and context.variant.slideshow: - context.update(get_slideshow(context.variant)) - else: - context.update(get_slideshow(self)) - - def set_attribute_context(self, context): - if self.has_variants: - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} - - # load attributes - for v in context.variants: - v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], filters={"parent": v.name}) - - for attr in v.attributes: - values = attribute_values_available.setdefault(attr.attribute, []) - if attr.attribute_value not in values: - values.append(attr.attribute_value) - - if v.name == context.variant.name: - context.selected_attributes[attr.attribute] = attr.attribute_value - - # filter attributes, order based on attribute table - for attr in self.attributes: - values = context.attribute_values.setdefault(attr.attribute, []) - - if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): - for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): - values.append(val) - - else: - # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): - - if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): - values.append(attr_value.attribute_value) - - context.variant_info = json.dumps(context.variants) - - def set_disabled_attributes(self, context): - """Disable selection options of attribute combinations that do not result in a variant""" - if not self.attributes or not self.has_variants: - return - - context.disabled_attributes = {} - attributes = [attr.attribute for attr in self.attributes] - - def find_variant(combination): - for variant in context.variants: - if len(variant.attributes) < len(attributes): - continue - - if "combination" not in variant: - ref_combination = [] - - for attr in variant.attributes: - idx = attributes.index(attr.attribute) - ref_combination.insert(idx, attr.attribute_value) - - variant["combination"] = ref_combination - - if not (set(combination) - set(variant["combination"])): - # check if the combination is a subset of a variant combination - # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] - return True - - for i, attr in enumerate(self.attributes): - if i == 0: - continue - - combination_source = [] - - # loop through previous attributes - for prev_attr in self.attributes[:i]: - combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) - - combination_source.append(context.attribute_values[attr.attribute]) - - for combination in itertools.product(*combination_source): - if not find_variant(combination): - context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) - - def add_default_uom_in_conversion_factor_table(self): - uom_conv_list = [d.uom for d in self.get("uoms")] - if self.stock_uom not in uom_conv_list: - ch = self.append('uoms', {}) - ch.uom = self.stock_uom - ch.conversion_factor = 1 - - to_remove = [] - for d in self.get("uoms"): - if d.conversion_factor == 1 and d.uom != self.stock_uom: - to_remove.append(d) - - [self.remove(d) for d in to_remove] - - def update_template_tables(self): - template = frappe.get_doc("Item", self.variant_of) - - # add item taxes from template - for d in template.get("taxes"): - self.append("taxes", {"tax_type": d.tax_type, "tax_rate": d.tax_rate}) - - # copy re-order table if empty - if not self.get("reorder_levels"): - for d in template.get("reorder_levels"): - n = {} - for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): - n[k] = d.get(k) - self.append("reorder_levels", n) - - def validate_conversion_factor(self): - check_list = [] - for d in self.get('uoms'): - if cstr(d.uom) in check_list: - frappe.throw( - _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) - else: - check_list.append(cstr(d.uom)) - - if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: - frappe.throw( - _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) - - def validate_item_type(self): - if self.has_serial_no == 1 and self.is_stock_item == 0: - msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) - - if self.has_serial_no == 0 and self.serial_no_series: - self.serial_no_series = None - - def check_for_active_boms(self): - if self.default_bom: - bom_item = frappe.db.get_value("BOM", self.default_bom, "item") - if bom_item not in (self.name, self.variant_of): - frappe.throw( - _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) - - def fill_customer_code(self): - """ Append all the customer codes and insert into "customer_code" field of item table """ - cust_code = [] - for d in self.get('customer_items'): - cust_code.append(d.ref_code) - self.customer_code = ','.join(cust_code) - - def check_item_tax(self): - """Check whether Tax Rate is not entered twice for same Tax Type""" - check_list = [] - for d in self.get('taxes'): - if d.tax_type: - account_type = frappe.db.get_value("Account", d.tax_type, "account_type") - - if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account']: - frappe.throw( - _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(d.idx)) - else: - if d.tax_type in check_list: - frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) - else: - check_list.append(d.tax_type) - - def validate_barcode(self): - from stdnum import ean - if len(self.barcodes) > 0: - for item_barcode in self.barcodes: - if item_barcode.barcode: - duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) - if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format( - item_barcode.barcode, duplicate[0][0])) - - if item_barcode.barcode_type: - if not ean.is_valid(item_barcode.barcode): - frappe.throw(_("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type)) - - def validate_warehouse_for_reorder(self): - '''Validate Reorder level table for duplicate and conditional mandatory''' - warehouse = [] - for d in self.get("reorder_levels"): - if not d.warehouse_group: - d.warehouse_group = d.warehouse - if d.get("warehouse") and d.get("warehouse") not in warehouse: - warehouse += [d.get("warehouse")] - else: - frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) - - if d.warehouse_reorder_level and not d.warehouse_reorder_qty: - frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) - - def stock_ledger_created(self): - if not hasattr(self, '_stock_ledger_created'): - self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` + def on_update(self): + invalidate_cache_for_item(self) + self.validate_name_with_item_group() + self.update_variants() + self.update_item_price() + self.update_template_item() + + def validate_description(self): + '''Clean HTML description if set''' + if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): + self.description = clean_html(self.description) + + def add_price(self, price_list=None): + '''Add a new price''' + if not price_list: + price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') + or frappe.db.get_value('Price List', _('Standard Selling'))) + if price_list: + item_price = frappe.get_doc({ + "doctype": "Item Price", + "price_list": price_list, + "item_code": self.name, + "currency": erpnext.get_default_currency(), + "price_list_rate": self.standard_rate + }) + item_price.insert() + + def set_opening_stock(self): + '''set opening stock''' + if not self.is_stock_item or self.has_serial_no or self.has_batch_no: + return + + if not self.valuation_rate and self.standard_rate: + self.valuation_rate = self.standard_rate + + if not self.valuation_rate: + frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) + + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + # default warehouse, or Stores + default_warehouse = (self.default_warehouse + or frappe.db.get_single_value('Stock Settings', 'default_warehouse') + or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) + + if default_warehouse: + stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, + qty=self.opening_stock, rate=self.valuation_rate) + + stock_entry.add_comment("Comment", _("Opening Stock")) + + def make_route(self): + if not self.route: + return cstr(frappe.db.get_value('Item Group', self.item_group, + 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + + def validate_website_image(self): + """Validate if the website image is a public file""" + auto_set_website_image = False + if not self.website_image and self.image: + auto_set_website_image = True + self.website_image = self.image + + if not self.website_image: + return + + # find if website image url exists as public + file_doc = frappe.get_all("File", filters={ + "file_url": self.website_image + }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1) + + if file_doc: + file_doc = file_doc[0] + + if not file_doc: + if not auto_set_website_image: + frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") + .format(self.website_image, self.name)) + + self.website_image = None + + elif file_doc.is_private: + if not auto_set_website_image: + frappe.msgprint(_("Website Image should be a public file or website URL")) + + self.website_image = None + + def make_thumbnail(self): + """Make a thumbnail of `website_image`""" + import requests.exceptions + + if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): + self.thumbnail = None + + if self.website_image and not self.thumbnail: + file_doc = None + + try: + file_doc = frappe.get_doc("File", { + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }) + except frappe.DoesNotExistError: + pass + # cleanup + frappe.local.message_log.pop() + + except requests.exceptions.HTTPError: + frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image)) + self.website_image = None + + except requests.exceptions.SSLError: + frappe.msgprint( + _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) + self.website_image = None + + # for CSV import + if self.website_image and not file_doc: + try: + file_doc = frappe.get_doc({ + "doctype": "File", + "file_url": self.website_image, + "attached_to_doctype": "Item", + "attached_to_name": self.name + }).insert() + + except IOError: + self.website_image = None + + if file_doc: + if not file_doc.thumbnail_url: + file_doc.make_thumbnail() + + self.thumbnail = file_doc.thumbnail_url + + def validate_fixed_asset(self): + if self.is_fixed_asset: + if self.is_stock_item: + frappe.throw(_("Fixed Asset Item must be a non-stock item.")) + + if not self.asset_category: + frappe.throw(_("Asset Category is mandatory for Fixed Asset item")) + + if self.stock_ledger_created(): + frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created.")) + + if not self.is_fixed_asset: + asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) + if asset: + frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) + + def validate_retain_sample(self): + if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): + frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) + if self.retain_sample and not self.has_batch_no: + frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( + self.item_code)) + + def get_context(self, context): + context.show_search = True + context.search_link = '/product_search' + + context.parents = get_parent_item_groups(self.item_group) + + self.set_variant_context(context) + self.set_attribute_context(context) + self.set_disabled_attributes(context) + + return context + + def set_variant_context(self, context): + if self.has_variants: + context.no_cache = True + + # load variants + # also used in set_attribute_context + context.variants = frappe.get_all("Item", + filters={"variant_of": self.name, "show_variant_in_website": 1}, + order_by="name asc") + + variant = frappe.form_dict.variant + if not variant and context.variants: + # the case when the item is opened for the first time from its list + variant = context.variants[0] + + if variant: + context.variant = frappe.get_doc("Item", variant) + + for fieldname in ("website_image", "web_long_description", "description", + "website_specifications"): + if context.variant.get(fieldname): + value = context.variant.get(fieldname) + if isinstance(value, list): + value = [d.as_dict() for d in value] + + context[fieldname] = value + + if self.slideshow: + if context.variant and context.variant.slideshow: + context.update(get_slideshow(context.variant)) + else: + context.update(get_slideshow(self)) + + def set_attribute_context(self, context): + if self.has_variants: + attribute_values_available = {} + context.attribute_values = {} + context.selected_attributes = {} + + # load attributes + for v in context.variants: + v.attributes = frappe.get_all("Item Variant Attribute", + fields=["attribute", "attribute_value"], filters={"parent": v.name}) + + for attr in v.attributes: + values = attribute_values_available.setdefault(attr.attribute, []) + if attr.attribute_value not in values: + values.append(attr.attribute_value) + + if v.name == context.variant.name: + context.selected_attributes[attr.attribute] = attr.attribute_value + + # filter attributes, order based on attribute table + for attr in self.attributes: + values = context.attribute_values.setdefault(attr.attribute, []) + + if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")): + for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt): + values.append(val) + + else: + # get list of values defined (for sequence) + for attr_value in frappe.db.get_all("Item Attribute Value", + fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): + + if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): + values.append(attr_value.attribute_value) + + context.variant_info = json.dumps(context.variants) + + def set_disabled_attributes(self, context): + """Disable selection options of attribute combinations that do not result in a variant""" + if not self.attributes or not self.has_variants: + return + + context.disabled_attributes = {} + attributes = [attr.attribute for attr in self.attributes] + + def find_variant(combination): + for variant in context.variants: + if len(variant.attributes) < len(attributes): + continue + + if "combination" not in variant: + ref_combination = [] + + for attr in variant.attributes: + idx = attributes.index(attr.attribute) + ref_combination.insert(idx, attr.attribute_value) + + variant["combination"] = ref_combination + + if not (set(combination) - set(variant["combination"])): + # check if the combination is a subset of a variant combination + # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] + return True + + for i, attr in enumerate(self.attributes): + if i == 0: + continue + + combination_source = [] + + # loop through previous attributes + for prev_attr in self.attributes[:i]: + combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) + + combination_source.append(context.attribute_values[attr.attribute]) + + for combination in itertools.product(*combination_source): + if not find_variant(combination): + context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) + + def add_default_uom_in_conversion_factor_table(self): + uom_conv_list = [d.uom for d in self.get("uoms")] + if self.stock_uom not in uom_conv_list: + ch = self.append('uoms', {}) + ch.uom = self.stock_uom + ch.conversion_factor = 1 + + to_remove = [] + for d in self.get("uoms"): + if d.conversion_factor == 1 and d.uom != self.stock_uom: + to_remove.append(d) + + [self.remove(d) for d in to_remove] + + def update_template_tables(self): + template = frappe.get_doc("Item", self.variant_of) + + # add item taxes from template + for d in template.get("taxes"): + self.append("taxes", {"tax_type": d.tax_type, "tax_rate": d.tax_rate}) + + # copy re-order table if empty + if not self.get("reorder_levels"): + for d in template.get("reorder_levels"): + n = {} + for k in ("warehouse", "warehouse_reorder_level", + "warehouse_reorder_qty", "material_request_type"): + n[k] = d.get(k) + self.append("reorder_levels", n) + + def validate_conversion_factor(self): + check_list = [] + for d in self.get('uoms'): + if cstr(d.uom) in check_list: + frappe.throw( + _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) + else: + check_list.append(cstr(d.uom)) + + if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: + frappe.throw( + _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) + + def validate_item_type(self): + if self.has_serial_no == 1 and self.is_stock_item == 0: + msgprint(_("'Has Serial No' can not be 'Yes' for non-stock item"), raise_exception=1) + + if self.has_serial_no == 0 and self.serial_no_series: + self.serial_no_series = None + + def check_for_active_boms(self): + if self.default_bom: + bom_item = frappe.db.get_value("BOM", self.default_bom, "item") + if bom_item not in (self.name, self.variant_of): + frappe.throw( + _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) + + def fill_customer_code(self): + """ Append all the customer codes and insert into "customer_code" field of item table """ + cust_code = [] + for d in self.get('customer_items'): + cust_code.append(d.ref_code) + self.customer_code = ','.join(cust_code) + + def check_item_tax(self): + """Check whether Tax Rate is not entered twice for same Tax Type""" + check_list = [] + for d in self.get('taxes'): + if d.tax_type: + account_type = frappe.db.get_value("Account", d.tax_type, "account_type") + + if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account']: + frappe.throw( + _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(d.idx)) + else: + if d.tax_type in check_list: + frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) + else: + check_list.append(d.tax_type) + + def validate_barcode(self): + from stdnum import ean + if len(self.barcodes) > 0: + for item_barcode in self.barcodes: + if item_barcode.barcode: + duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + if duplicate: + frappe.throw(_("Barcode {0} already used in Item {1}").format( + item_barcode.barcode, duplicate[0][0])) + + if item_barcode.barcode_type: + if not ean.is_valid(item_barcode.barcode): + frappe.throw(_("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type)) + + def validate_warehouse_for_reorder(self): + '''Validate Reorder level table for duplicate and conditional mandatory''' + warehouse = [] + for d in self.get("reorder_levels"): + if not d.warehouse_group: + d.warehouse_group = d.warehouse + if d.get("warehouse") and d.get("warehouse") not in warehouse: + warehouse += [d.get("warehouse")] + else: + frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") + .format(d.idx, d.warehouse), DuplicateReorderRows) + + if d.warehouse_reorder_level and not d.warehouse_reorder_qty: + frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) + + def stock_ledger_created(self): + if not hasattr(self, '_stock_ledger_created'): + self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` where item_code = %s limit 1""", self.name)) - return self._stock_ledger_created + return self._stock_ledger_created - def validate_name_with_item_group(self): - # causes problem with tree build - if frappe.db.exists("Item Group", self.name): - frappe.throw( - _("An Item Group exists with same name, please change the item name or rename the item group")) + def validate_name_with_item_group(self): + # causes problem with tree build + if frappe.db.exists("Item Group", self.name): + frappe.throw( + _("An Item Group exists with same name, please change the item name or rename the item group")) - def update_item_price(self): - frappe.db.sql("""update `tabItem Price` set item_name=%s, + def update_item_price(self): + frappe.db.sql("""update `tabItem Price` set item_name=%s, item_description=%s, modified=NOW() where item_code=%s""", - (self.item_name, self.description, self.name)) + (self.item_name, self.description, self.name)) - def on_trash(self): - super(Item, self).on_trash() - frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code) - frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name) - for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}): - frappe.delete_doc("Item", variant_of.name) + def on_trash(self): + super(Item, self).on_trash() + frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code) + frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name) + for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}): + frappe.delete_doc("Item", variant_of.name) - def before_rename(self, old_name, new_name, merge=False): - if self.item_name == old_name: - frappe.db.set_value("Item", old_name, "item_name", new_name) + def before_rename(self, old_name, new_name, merge=False): + if self.item_name == old_name: + frappe.db.set_value("Item", old_name, "item_name", new_name) - if merge: - # Validate properties before merging - if not frappe.db.exists("Item", new_name): - frappe.throw(_("Item {0} does not exist").format(new_name)) + if merge: + # Validate properties before merging + if not frappe.db.exists("Item", new_name): + frappe.throw(_("Item {0} does not exist").format(new_name)) - field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] - new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] - if new_properties != [cstr(self.get(fld)) for fld in field_list]: - frappe.throw(_("To merge, following properties must be same for both items") - + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) + field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"] + new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] + if new_properties != [cstr(self.get(fld)) for fld in field_list]: + frappe.throw(_("To merge, following properties must be same for both items") + + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) - def after_rename(self, old_name, new_name, merge): - if self.route: - invalidate_cache_for_item(self) - clear_cache(self.route) + def after_rename(self, old_name, new_name, merge): + if self.route: + invalidate_cache_for_item(self) + clear_cache(self.route) - frappe.db.set_value("Item", new_name, "item_code", new_name) + frappe.db.set_value("Item", new_name, "item_code", new_name) - if merge: - self.set_last_purchase_rate(new_name) - self.recalculate_bin_qty(new_name) + if merge: + self.set_last_purchase_rate(new_name) + self.recalculate_bin_qty(new_name) - for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): - for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` + for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): + for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` where ifnull(item_wise_tax_detail, '') != ''""".format(dt), as_dict=1): - item_wise_tax_detail = json.loads(d.item_wise_tax_detail) - if old_name in item_wise_tax_detail: - item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] - item_wise_tax_detail.pop(old_name) + item_wise_tax_detail = json.loads(d.item_wise_tax_detail) + if old_name in item_wise_tax_detail: + item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] + item_wise_tax_detail.pop(old_name) - frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + frappe.db.set_value(dt, d.name, "item_wise_tax_detail", + json.dumps(item_wise_tax_detail), update_modified=False) - def set_last_purchase_rate(self, new_name): - last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) - frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) + def set_last_purchase_rate(self, new_name): + last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) + frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) + def recalculate_bin_qty(self, new_name): + from erpnext.stock.stock_balance import repost_stock + frappe.db.auto_commit_on_many_writes = 1 + existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse + repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse from tabBin where item_code=%s""", new_name) - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) + # Delete all existing bins to avoid duplicate bins for the same item and warehouse + frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) - for warehouse in repost_stock_for_warehouses: - repost_stock(new_name, warehouse) + for warehouse in repost_stock_for_warehouses: + repost_stock(new_name, warehouse) - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.auto_commit_on_many_writes = 0 - def copy_specification_from_item_group(self): - self.set("website_specifications", []) - if self.item_group: - for label, desc in frappe.db.get_values("Item Website Specification", - {"parent": self.item_group}, ["label", "description"]): - row = self.append("website_specifications") - row.label = label - row.description = desc + def copy_specification_from_item_group(self): + self.set("website_specifications", []) + if self.item_group: + for label, desc in frappe.db.get_values("Item Website Specification", + {"parent": self.item_group}, ["label", "description"]): + row = self.append("website_specifications") + row.label = label + row.description = desc - def update_bom_item_desc(self): - if self.is_new(): - return + def update_bom_item_desc(self): + if self.is_new(): + return - if self.db_get('description') != self.description: - frappe.db.sql(""" + if self.db_get('description') != self.description: + frappe.db.sql(""" update `tabBOM` set description = %s where item = %s and docstatus < 2 """, (self.description, self.name)) - frappe.db.sql(""" + frappe.db.sql(""" update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2 """, (self.description, self.name)) - frappe.db.sql(""" + frappe.db.sql(""" update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2 """, (self.description, self.name)) - def update_template_item(self): - """Set Show in Website for Template Item if True for its Variant""" - if self.variant_of and self.show_in_website: - self.show_variant_in_website = 1 - self.show_in_website = 0 + def update_template_item(self): + """Set Show in Website for Template Item if True for its Variant""" + if self.variant_of and self.show_in_website: + self.show_variant_in_website = 1 + self.show_in_website = 0 - if self.show_variant_in_website: - # show template - template_item = frappe.get_doc("Item", self.variant_of) + if self.show_variant_in_website: + # show template + template_item = frappe.get_doc("Item", self.variant_of) - if not template_item.show_in_website: - template_item.show_in_website = 1 - template_item.flags.dont_update_variants = True - template_item.flags.ignore_permissions = True - template_item.save() + if not template_item.show_in_website: + template_item.show_in_website = 1 + template_item.flags.dont_update_variants = True + template_item.flags.ignore_permissions = True + template_item.save() - def update_variants(self): - if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): - return - if self.has_variants: - updated = [] - variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) - for d in variants: - variant = frappe.get_doc("Item", d) - copy_attributes_to_variant(self, variant) - variant.save() - updated.append(d.item_code) - if updated: - frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) + def update_variants(self): + if self.flags.dont_update_variants or \ + frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + return + if self.has_variants: + updated = [] + variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) + for d in variants: + variant = frappe.get_doc("Item", d) + copy_attributes_to_variant(self, variant) + variant.save() + updated.append(d.item_code) + if updated: + frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated))) - def validate_has_variants(self): - if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): - if frappe.db.exists("Item", {"variant_of": self.name}): - frappe.throw(_("Item has variants.")) + def validate_has_variants(self): + if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): + if frappe.db.exists("Item", {"variant_of": self.name}): + frappe.throw(_("Item has variants.")) - def validate_stock_exists_for_template_item(self): - if self.stock_ledger_created() and self._doc_before_save: - if (self._doc_before_save.has_variants != self.has_variants - or self._doc_before_save.variant_of != self.variant_of): - frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + def validate_stock_exists_for_template_item(self): + if self.stock_ledger_created() and self._doc_before_save: + if (self._doc_before_save.has_variants != self.has_variants + or self._doc_before_save.variant_of != self.variant_of): + frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name), + StockExistsForTemplate) - if self.has_variants or self.variant_of: - if not self.is_child_table_same('attributes'): - frappe.throw( - _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) + if self.has_variants or self.variant_of: + if not self.is_child_table_same('attributes'): + frappe.throw( + _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) - def validate_uom(self): - if not self.get("__islocal"): - check_stock_uom_with_bin(self.name, self.stock_uom) - if self.has_variants: - for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): - check_stock_uom_with_bin(d.name, self.stock_uom) - if self.variant_of: - template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") - if template_uom != self.stock_uom: - frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + def validate_uom(self): + if not self.get("__islocal"): + check_stock_uom_with_bin(self.name, self.stock_uom) + if self.has_variants: + for d in frappe.db.get_all("Item", filters={"variant_of": self.name}): + check_stock_uom_with_bin(d.name, self.stock_uom) + if self.variant_of: + template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") + if template_uom != self.stock_uom: + frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") + .format(self.stock_uom, template_uom)) - def validate_attributes(self): - if (self.has_variants or self.variant_of) and self.variant_based_on == 'Item Attribute': - attributes = [] - if not self.attributes: - frappe.throw(_("Attribute table is mandatory")) - for d in self.attributes: - if d.attribute in attributes: - frappe.throw( - _("Attribute {0} selected multiple times in Attributes Table".format(d.attribute))) - else: - attributes.append(d.attribute) + def validate_attributes(self): + if (self.has_variants or self.variant_of) and self.variant_based_on == 'Item Attribute': + attributes = [] + if not self.attributes: + frappe.throw(_("Attribute table is mandatory")) + for d in self.attributes: + if d.attribute in attributes: + frappe.throw( + _("Attribute {0} selected multiple times in Attributes Table".format(d.attribute))) + else: + attributes.append(d.attribute) - def validate_variant_attributes(self): - if self.variant_of and self.variant_based_on == 'Item Attribute': - args = {} - for d in self.attributes: - if not d.attribute_value: - frappe.throw(_("Please specify Attribute Value for attribute {0}").format(d.attribute)) - args[d.attribute] = d.attribute_value + def validate_variant_attributes(self): + if self.variant_of and self.variant_based_on == 'Item Attribute': + args = {} + for d in self.attributes: + if not d.attribute_value: + frappe.throw(_("Please specify Attribute Value for attribute {0}").format(d.attribute)) + args[d.attribute] = d.attribute_value - variant = get_variant(self.variant_of, args, self.name) - if variant: - frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + variant = get_variant(self.variant_of, args, self.name) + if variant: + frappe.throw(_("Item variant {0} exists with same attributes") + .format(variant), ItemVariantExistsError) - validate_item_variant_attributes(self, args) + validate_item_variant_attributes(self, args) def get_timeline_data(doctype, name): - '''returns timeline data based on stock ledger entry''' - out = {} - items = dict(frappe.db.sql('''select posting_date, count(*) + '''returns timeline data based on stock ledger entry''' + out = {} + items = dict(frappe.db.sql('''select posting_date, count(*) from `tabStock Ledger Entry` where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) group by posting_date''', name)) - for date, count in items.iteritems(): - timestamp = get_timestamp(date) - out.update({timestamp: count}) + for date, count in items.iteritems(): + timestamp = get_timestamp(date) + out.update({timestamp: count}) - return out + return out def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1): - if (not end_of_life) or (disabled is None): - end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) + if (not end_of_life) or (disabled is None): + end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) - if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): - msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) - _msgprint(msg, verbose) + if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): + msg = _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) + _msgprint(msg, verbose) - if disabled: - _msgprint(_("Item {0} is disabled").format(item_code), verbose) + if disabled: + _msgprint(_("Item {0} is disabled").format(item_code), verbose) def validate_is_stock_item(item_code, is_stock_item=None, verbose=1): - if not is_stock_item: - is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") + if not is_stock_item: + is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") - if is_stock_item != 1: - msg = _("Item {0} is not a stock Item").format(item_code) + if is_stock_item != 1: + msg = _("Item {0} is not a stock Item").format(item_code) - _msgprint(msg, verbose) + _msgprint(msg, verbose) def validate_cancelled_item(item_code, docstatus=None, verbose=1): - if docstatus is None: - docstatus = frappe.db.get_value("Item", item_code, "docstatus") + if docstatus is None: + docstatus = frappe.db.get_value("Item", item_code, "docstatus") - if docstatus == 2: - msg = _("Item {0} is cancelled").format(item_code) - _msgprint(msg, verbose) + if docstatus == 2: + msg = _("Item {0} is cancelled").format(item_code) + _msgprint(msg, verbose) def _msgprint(msg, verbose): - if verbose: - msgprint(msg, raise_exception=True) - else: - raise frappe.ValidationError(msg) + if verbose: + msgprint(msg, raise_exception=True) + else: + raise frappe.ValidationError(msg) def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): - """returns last purchase details in stock uom""" - # get last purchase order item details - last_purchase_order = frappe.db.sql("""\ + """returns last purchase details in stock uom""" + # get last purchase order item details + last_purchase_order = frappe.db.sql("""\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, po_item.discount_percentage, po_item.base_rate @@ -782,8 +782,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by po.transaction_date desc, po.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql("""\ + # get last purchase receipt item details + last_purchase_receipt = frappe.db.sql("""\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, pr_item.base_rate @@ -793,79 +793,79 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by pr.posting_date desc, pr.posting_time desc, pr.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date - or "1900-01-01") - purchase_receipt_date = getdate(last_purchase_receipt and - last_purchase_receipt[0].posting_date or "1900-01-01") + purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date + or "1900-01-01") + purchase_receipt_date = getdate(last_purchase_receipt and + last_purchase_receipt[0].posting_date or "1900-01-01") - if (purchase_order_date > purchase_receipt_date) or \ - (last_purchase_order and not last_purchase_receipt): - # use purchase order - last_purchase = last_purchase_order[0] - purchase_date = purchase_order_date + if (purchase_order_date > purchase_receipt_date) or \ + (last_purchase_order and not last_purchase_receipt): + # use purchase order + last_purchase = last_purchase_order[0] + purchase_date = purchase_order_date - elif (purchase_receipt_date > purchase_order_date) or \ - (last_purchase_receipt and not last_purchase_order): - # use purchase receipt - last_purchase = last_purchase_receipt[0] - purchase_date = purchase_receipt_date + elif (purchase_receipt_date > purchase_order_date) or \ + (last_purchase_receipt and not last_purchase_order): + # use purchase receipt + last_purchase = last_purchase_receipt[0] + purchase_date = purchase_receipt_date - else: - return frappe._dict() + else: + return frappe._dict() - conversion_factor = flt(last_purchase.conversion_factor) - out = frappe._dict({ - "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, - "base_rate": flt(last_purchase.base_rate) / conversion_factor, - "discount_percentage": flt(last_purchase.discount_percentage), - "purchase_date": purchase_date - }) + conversion_factor = flt(last_purchase.conversion_factor) + out = frappe._dict({ + "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, + "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "discount_percentage": flt(last_purchase.discount_percentage), + "purchase_date": purchase_date + }) - conversion_rate = flt(conversion_rate) or 1.0 - out.update({ - "price_list_rate": out.base_price_list_rate / conversion_rate, - "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate - }) + conversion_rate = flt(conversion_rate) or 1.0 + out.update({ + "price_list_rate": out.base_price_list_rate / conversion_rate, + "rate": out.base_rate / conversion_rate, + "base_rate": out.base_rate + }) - return out + return out def invalidate_cache_for_item(doc): - invalidate_cache_for(doc, doc.item_group) + invalidate_cache_for(doc, doc.item_group) - website_item_groups = list(set((doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) + website_item_groups = list(set((doc.get("old_website_item_groups") or []) + + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) - for item_group in website_item_groups: - invalidate_cache_for(doc, item_group) + for item_group in website_item_groups: + invalidate_cache_for(doc, item_group) - if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: - invalidate_cache_for(doc, doc.old_item_group) + if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group: + invalidate_cache_for(doc, doc.old_item_group) def check_stock_uom_with_bin(item, stock_uom): - if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): - return + if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): + return - matched = True - ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + matched = True + ref_uom = frappe.db.get_value("Stock Ledger Entry", + {"item_code": item}, "stock_uom") - if ref_uom: - if cstr(ref_uom) != cstr(stock_uom): - matched = False - else: - bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) - for bin in bin_list: - if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 - or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): - matched = False - break + if ref_uom: + if cstr(ref_uom) != cstr(stock_uom): + matched = False + else: + bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) + for bin in bin_list: + if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 + or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): + matched = False + break - if matched and bin_list: - frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) + if matched and bin_list: + frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) - if not matched: - frappe.throw( - _("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) + if not matched: + frappe.throw( + _("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) From a8d5ef4891d8cf0476651508404aaae14f4df3f3 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 5 Feb 2018 13:06:30 +0100 Subject: [PATCH 19/22] space to tabs --- erpnext/accounts/doctype/sales_invoice/pos.py | 164 ++++++++++-------- 1 file changed, 91 insertions(+), 73 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index e37dbbf11c..ddc9cb940f 100644 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -17,48 +17,48 @@ 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")) + 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') + if not doc.company: + doc.company = pos_profile.get('company') - doc.update_stock = pos_profile.get('update_stock') + 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() + 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) + 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': 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() + } def get_meta(): @@ -68,7 +68,7 @@ def get_meta(): } for row in frappe.get_all('DocField', fields=['fieldname', 'options'], - filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): + filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): doctype_meta[row.fieldname] = frappe.get_meta(row.options) return doctype_meta @@ -106,7 +106,7 @@ def update_pos_profile_data(doc, pos_profile, company_data): 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 '' + 'tc_name'), 'terms') or doc.terms or '' doc.offline_pos_name = '' @@ -140,8 +140,8 @@ def update_multi_mode_option(doc, pos_profile): 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) + 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): @@ -206,7 +206,8 @@ def get_customers_address(customers): (select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s and parenttype = 'Address')""", data.name, as_dict=1) address_data = {} - if address: address_data = address[0] + if address: + address_data = address[0] address_data.update({'full_name': data.customer_name, 'customer_pos_id': data.customer_pos_id}) customer_address[data.name] = address_data @@ -275,27 +276,27 @@ def get_batch_no_data(): 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 + # 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) + 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) + 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 + return itemwise_barcode def get_item_tax_data(): - # get default tax of an item - # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} + # get default tax of an item + # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} itemwise_tax = {} taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax`""", as_dict=1) @@ -307,17 +308,19 @@ def get_item_tax_data(): return itemwise_tax + def get_price_list_data(selling_price_list): itemwise_price_list = {} price_lists = frappe.db.sql("""Select ifnull(price_list_rate, 0) as price_list_rate, item_code from `tabItem Price` ip where price_list = %(price_list)s""", - {'price_list': selling_price_list}, as_dict=1) + {'price_list': selling_price_list}, as_dict=1) for item in price_lists: itemwise_price_list[item.item_code] = item.price_list_rate return itemwise_price_list + def get_bin_data(pos_profile): itemwise_bin_data = {} cond = "1=1" @@ -334,6 +337,7 @@ def get_bin_data(pos_profile): return itemwise_bin_data + def get_pricing_rule_data(doc): pricing_rules = "" if doc.ignore_pricing_rule == 0: @@ -342,9 +346,10 @@ def get_pricing_rule_data(doc): and ifnull(company, '') in (%(company)s, '') and disable = 0 and %(date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31') order by priority desc, name desc""", - {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) + {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) return pricing_rules + @frappe.whitelist() def make_invoice(doc_list={}, email_queue_list={}, customers_list={}): if isinstance(doc_list, basestring): @@ -383,14 +388,16 @@ def make_invoice(doc_list={}, email_queue_list={}, customers_list={}): 'synced_contacts': get_contacts(customers) } + def validate_records(doc): validate_item(doc) + def get_customer_id(doc, customer=None): cust_id = None if doc.get('customer_pos_id'): cust_id = frappe.db.get_value('Customer', - {'customer_pos_id': doc.get('customer_pos_id')}, 'name') + {'customer_pos_id': doc.get('customer_pos_id')}, 'name') if not cust_id: customer = customer or doc.get('customer') @@ -401,6 +408,7 @@ def get_customer_id(doc, customer=None): return cust_id + def make_customer_and_address(customers): customers_list = [] for customer, data in customers.items(): @@ -417,6 +425,7 @@ def make_customer_and_address(customers): frappe.db.commit() return customers_list + def add_customer(data): customer_doc = frappe.new_doc('Customer') customer_doc.customer_name = data.get('full_name') or data.get('customer') @@ -425,28 +434,31 @@ def add_customer(data): customer_doc.customer_group = get_customer_group(data) customer_doc.territory = get_territory(data) customer_doc.flags.ignore_mandatory = True - customer_doc.save(ignore_permissions = True) + customer_doc.save(ignore_permissions=True) frappe.db.commit() return customer_doc.name + def get_territory(data): if data.get('territory'): return data.get('territory') return frappe.db.get_single_value('Selling Settings', - 'territory') or _('All Territories') + 'territory') or _('All Territories') + def get_customer_group(data): if data.get('customer_group'): return data.get('customer_group') return frappe.db.get_single_value('Selling Settings', - 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') + 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') -def make_contact(args,customer): + +def make_contact(args, customer): if args.get('email_id') or args.get('phone'): name = frappe.db.get_value('Dynamic Link', - {'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent') + {'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent') args = { 'first_name': args.get('full_name'), @@ -461,15 +473,17 @@ def make_contact(args,customer): doc.update(args) doc.is_primary_contact = 1 if not name: - doc.append('links',{ + doc.append('links', { 'link_doctype': 'Customer', 'link_name': customer }) doc.flags.ignore_mandatory = True doc.save(ignore_permissions=True) + def make_address(args, customer): - if not args.get('address_line1'): return + if not args.get('address_line1'): + return name = args.get('name') @@ -482,7 +496,7 @@ def make_address(args, customer): else: address = frappe.new_doc('Address') address.country = frappe.db.get_value('Company', args.get('company'), 'country') - address.append('links',{ + address.append('links', { 'link_doctype': 'Customer', 'link_name': customer }) @@ -491,7 +505,8 @@ def make_address(args, customer): address.is_shipping_address = 1 address.update(args) address.flags.ignore_mandatory = True - address.save(ignore_permissions = True) + address.save(ignore_permissions=True) + def make_email_queue(email_queue): name_list = [] @@ -500,15 +515,16 @@ def make_email_queue(email_queue): data = json.loads(data) sender = frappe.session.user print_format = "POS Invoice" - attachments = [frappe.attach_print('Sales Invoice', name, print_format= print_format)] + attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)] - make(subject = data.get('subject'), content = data.get('content'), recipients = data.get('recipients'), - sender=sender,attachments = attachments, send_email=True, + make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'), + sender=sender, attachments=attachments, send_email=True, doctype='Sales Invoice', name=name) name_list.append(key) return name_list + def validate_item(doc): for item in doc.get('items'): if not frappe.db.exists('Item', item.get('item_code')): @@ -531,13 +547,15 @@ def submit_invoice(si_doc, name, doc, name_list): frappe.db.commit() name_list.append(name) except Exception as e: - if frappe.message_log: frappe.message_log.pop() + if frappe.message_log: + frappe.message_log.pop() frappe.db.rollback() frappe.log_error(frappe.get_traceback()) name_list = save_invoice(doc, name, name_list) return name_list + def save_invoice(doc, name, name_list): try: if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): From 097a98cc10682f41dd516099416bb5e36a298973 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 5 Feb 2018 13:16:00 +0100 Subject: [PATCH 20/22] fixed spaces to tabs --- erpnext/stock/doctype/item/item.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 6d4f850692..6e6d816f49 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -5,19 +5,17 @@ from __future__ import unicode_literals import itertools import json - import erpnext import frappe from erpnext.controllers.item_variant import (ItemVariantExistsError, - copy_attributes_to_variant, - get_variant, - make_variant_item_code, - validate_item_variant_attributes) -from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, - invalidate_cache_for) + copy_attributes_to_variant, + get_variant, + make_variant_item_code, + validate_item_variant_attributes) +from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from frappe import _, msgprint from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, - now_datetime, random_string, strip) + now_datetime, random_string, strip) from frappe.utils.html_utils import clean_html from frappe.website.doctype.website_slideshow.website_slideshow import \ get_slideshow @@ -487,7 +485,8 @@ class Item(WebsiteGenerator): if len(self.barcodes) > 0: for item_barcode in self.barcodes: if item_barcode.barcode: - duplicate = frappe.db.sql("""select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + duplicate = frappe.db.sql( + """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) if duplicate: frappe.throw(_("Barcode {0} already used in Item {1}").format( item_barcode.barcode, duplicate[0][0])) From f9cb7ea61193719f07ba24910f085530e8c4d3a2 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 12 Feb 2018 10:05:59 +0100 Subject: [PATCH 21/22] removed extra spaces --- erpnext/accounts/doctype/sales_invoice/pos.py | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index ddc9cb940f..20eb14b26d 100644 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -68,7 +68,7 @@ def get_meta(): } for row in frappe.get_all('DocField', fields=['fieldname', 'options'], - filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): + filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): doctype_meta[row.fieldname] = frappe.get_meta(row.options) return doctype_meta @@ -105,8 +105,7 @@ 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 = '' @@ -141,7 +140,7 @@ def update_multi_mode_option(doc, pos_profile): 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) + `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): @@ -295,8 +294,8 @@ def get_barcode_data(items_list): def get_item_tax_data(): - # get default tax of an item - # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} + # get default tax of an item + # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} itemwise_tax = {} taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax`""", as_dict=1) @@ -313,7 +312,7 @@ def get_price_list_data(selling_price_list): itemwise_price_list = {} price_lists = frappe.db.sql("""Select ifnull(price_list_rate, 0) as price_list_rate, item_code from `tabItem Price` ip where price_list = %(price_list)s""", - {'price_list': selling_price_list}, as_dict=1) + {'price_list': selling_price_list}, as_dict=1) for item in price_lists: itemwise_price_list[item.item_code] = item.price_list_rate @@ -346,7 +345,7 @@ def get_pricing_rule_data(doc): and ifnull(company, '') in (%(company)s, '') and disable = 0 and %(date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31') order by priority desc, name desc""", - {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) + {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) return pricing_rules @@ -396,8 +395,7 @@ def validate_records(doc): def get_customer_id(doc, customer=None): cust_id = None if doc.get('customer_pos_id'): - cust_id = frappe.db.get_value('Customer', - {'customer_pos_id': doc.get('customer_pos_id')}, 'name') + cust_id = frappe.db.get_value('Customer',{'customer_pos_id': doc.get('customer_pos_id')}, 'name') if not cust_id: customer = customer or doc.get('customer') @@ -443,22 +441,20 @@ def get_territory(data): if data.get('territory'): return data.get('territory') - return frappe.db.get_single_value('Selling Settings', - 'territory') or _('All Territories') + return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories') def get_customer_group(data): if data.get('customer_group'): return data.get('customer_group') - return frappe.db.get_single_value('Selling Settings', - 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') + return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') def make_contact(args, customer): if args.get('email_id') or args.get('phone'): name = frappe.db.get_value('Dynamic Link', - {'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent') + {'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent') args = { 'first_name': args.get('full_name'), @@ -518,8 +514,8 @@ def make_email_queue(email_queue): attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)] make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'), - sender=sender, attachments=attachments, send_email=True, - doctype='Sales Invoice', name=name) + sender=sender, attachments=attachments, send_email=True, + doctype='Sales Invoice', name=name) name_list.append(key) return name_list From 04338cfb4b1b55379c74ddce19e4a1a97df23ba8 Mon Sep 17 00:00:00 2001 From: joezsweet Date: Mon, 12 Feb 2018 10:12:27 +0100 Subject: [PATCH 22/22] removed extra spaces --- erpnext/stock/doctype/item/item.py | 69 ++++++++++++++---------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 6e6d816f49..27f2c7b466 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -8,14 +8,11 @@ import json import erpnext import frappe from erpnext.controllers.item_variant import (ItemVariantExistsError, - copy_attributes_to_variant, - get_variant, - make_variant_item_code, - validate_item_variant_attributes) + copy_attributes_to_variant, get_variant, make_variant_item_code, validate_item_variant_attributes) from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for) from frappe import _, msgprint from frappe.utils import (cint, cstr, flt, formatdate, get_timestamp, getdate, - now_datetime, random_string, strip) + now_datetime, random_string, strip) from frappe.utils.html_utils import clean_html from frappe.website.doctype.website_slideshow.website_slideshow import \ get_slideshow @@ -113,8 +110,8 @@ class Item(WebsiteGenerator): if not self.get("__islocal"): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") self.old_website_item_groups = frappe.db.sql_list("""select item_group - from `tabWebsite Item Group` - where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) + from `tabWebsite Item Group` + where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name) def on_update(self): invalidate_cache_for_item(self) @@ -132,7 +129,7 @@ class Item(WebsiteGenerator): '''Add a new price''' if not price_list: price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) + or frappe.db.get_value('Price List', _('Standard Selling'))) if price_list: item_price = frappe.get_doc({ "doctype": "Item Price", @@ -158,19 +155,19 @@ class Item(WebsiteGenerator): # default warehouse, or Stores default_warehouse = (self.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse') - or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) + or frappe.db.get_single_value('Stock Settings', 'default_warehouse') + or frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')})) if default_warehouse: stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, - qty=self.opening_stock, rate=self.valuation_rate) + qty=self.opening_stock, rate=self.valuation_rate) stock_entry.add_comment("Comment", _("Opening Stock")) def make_route(self): if not self.route: return cstr(frappe.db.get_value('Item Group', self.item_group, - 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) + 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) def validate_website_image(self): """Validate if the website image is a public file""" @@ -193,7 +190,7 @@ class Item(WebsiteGenerator): if not file_doc: if not auto_set_website_image: frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") - .format(self.website_image, self.name)) + .format(self.website_image, self.name)) self.website_image = None @@ -294,8 +291,8 @@ class Item(WebsiteGenerator): # load variants # also used in set_attribute_context context.variants = frappe.get_all("Item", - filters={"variant_of": self.name, "show_variant_in_website": 1}, - order_by="name asc") + filters={"variant_of": self.name, "show_variant_in_website": 1}, + order_by="name asc") variant = frappe.form_dict.variant if not variant and context.variants: @@ -306,7 +303,7 @@ class Item(WebsiteGenerator): context.variant = frappe.get_doc("Item", variant) for fieldname in ("website_image", "web_long_description", "description", - "website_specifications"): + "website_specifications"): if context.variant.get(fieldname): value = context.variant.get(fieldname) if isinstance(value, list): @@ -329,7 +326,7 @@ class Item(WebsiteGenerator): # load attributes for v in context.variants: v.attributes = frappe.get_all("Item Variant Attribute", - fields=["attribute", "attribute_value"], filters={"parent": v.name}) + fields=["attribute", "attribute_value"], filters={"parent": v.name}) for attr in v.attributes: values = attribute_values_available.setdefault(attr.attribute, []) @@ -350,7 +347,7 @@ class Item(WebsiteGenerator): else: # get list of values defined (for sequence) for attr_value in frappe.db.get_all("Item Attribute Value", - fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): + fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"): if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): values.append(attr_value.attribute_value) @@ -426,7 +423,7 @@ class Item(WebsiteGenerator): for d in template.get("reorder_levels"): n = {} for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): + "warehouse_reorder_qty", "material_request_type"): n[k] = d.get(k) self.append("reorder_levels", n) @@ -506,7 +503,7 @@ class Item(WebsiteGenerator): warehouse += [d.get("warehouse")] else: frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) + .format(d.idx, d.warehouse), DuplicateReorderRows) if d.warehouse_reorder_level and not d.warehouse_reorder_qty: frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) @@ -526,7 +523,7 @@ class Item(WebsiteGenerator): def update_item_price(self): frappe.db.sql("""update `tabItem Price` set item_name=%s, item_description=%s, modified=NOW() where item_code=%s""", - (self.item_name, self.description, self.name)) + (self.item_name, self.description, self.name)) def on_trash(self): super(Item, self).on_trash() @@ -548,7 +545,7 @@ class Item(WebsiteGenerator): new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)] if new_properties != [cstr(self.get(fld)) for fld in field_list]: frappe.throw(_("To merge, following properties must be same for both items") - + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) + + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])) def after_rename(self, old_name, new_name, merge): if self.route: @@ -571,7 +568,7 @@ class Item(WebsiteGenerator): item_wise_tax_detail.pop(old_name) frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + json.dumps(item_wise_tax_detail), update_modified=False) def set_last_purchase_rate(self, new_name): last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) @@ -599,7 +596,7 @@ class Item(WebsiteGenerator): self.set("website_specifications", []) if self.item_group: for label, desc in frappe.db.get_values("Item Website Specification", - {"parent": self.item_group}, ["label", "description"]): + {"parent": self.item_group}, ["label", "description"]): row = self.append("website_specifications") row.label = label row.description = desc @@ -645,7 +642,7 @@ class Item(WebsiteGenerator): def update_variants(self): if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): return if self.has_variants: updated = [] @@ -666,9 +663,9 @@ class Item(WebsiteGenerator): def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (self._doc_before_save.has_variants != self.has_variants - or self._doc_before_save.variant_of != self.variant_of): + or self._doc_before_save.variant_of != self.variant_of): frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + StockExistsForTemplate) if self.has_variants or self.variant_of: if not self.is_child_table_same('attributes'): @@ -685,7 +682,7 @@ class Item(WebsiteGenerator): template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") if template_uom != self.stock_uom: frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + .format(self.stock_uom, template_uom)) def validate_attributes(self): if (self.has_variants or self.variant_of) and self.variant_based_on == 'Item Attribute': @@ -710,7 +707,7 @@ class Item(WebsiteGenerator): variant = get_variant(self.variant_of, args, self.name) if variant: frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + .format(variant), ItemVariantExistsError) validate_item_variant_attributes(self, args) @@ -793,18 +790,18 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): limit 1""", (item_code, cstr(doc_name)), as_dict=1) purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date - or "1900-01-01") + or "1900-01-01") purchase_receipt_date = getdate(last_purchase_receipt and - last_purchase_receipt[0].posting_date or "1900-01-01") + last_purchase_receipt[0].posting_date or "1900-01-01") if (purchase_order_date > purchase_receipt_date) or \ - (last_purchase_order and not last_purchase_receipt): + (last_purchase_order and not last_purchase_receipt): # use purchase order last_purchase = last_purchase_order[0] purchase_date = purchase_order_date elif (purchase_receipt_date > purchase_order_date) or \ - (last_purchase_receipt and not last_purchase_order): + (last_purchase_receipt and not last_purchase_order): # use purchase receipt last_purchase = last_purchase_receipt[0] purchase_date = purchase_receipt_date @@ -834,7 +831,7 @@ def invalidate_cache_for_item(doc): invalidate_cache_for(doc, doc.item_group) website_item_groups = list(set((doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) + + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) for item_group in website_item_groups: invalidate_cache_for(doc, item_group) @@ -849,7 +846,7 @@ def check_stock_uom_with_bin(item, stock_uom): matched = True ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + {"item_code": item}, "stock_uom") if ref_uom: if cstr(ref_uom) != cstr(stock_uom): @@ -858,7 +855,7 @@ def check_stock_uom_with_bin(item, stock_uom): bin_list = frappe.db.sql("select * from tabBin where item_code=%s", item, as_dict=1) for bin in bin_list: if (bin.reserved_qty > 0 or bin.ordered_qty > 0 or bin.indented_qty > 0 - or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): + or bin.planned_qty > 0) and cstr(bin.stock_uom) != cstr(stock_uom): matched = False break